3 * Docs & License: https://fullcalendar.io/
8 if (typeof define
=== 'function' && define
.amd
) {
9 define([ 'jquery', 'moment' ], factory
);
11 else if (typeof exports
=== 'object') { // Node/CommonJS
12 module
.exports
= factory(require('jquery'), require('moment'));
15 factory(jQuery
, moment
);
17 })(function($, moment
) {
21 var FC
= $.fullCalendar
= {
23 // When introducing internal API incompatibilities (where fullcalendar plugins would break),
24 // the minor version of the calendar should be upped (ex: 2.7.2 -> 2.8.0)
25 // and the below integer should be incremented.
28 var fcViews
= FC
.views
= {};
31 $.fn
.fullCalendar = function(options
) {
32 var args
= Array
.prototype.slice
.call(arguments
, 1); // for a possible method call
33 var res
= this; // what this function will return (this jQuery object by default)
35 this.each(function(i
, _element
) { // loop each DOM element involved
36 var element
= $(_element
);
37 var calendar
= element
.data('fullCalendar'); // get the existing calendar object (if any)
38 var singleRes
; // the returned value of this single method call
41 if (typeof options
=== 'string') {
42 if (calendar
&& $.isFunction(calendar
[options
])) {
43 singleRes
= calendar
[options
].apply(calendar
, args
);
45 res
= singleRes
; // record the first method call result
47 if (options
=== 'destroy') { // for the destroy method, must remove Calendar object data
48 element
.removeData('fullCalendar');
52 // a new calendar initialization
53 else if (!calendar
) { // don't initialize twice
54 calendar
= new Calendar(element
, options
);
55 element
.data('fullCalendar', calendar
);
64 var complexOptions
= [ // names of options that are objects whose properties should be combined
73 // Merges an array of option objects into a single object
74 function mergeOptions(optionObjs
) {
75 return mergeProps(optionObjs
, complexOptions
);
81 FC
.intersectRanges
= intersectRanges
;
82 FC
.applyAll
= applyAll
;
83 FC
.debounce
= debounce
;
85 FC
.htmlEscape
= htmlEscape
;
86 FC
.cssToStr
= cssToStr
;
88 FC
.capitaliseFirstLetter
= capitaliseFirstLetter
;
91 /* FullCalendar-specific DOM Utilities
92 ----------------------------------------------------------------------------------------------------------------------*/
95 // Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left
96 // and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that.
97 function compensateScroll(rowEls
, scrollbarWidths
) {
98 if (scrollbarWidths
.left
) {
100 'border-left-width': 1,
101 'margin-left': scrollbarWidths
.left
- 1
104 if (scrollbarWidths
.right
) {
106 'border-right-width': 1,
107 'margin-right': scrollbarWidths
.right
- 1
113 // Undoes compensateScroll and restores all borders/margins
114 function uncompensateScroll(rowEls
) {
118 'border-left-width': '',
119 'border-right-width': ''
124 // Make the mouse cursor express that an event is not allowed in the current area
125 function disableCursor() {
126 $('body').addClass('fc-not-allowed');
130 // Returns the mouse cursor to its original look
131 function enableCursor() {
132 $('body').removeClass('fc-not-allowed');
136 // Given a total available height to fill, have `els` (essentially child rows) expand to accomodate.
137 // By default, all elements that are shorter than the recommended height are expanded uniformly, not considering
138 // any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and
139 // reduces the available height.
140 function distributeHeight(els
, availableHeight
, shouldRedistribute
) {
142 // *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions,
143 // and it is better to be shorter than taller, to avoid creating unnecessary scrollbars.
145 var minOffset1
= Math
.floor(availableHeight
/ els
.length
); // for non-last element
146 var minOffset2
= Math
.floor(availableHeight
- minOffset1
* (els
.length
- 1)); // for last element *FLOORING NOTE*
147 var flexEls
= []; // elements that are allowed to expand. array of DOM nodes
148 var flexOffsets
= []; // amount of vertical space it takes up
149 var flexHeights
= []; // actual css height
152 undistributeHeight(els
); // give all elements their natural height
154 // find elements that are below the recommended height (expandable).
155 // important to query for heights in a single first pass (to avoid reflow oscillation).
156 els
.each(function(i
, el
) {
157 var minOffset
= i
=== els
.length
- 1 ? minOffset2
: minOffset1
;
158 var naturalOffset
= $(el
).outerHeight(true);
160 if (naturalOffset
< minOffset
) {
162 flexOffsets
.push(naturalOffset
);
163 flexHeights
.push($(el
).height());
166 // this element stretches past recommended height (non-expandable). mark the space as occupied.
167 usedHeight
+= naturalOffset
;
171 // readjust the recommended height to only consider the height available to non-maxed-out rows.
172 if (shouldRedistribute
) {
173 availableHeight
-= usedHeight
;
174 minOffset1
= Math
.floor(availableHeight
/ flexEls
.length
);
175 minOffset2
= Math
.floor(availableHeight
- minOffset1
* (flexEls
.length
- 1)); // *FLOORING NOTE*
178 // assign heights to all expandable elements
179 $(flexEls
).each(function(i
, el
) {
180 var minOffset
= i
=== flexEls
.length
- 1 ? minOffset2
: minOffset1
;
181 var naturalOffset
= flexOffsets
[i
];
182 var naturalHeight
= flexHeights
[i
];
183 var newHeight
= minOffset
- (naturalOffset
- naturalHeight
); // subtract the margin/padding
185 if (naturalOffset
< minOffset
) { // we check this again because redistribution might have changed things
186 $(el
).height(newHeight
);
192 // Undoes distrubuteHeight, restoring all els to their natural height
193 function undistributeHeight(els
) {
198 // Given `els`, a jQuery set of <td> cells, find the cell with the largest natural width and set the widths of all the
199 // cells to be that width.
200 // PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline
201 function matchCellWidths(els
) {
202 var maxInnerWidth
= 0;
204 els
.find('> *').each(function(i
, innerEl
) {
205 var innerWidth
= $(innerEl
).outerWidth();
206 if (innerWidth
> maxInnerWidth
) {
207 maxInnerWidth
= innerWidth
;
211 maxInnerWidth
++; // sometimes not accurate of width the text needs to stay on one line. insurance
213 els
.width(maxInnerWidth
);
215 return maxInnerWidth
;
219 // Given one element that resides inside another,
220 // Subtracts the height of the inner element from the outer element.
221 function subtractInnerElHeight(outerEl
, innerEl
) {
222 var both
= outerEl
.add(innerEl
);
225 // effin' IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked
227 position
: 'relative', // cause a reflow, which will force fresh dimension recalculation
228 left
: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll
230 diff
= outerEl
.outerHeight() - innerEl
.outerHeight(); // grab the dimensions
231 both
.css({ position
: '', left
: '' }); // undo hack
237 /* Element Geom Utilities
238 ----------------------------------------------------------------------------------------------------------------------*/
240 FC
.getOuterRect
= getOuterRect
;
241 FC
.getClientRect
= getClientRect
;
242 FC
.getContentRect
= getContentRect
;
243 FC
.getScrollbarWidths
= getScrollbarWidths
;
246 // borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51
247 function getScrollParent(el
) {
248 var position
= el
.css('position'),
249 scrollParent
= el
.parents().filter(function() {
250 var parent
= $(this);
251 return (/(auto|scroll)/).test(
252 parent
.css('overflow') + parent
.css('overflow-y') + parent
.css('overflow-x')
256 return position
=== 'fixed' || !scrollParent
.length
? $(el
[0].ownerDocument
|| document
) : scrollParent
;
260 // Queries the outer bounding area of a jQuery element.
261 // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
262 // Origin is optional.
263 function getOuterRect(el
, origin
) {
264 var offset
= el
.offset();
265 var left
= offset
.left
- (origin
? origin
.left
: 0);
266 var top
= offset
.top
- (origin
? origin
.top
: 0);
270 right
: left
+ el
.outerWidth(),
272 bottom
: top
+ el
.outerHeight()
277 // Queries the area within the margin/border/scrollbars of a jQuery element. Does not go within the padding.
278 // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
279 // Origin is optional.
280 // NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
281 function getClientRect(el
, origin
) {
282 var offset
= el
.offset();
283 var scrollbarWidths
= getScrollbarWidths(el
);
284 var left
= offset
.left
+ getCssFloat(el
, 'border-left-width') + scrollbarWidths
.left
- (origin
? origin
.left
: 0);
285 var top
= offset
.top
+ getCssFloat(el
, 'border-top-width') + scrollbarWidths
.top
- (origin
? origin
.top
: 0);
289 right
: left
+ el
[0].clientWidth
, // clientWidth includes padding but NOT scrollbars
291 bottom
: top
+ el
[0].clientHeight
// clientHeight includes padding but NOT scrollbars
296 // Queries the area within the margin/border/padding of a jQuery element. Assumed not to have scrollbars.
297 // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
298 // Origin is optional.
299 function getContentRect(el
, origin
) {
300 var offset
= el
.offset(); // just outside of border, margin not included
301 var left
= offset
.left
+ getCssFloat(el
, 'border-left-width') + getCssFloat(el
, 'padding-left') -
302 (origin
? origin
.left
: 0);
303 var top
= offset
.top
+ getCssFloat(el
, 'border-top-width') + getCssFloat(el
, 'padding-top') -
304 (origin
? origin
.top
: 0);
308 right
: left
+ el
.width(),
310 bottom
: top
+ el
.height()
315 // Returns the computed left/right/top/bottom scrollbar widths for the given jQuery element.
316 // NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
317 function getScrollbarWidths(el
) {
318 var leftRightWidth
= el
.innerWidth() - el
[0].clientWidth
; // the paddings cancel out, leaving the scrollbars
319 var bottomWidth
= el
.innerHeight() - el
[0].clientHeight
; // "
322 leftRightWidth
= sanitizeScrollbarWidth(leftRightWidth
);
323 bottomWidth
= sanitizeScrollbarWidth(bottomWidth
);
325 widths
= { left
: 0, right
: 0, top
: 0, bottom
: bottomWidth
};
327 if (getIsLeftRtlScrollbars() && el
.css('direction') == 'rtl') { // is the scrollbar on the left side?
328 widths
.left
= leftRightWidth
;
331 widths
.right
= leftRightWidth
;
338 // The scrollbar width computations in getScrollbarWidths are sometimes flawed when it comes to
339 // retina displays, rounding, and IE11. Massage them into a usable value.
340 function sanitizeScrollbarWidth(width
) {
341 width
= Math
.max(0, width
); // no negatives
342 width
= Math
.round(width
);
347 // Logic for determining if, when the element is right-to-left, the scrollbar appears on the left side
349 var _isLeftRtlScrollbars
= null;
351 function getIsLeftRtlScrollbars() { // responsible for caching the computation
352 if (_isLeftRtlScrollbars
=== null) {
353 _isLeftRtlScrollbars
= computeIsLeftRtlScrollbars();
355 return _isLeftRtlScrollbars
;
358 function computeIsLeftRtlScrollbars() { // creates an offscreen test element, then removes it
359 var el
= $('<div><div/></div>')
361 position
: 'absolute',
370 var innerEl
= el
.children();
371 var res
= innerEl
.offset().left
> el
.offset().left
; // is the inner div shifted to accommodate a left scrollbar?
377 // Retrieves a jQuery element's computed CSS value as a floating-point number.
378 // If the queried value is non-numeric (ex: IE can return "medium" for border width), will just return zero.
379 function getCssFloat(el
, prop
) {
380 return parseFloat(el
.css(prop
)) || 0;
384 /* Mouse / Touch Utilities
385 ----------------------------------------------------------------------------------------------------------------------*/
387 FC
.preventDefault
= preventDefault
;
390 // Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)
391 function isPrimaryMouseButton(ev
) {
392 return ev
.which
== 1 && !ev
.ctrlKey
;
396 function getEvX(ev
) {
397 var touches
= ev
.originalEvent
.touches
;
399 // on mobile FF, pageX for touch events is present, but incorrect,
400 // so, look at touch coordinates first.
401 if (touches
&& touches
.length
) {
402 return touches
[0].pageX
;
409 function getEvY(ev
) {
410 var touches
= ev
.originalEvent
.touches
;
412 // on mobile FF, pageX for touch events is present, but incorrect,
413 // so, look at touch coordinates first.
414 if (touches
&& touches
.length
) {
415 return touches
[0].pageY
;
422 function getEvIsTouch(ev
) {
423 return /^touch/.test(ev
.type
);
427 function preventSelection(el
) {
428 el
.addClass('fc-unselectable')
429 .on('selectstart', preventDefault
);
433 function allowSelection(el
) {
434 el
.removeClass('fc-unselectable')
435 .off('selectstart', preventDefault
);
439 // Stops a mouse/touch event from doing it's native browser action
440 function preventDefault(ev
) {
445 /* General Geometry Utils
446 ----------------------------------------------------------------------------------------------------------------------*/
448 FC
.intersectRects
= intersectRects
;
450 // Returns a new rectangle that is the intersection of the two rectangles. If they don't intersect, returns false
451 function intersectRects(rect1
, rect2
) {
453 left
: Math
.max(rect1
.left
, rect2
.left
),
454 right
: Math
.min(rect1
.right
, rect2
.right
),
455 top
: Math
.max(rect1
.top
, rect2
.top
),
456 bottom
: Math
.min(rect1
.bottom
, rect2
.bottom
)
459 if (res
.left
< res
.right
&& res
.top
< res
.bottom
) {
466 // Returns a new point that will have been moved to reside within the given rectangle
467 function constrainPoint(point
, rect
) {
469 left
: Math
.min(Math
.max(point
.left
, rect
.left
), rect
.right
),
470 top
: Math
.min(Math
.max(point
.top
, rect
.top
), rect
.bottom
)
475 // Returns a point that is the center of the given rectangle
476 function getRectCenter(rect
) {
478 left
: (rect
.left
+ rect
.right
) / 2,
479 top
: (rect
.top
+ rect
.bottom
) / 2
484 // Subtracts point2's coordinates from point1's coordinates, returning a delta
485 function diffPoints(point1
, point2
) {
487 left
: point1
.left
- point2
.left
,
488 top
: point1
.top
- point2
.top
493 /* Object Ordering by Field
494 ----------------------------------------------------------------------------------------------------------------------*/
496 FC
.parseFieldSpecs
= parseFieldSpecs
;
497 FC
.compareByFieldSpecs
= compareByFieldSpecs
;
498 FC
.compareByFieldSpec
= compareByFieldSpec
;
499 FC
.flexibleCompare
= flexibleCompare
;
502 function parseFieldSpecs(input
) {
507 if (typeof input
=== 'string') {
508 tokens
= input
.split(/\s*,\s*/);
510 else if (typeof input
=== 'function') {
513 else if ($.isArray(input
)) {
517 for (i
= 0; i
< tokens
.length
; i
++) {
520 if (typeof token
=== 'string') {
522 token
.charAt(0) == '-' ?
523 { field
: token
.substring(1), order
: -1 } :
524 { field
: token
, order
: 1 }
527 else if (typeof token
=== 'function') {
528 specs
.push({ func
: token
});
536 function compareByFieldSpecs(obj1
, obj2
, fieldSpecs
) {
540 for (i
= 0; i
< fieldSpecs
.length
; i
++) {
541 cmp
= compareByFieldSpec(obj1
, obj2
, fieldSpecs
[i
]);
551 function compareByFieldSpec(obj1
, obj2
, fieldSpec
) {
552 if (fieldSpec
.func
) {
553 return fieldSpec
.func(obj1
, obj2
);
555 return flexibleCompare(obj1
[fieldSpec
.field
], obj2
[fieldSpec
.field
]) *
556 (fieldSpec
.order
|| 1);
560 function flexibleCompare(a
, b
) {
570 if ($.type(a
) === 'string' || $.type(b
) === 'string') {
571 return String(a
).localeCompare(String(b
));
577 /* FullCalendar-specific Misc Utilities
578 ----------------------------------------------------------------------------------------------------------------------*/
581 // Computes the intersection of the two ranges. Will return fresh date clones in a range.
582 // Returns undefined if no intersection.
583 // Expects all dates to be normalized to the same timezone beforehand.
584 // TODO: move to date section?
585 function intersectRanges(subjectRange
, constraintRange
) {
586 var subjectStart
= subjectRange
.start
;
587 var subjectEnd
= subjectRange
.end
;
588 var constraintStart
= constraintRange
.start
;
589 var constraintEnd
= constraintRange
.end
;
590 var segStart
, segEnd
;
593 if (subjectEnd
> constraintStart
&& subjectStart
< constraintEnd
) { // in bounds at all?
595 if (subjectStart
>= constraintStart
) {
596 segStart
= subjectStart
.clone();
600 segStart
= constraintStart
.clone();
604 if (subjectEnd
<= constraintEnd
) {
605 segEnd
= subjectEnd
.clone();
609 segEnd
= constraintEnd
.clone();
624 ----------------------------------------------------------------------------------------------------------------------*/
626 FC
.computeIntervalUnit
= computeIntervalUnit
;
627 FC
.divideRangeByDuration
= divideRangeByDuration
;
628 FC
.divideDurationByDuration
= divideDurationByDuration
;
629 FC
.multiplyDuration
= multiplyDuration
;
630 FC
.durationHasTime
= durationHasTime
;
632 var dayIDs
= [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ];
633 var intervalUnits
= [ 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond' ];
636 // Diffs the two moments into a Duration where full-days are recorded first, then the remaining time.
637 // Moments will have their timezones normalized.
638 function diffDayTime(a
, b
) {
639 return moment
.duration({
640 days
: a
.clone().stripTime().diff(b
.clone().stripTime(), 'days'),
641 ms
: a
.time() - b
.time() // time-of-day from day start. disregards timezone
646 // Diffs the two moments via their start-of-day (regardless of timezone). Produces whole-day durations.
647 function diffDay(a
, b
) {
648 return moment
.duration({
649 days
: a
.clone().stripTime().diff(b
.clone().stripTime(), 'days')
654 // Diffs two moments, producing a duration, made of a whole-unit-increment of the given unit. Uses rounding.
655 function diffByUnit(a
, b
, unit
) {
656 return moment
.duration(
657 Math
.round(a
.diff(b
, unit
, true)), // returnFloat=true
663 // Computes the unit name of the largest whole-unit period of time.
664 // For example, 48 hours will be "days" whereas 49 hours will be "hours".
665 // Accepts start/end, a range object, or an original duration object.
666 function computeIntervalUnit(start
, end
) {
670 for (i
= 0; i
< intervalUnits
.length
; i
++) {
671 unit
= intervalUnits
[i
];
672 val
= computeRangeAs(unit
, start
, end
);
674 if (val
>= 1 && isInt(val
)) {
679 return unit
; // will be "milliseconds" if nothing else matches
683 // Computes the number of units (like "hours") in the given range.
684 // Range can be a {start,end} object, separate start/end args, or a Duration.
685 // Results are based on Moment's .as() and .diff() methods, so results can depend on internal handling
686 // of month-diffing logic (which tends to vary from version to version).
687 function computeRangeAs(unit
, start
, end
) {
689 if (end
!= null) { // given start, end
690 return end
.diff(start
, unit
, true);
692 else if (moment
.isDuration(start
)) { // given duration
693 return start
.as(unit
);
695 else { // given { start, end } range object
696 return start
.end
.diff(start
.start
, unit
, true);
701 // Intelligently divides a range (specified by a start/end params) by a duration
702 function divideRangeByDuration(start
, end
, dur
) {
705 if (durationHasTime(dur
)) {
706 return (end
- start
) / dur
;
708 months
= dur
.asMonths();
709 if (Math
.abs(months
) >= 1 && isInt(months
)) {
710 return end
.diff(start
, 'months', true) / months
;
712 return end
.diff(start
, 'days', true) / dur
.asDays();
716 // Intelligently divides one duration by another
717 function divideDurationByDuration(dur1
, dur2
) {
718 var months1
, months2
;
720 if (durationHasTime(dur1
) || durationHasTime(dur2
)) {
723 months1
= dur1
.asMonths();
724 months2
= dur2
.asMonths();
726 Math
.abs(months1
) >= 1 && isInt(months1
) &&
727 Math
.abs(months2
) >= 1 && isInt(months2
)
729 return months1
/ months2
;
731 return dur1
.asDays() / dur2
.asDays();
735 // Intelligently multiplies a duration by a number
736 function multiplyDuration(dur
, n
) {
739 if (durationHasTime(dur
)) {
740 return moment
.duration(dur
* n
);
742 months
= dur
.asMonths();
743 if (Math
.abs(months
) >= 1 && isInt(months
)) {
744 return moment
.duration({ months
: months
* n
});
746 return moment
.duration({ days
: dur
.asDays() * n
});
750 // Returns a boolean about whether the given duration has any time parts (hours/minutes/seconds/ms)
751 function durationHasTime(dur
) {
752 return Boolean(dur
.hours() || dur
.minutes() || dur
.seconds() || dur
.milliseconds());
756 function isNativeDate(input
) {
757 return Object
.prototype.toString
.call(input
) === '[object Date]' || input
instanceof Date
;
761 // Returns a boolean about whether the given input is a time string, like "06:40:00" or "06:00"
762 function isTimeString(str
) {
763 return /^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str
);
768 ----------------------------------------------------------------------------------------------------------------------*/
770 FC
.log = function() {
771 var console
= window
.console
;
773 if (console
&& console
.log
) {
774 return console
.log
.apply(console
, arguments
);
778 FC
.warn = function() {
779 var console
= window
.console
;
781 if (console
&& console
.warn
) {
782 return console
.warn
.apply(console
, arguments
);
785 return FC
.log
.apply(FC
, arguments
);
791 ----------------------------------------------------------------------------------------------------------------------*/
793 var hasOwnPropMethod
= {}.hasOwnProperty
;
796 // Merges an array of objects into a single object.
797 // The second argument allows for an array of property names who's object values will be merged together.
798 function mergeProps(propObjs
, complexProps
) {
806 for (i
= 0; i
< complexProps
.length
; i
++) {
807 name
= complexProps
[i
];
810 // collect the trailing object values, stopping when a non-object is discovered
811 for (j
= propObjs
.length
- 1; j
>= 0; j
--) {
812 val
= propObjs
[j
][name
];
814 if (typeof val
=== 'object') {
815 complexObjs
.unshift(val
);
817 else if (val
!== undefined) {
818 dest
[name
] = val
; // if there were no objects, this value will be used
823 // if the trailing values were objects, use the merged value
824 if (complexObjs
.length
) {
825 dest
[name
] = mergeProps(complexObjs
);
830 // copy values into the destination, going from last to first
831 for (i
= propObjs
.length
- 1; i
>= 0; i
--) {
834 for (name
in props
) {
835 if (!(name
in dest
)) { // if already assigned by previous props or complex props, don't reassign
836 dest
[name
] = props
[name
];
845 // Create an object that has the given prototype. Just like Object.create
846 function createObject(proto
) {
847 var f = function() {};
851 FC
.createObject
= createObject
;
854 function copyOwnProps(src
, dest
) {
855 for (var name
in src
) {
856 if (hasOwnProp(src
, name
)) {
857 dest
[name
] = src
[name
];
863 function hasOwnProp(obj
, name
) {
864 return hasOwnPropMethod
.call(obj
, name
);
868 // Is the given value a non-object non-function value?
869 function isAtomic(val
) {
870 return /undefined|null|boolean|number|string/.test($.type(val
));
874 function applyAll(functions
, thisObj
, args
) {
875 if ($.isFunction(functions
)) {
876 functions
= [ functions
];
881 for (i
=0; i
<functions
.length
; i
++) {
882 ret
= functions
[i
].apply(thisObj
, args
) || ret
;
889 function firstDefined() {
890 for (var i
=0; i
<arguments
.length
; i
++) {
891 if (arguments
[i
] !== undefined) {
898 function htmlEscape(s
) {
899 return (s
+ '').replace(/&/g
, '&')
900 .replace(/</g
, '<')
901 .replace(/>/g
, '>')
902 .replace(/'/g, ''')
903 .replace(/"/g, '"
;')
904 .replace(/\n/g, '<br
/>');
908 function stripHtmlEntities(text) {
909 return text.replace(/&.*?;/g, '');
913 // Given a hash of CSS properties, returns a string of CSS.
914 // Uses property names as-is (no camel-case conversion). Will not make statements for null/undefined values.
915 function cssToStr(cssProps) {
918 $.each(cssProps, function(name, val) {
920 statements.push(name + ':' + val);
924 return statements.join(';');
928 // Given an object hash of HTML attribute names to values,
929 // generates a string that can be injected between < > in HTML
930 function attrsToStr(attrs) {
933 $.each(attrs, function(name, val) {
935 parts.push(name + '="' + htmlEscape(val) + '"');
939 return parts.join(' ');
943 function capitaliseFirstLetter(str) {
944 return str.charAt(0).toUpperCase() + str.slice(1);
948 function compareNumbers(a, b) { // for .sort()
958 // Returns a method bound to the given object context.
959 // Just like one of the jQuery.proxy signatures, but without the undesired behavior of treating the same method with
960 // different contexts as identical when binding/unbinding events.
961 function proxy(obj, methodName) {
962 var method = obj[methodName];
965 return method.apply(obj, arguments);
970 // Returns a function, that, as long as it continues to be invoked, will not
971 // be triggered. The function will be called after it stops being called for
972 // N milliseconds. If `immediate` is passed, trigger the function on the
973 // leading edge, instead of the trailing.
974 // https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714
975 function debounce(func, wait, immediate) {
976 var timeout, args, context, timestamp, result;
978 var later = function() {
979 var last = +new Date() - timestamp;
981 timeout = setTimeout(later, wait - last);
986 result = func.apply(context, args);
987 context = args = null;
995 timestamp = +new Date();
996 var callNow = immediate && !timeout;
998 timeout = setTimeout(later, wait);
1001 result = func.apply(context, args);
1002 context = args = null;
1011 GENERAL NOTE on moments throughout the *entire rest* of the codebase:
1012 All moments are assumed to be ambiguously-zoned unless otherwise noted,
1013 with the NOTABLE EXCEOPTION of start/end dates that live on *Event Objects*.
1014 Ambiguously-TIMED moments are assumed to be ambiguously-zoned by nature.
1017 var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/;
1018 var ambigTimeOrZoneRegex =
1019 /^\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+)?)?)?)?)?$/;
1020 var newMomentProto = moment.fn; // where we will attach our new methods
1021 var oldMomentProto = $.extend({}, newMomentProto); // copy of original moment methods
1023 // tell momentjs to transfer these properties upon clone
1024 var momentProperties = moment.momentProperties;
1025 momentProperties.push('_fullCalendar
');
1026 momentProperties.push('_ambigTime
');
1027 momentProperties.push('_ambigZone
');
1031 // -------------------------------------------------------------------------------------------------
1033 // Creates a new moment, similar to the vanilla moment(...) constructor, but with
1034 // extra features (ambiguous time, enhanced formatting). When given an existing moment,
1035 // it will function as a clone (and retain the zone of the moment). Anything else will
1036 // result in a moment in the local zone.
1037 FC.moment = function() {
1038 return makeMoment(arguments);
1041 // Sames as FC.moment, but forces the resulting moment to be in the UTC timezone.
1042 FC.moment.utc = function() {
1043 var mom = makeMoment(arguments, true);
1045 // Force it into UTC because makeMoment doesn't guarantee it
1046 // (if given a pre-existing moment for example)
1047 if (mom
.hasTime()) { // don't give ambiguously-timed moments a UTC zone
1054 // Same as FC.moment, but when given an ISO8601 string, the timezone offset is preserved.
1055 // ISO8601 strings with no timezone offset will become ambiguously zoned.
1056 FC
.moment
.parseZone = function() {
1057 return makeMoment(arguments
, true, true);
1060 // Builds an enhanced moment from args. When given an existing moment, it clones. When given a
1061 // native Date, or called with no arguments (the current time), the resulting moment will be local.
1062 // Anything else needs to be "parsed" (a string or an array), and will be affected by:
1063 // parseAsUTC - if there is no zone information, should we parse the input in UTC?
1064 // parseZone - if there is zone information, should we force the zone of the moment?
1065 function makeMoment(args
, parseAsUTC
, parseZone
) {
1066 var input
= args
[0];
1067 var isSingleString
= args
.length
== 1 && typeof input
=== 'string';
1073 if (moment
.isMoment(input
) || isNativeDate(input
) || input
=== undefined) {
1074 mom
= moment
.apply(null, args
);
1076 else { // "parsing" is required
1077 isAmbigTime
= false;
1078 isAmbigZone
= false;
1080 if (isSingleString
) {
1081 if (ambigDateOfMonthRegex
.test(input
)) {
1082 // accept strings like '2014-05', but convert to the first of the month
1084 args
= [ input
]; // for when we pass it on to moment's constructor
1088 else if ((ambigMatch
= ambigTimeOrZoneRegex
.exec(input
))) {
1089 isAmbigTime
= !ambigMatch
[5]; // no time part?
1093 else if ($.isArray(input
)) {
1094 // arrays have no timezone information, so assume ambiguous zone
1097 // otherwise, probably a string with a format
1099 if (parseAsUTC
|| isAmbigTime
) {
1100 mom
= moment
.utc
.apply(moment
, args
);
1103 mom
= moment
.apply(null, args
);
1107 mom
._ambigTime
= true;
1108 mom
._ambigZone
= true; // ambiguous time always means ambiguous zone
1110 else if (parseZone
) { // let's record the inputted zone somehow
1112 mom
._ambigZone
= true;
1114 else if (isSingleString
) {
1115 mom
.utcOffset(input
); // if not a valid zone, will assign UTC
1120 mom
._fullCalendar
= true; // flag for extended functionality
1127 // -------------------------------------------------------------------------------------------------
1130 // Returns the week number, considering the locale's custom week number calcuation
1131 // `weeks` is an alias for `week`
1132 newMomentProto
.week
= newMomentProto
.weeks = function(input
) {
1133 var weekCalc
= this._locale
._fullCalendar_weekCalc
;
1135 if (input
== null && typeof weekCalc
=== 'function') { // custom function only works for getter
1136 return weekCalc(this);
1138 else if (weekCalc
=== 'ISO') {
1139 return oldMomentProto
.isoWeek
.apply(this, arguments
); // ISO getter/setter
1142 return oldMomentProto
.week
.apply(this, arguments
); // local getter/setter
1147 // -------------------------------------------------------------------------------------------------
1150 // Returns a Duration with the hours/minutes/seconds/ms values of the moment.
1151 // If the moment has an ambiguous time, a duration of 00:00 will be returned.
1154 // You can supply a Duration, a Moment, or a Duration-like argument.
1155 // When setting the time, and the moment has an ambiguous time, it then becomes unambiguous.
1156 newMomentProto
.time = function(time
) {
1158 // Fallback to the original method (if there is one) if this moment wasn't created via FullCalendar.
1159 // `time` is a generic enough method name where this precaution is necessary to avoid collisions w/ other plugins.
1160 if (!this._fullCalendar
) {
1161 return oldMomentProto
.time
.apply(this, arguments
);
1164 if (time
== null) { // getter
1165 return moment
.duration({
1166 hours
: this.hours(),
1167 minutes
: this.minutes(),
1168 seconds
: this.seconds(),
1169 milliseconds
: this.milliseconds()
1174 this._ambigTime
= false; // mark that the moment now has a time
1176 if (!moment
.isDuration(time
) && !moment
.isMoment(time
)) {
1177 time
= moment
.duration(time
);
1180 // The day value should cause overflow (so 24 hours becomes 00:00:00 of next day).
1181 // Only for Duration times, not Moment times.
1183 if (moment
.isDuration(time
)) {
1184 dayHours
= Math
.floor(time
.asDays()) * 24;
1187 // We need to set the individual fields.
1188 // Can't use startOf('day') then add duration. In case of DST at start of day.
1189 return this.hours(dayHours
+ time
.hours())
1190 .minutes(time
.minutes())
1191 .seconds(time
.seconds())
1192 .milliseconds(time
.milliseconds());
1196 // Converts the moment to UTC, stripping out its time-of-day and timezone offset,
1197 // but preserving its YMD. A moment with a stripped time will display no time
1198 // nor timezone offset when .format() is called.
1199 newMomentProto
.stripTime = function() {
1201 if (!this._ambigTime
) {
1203 this.utc(true); // keepLocalTime=true (for keeping *date* value)
1213 // Mark the time as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(),
1214 // which clears all ambig flags.
1215 this._ambigTime
= true;
1216 this._ambigZone
= true; // if ambiguous time, also ambiguous timezone offset
1219 return this; // for chaining
1222 // Returns if the moment has a non-ambiguous time (boolean)
1223 newMomentProto
.hasTime = function() {
1224 return !this._ambigTime
;
1229 // -------------------------------------------------------------------------------------------------
1231 // Converts the moment to UTC, stripping out its timezone offset, but preserving its
1232 // YMD and time-of-day. A moment with a stripped timezone offset will display no
1233 // timezone offset when .format() is called.
1234 newMomentProto
.stripZone = function() {
1237 if (!this._ambigZone
) {
1239 wasAmbigTime
= this._ambigTime
;
1241 this.utc(true); // keepLocalTime=true (for keeping date and time values)
1243 // the above call to .utc()/.utcOffset() unfortunately might clear the ambig flags, so restore
1244 this._ambigTime
= wasAmbigTime
|| false;
1246 // Mark the zone as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(),
1247 // which clears the ambig flags.
1248 this._ambigZone
= true;
1251 return this; // for chaining
1254 // Returns of the moment has a non-ambiguous timezone offset (boolean)
1255 newMomentProto
.hasZone = function() {
1256 return !this._ambigZone
;
1260 // implicitly marks a zone
1261 newMomentProto
.local = function(keepLocalTime
) {
1263 // for when converting from ambiguously-zoned to local,
1264 // keep the time values when converting from UTC -> local
1265 oldMomentProto
.local
.call(this, this._ambigZone
|| keepLocalTime
);
1267 // ensure non-ambiguous
1268 // this probably already happened via local() -> utcOffset(), but don't rely on Moment's internals
1269 this._ambigTime
= false;
1270 this._ambigZone
= false;
1272 return this; // for chaining
1276 // implicitly marks a zone
1277 newMomentProto
.utc = function(keepLocalTime
) {
1279 oldMomentProto
.utc
.call(this, keepLocalTime
);
1281 // ensure non-ambiguous
1282 // this probably already happened via utc() -> utcOffset(), but don't rely on Moment's internals
1283 this._ambigTime
= false;
1284 this._ambigZone
= false;
1290 // implicitly marks a zone (will probably get called upon .utc() and .local())
1291 newMomentProto
.utcOffset = function(tzo
) {
1293 if (tzo
!= null) { // setter
1294 // these assignments needs to happen before the original zone method is called.
1295 // I forget why, something to do with a browser crash.
1296 this._ambigTime
= false;
1297 this._ambigZone
= false;
1300 return oldMomentProto
.utcOffset
.apply(this, arguments
);
1305 // -------------------------------------------------------------------------------------------------
1307 newMomentProto
.format = function() {
1308 if (this._fullCalendar
&& arguments
[0]) { // an enhanced moment? and a format string provided?
1309 return formatDate(this, arguments
[0]); // our extended formatting
1311 if (this._ambigTime
) {
1312 return oldMomentFormat(this, 'YYYY-MM-DD');
1314 if (this._ambigZone
) {
1315 return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
1317 return oldMomentProto
.format
.apply(this, arguments
);
1320 newMomentProto
.toISOString = function() {
1321 if (this._ambigTime
) {
1322 return oldMomentFormat(this, 'YYYY-MM-DD');
1324 if (this._ambigZone
) {
1325 return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
1327 return oldMomentProto
.toISOString
.apply(this, arguments
);
1334 FC
.formatDate
= formatDate
;
1335 FC
.formatRange
= formatRange
;
1336 FC
.oldMomentFormat
= oldMomentFormat
;
1337 FC
.queryMostGranularFormatUnit
= queryMostGranularFormatUnit
;
1341 // ---------------------------------------------------------------------------------------------------------------------
1344 Inserted between chunks in the fake ("intermediate") formatting string.
1345 Important that it passes as whitespace (\s) because moment often identifies non-standalone months
1346 via a regexp with an \s.
1348 var PART_SEPARATOR
= '\u000b'; // vertical tab
1351 Inserted as the first character of a literal-text chunk to indicate that the literal text is not actually literal text,
1352 but rather, a "special" token that has custom rendering (see specialTokens map).
1354 var SPECIAL_TOKEN_MARKER
= '\u001f'; // information separator 1
1357 Inserted at the beginning and end of a span of text that must have non-zero numeric characters.
1358 Handling of these markers is done in a post-processing step at the very end of text rendering.
1360 var MAYBE_MARKER
= '\u001e'; // information separator 2
1361 var MAYBE_REGEXP
= new RegExp(MAYBE_MARKER
+ '([^' + MAYBE_MARKER
+ ']*)' + MAYBE_MARKER
, 'g'); // must be global
1364 Addition formatting tokens we want recognized
1366 var specialTokens
= {
1367 t: function(date
) { // "a" or "p"
1368 return oldMomentFormat(date
, 'a').charAt(0);
1370 T: function(date
) { // "A" or "P"
1371 return oldMomentFormat(date
, 'A').charAt(0);
1376 The first characters of formatting tokens for units that are 1 day or larger.
1377 `value` is for ranking relative size (lower means bigger).
1378 `unit` is a normalized unit, used for comparing moments.
1380 var largeTokenMap
= {
1381 Y
: { value
: 1, unit
: 'year' },
1382 M
: { value
: 2, unit
: 'month' },
1383 W
: { value
: 3, unit
: 'week' }, // ISO week
1384 w
: { value
: 3, unit
: 'week' }, // local week
1385 D
: { value
: 4, unit
: 'day' }, // day of month
1386 d
: { value
: 4, unit
: 'day' } // day of week
1390 // Single Date Formatting
1391 // ---------------------------------------------------------------------------------------------------------------------
1394 Formats `date` with a Moment formatting string, but allow our non-zero areas and special token
1396 function formatDate(date
, formatStr
) {
1397 return renderFakeFormatString(
1398 getParsedFormatString(formatStr
).fakeFormatString
,
1404 Call this if you want Moment's original format method to be used
1406 function oldMomentFormat(mom
, formatStr
) {
1407 return oldMomentProto
.format
.call(mom
, formatStr
); // oldMomentProto defined in moment-ext.js
1411 // Date Range Formatting
1412 // -------------------------------------------------------------------------------------------------
1413 // TODO: make it work with timezone offset
1416 Using a formatting string meant for a single date, generate a range string, like
1417 "Sep 2 - 9 2013", that intelligently inserts a separator where the dates differ.
1418 If the dates are the same as far as the format string is concerned, just return a single
1419 rendering of one date, without any separator.
1421 function formatRange(date1
, date2
, formatStr
, separator
, isRTL
) {
1424 date1
= FC
.moment
.parseZone(date1
);
1425 date2
= FC
.moment
.parseZone(date2
);
1427 localeData
= date1
.localeData();
1429 // Expand localized format strings, like "LL" -> "MMMM D YYYY".
1430 // BTW, this is not important for `formatDate` because it is impossible to put custom tokens
1431 // or non-zero areas in Moment's localized format strings.
1432 formatStr
= localeData
.longDateFormat(formatStr
) || formatStr
;
1434 return renderParsedFormat(
1435 getParsedFormatString(formatStr
),
1444 Renders a range with an already-parsed format string.
1446 function renderParsedFormat(parsedFormat
, date1
, date2
, separator
, isRTL
) {
1447 var sameUnits
= parsedFormat
.sameUnits
;
1448 var unzonedDate1
= date1
.clone().stripZone(); // for same-unit comparisons
1449 var unzonedDate2
= date2
.clone().stripZone(); // "
1451 var renderedParts1
= renderFakeFormatStringParts(parsedFormat
.fakeFormatString
, date1
);
1452 var renderedParts2
= renderFakeFormatStringParts(parsedFormat
.fakeFormatString
, date2
);
1459 var middleStr1
= '';
1460 var middleStr2
= '';
1463 // Start at the leftmost side of the formatting string and continue until you hit a token
1464 // that is not the same between dates.
1467 leftI
< sameUnits
.length
&& (!sameUnits
[leftI
] || unzonedDate1
.isSame(unzonedDate2
, sameUnits
[leftI
]));
1470 leftStr
+= renderedParts1
[leftI
];
1473 // Similarly, start at the rightmost side of the formatting string and move left
1475 rightI
= sameUnits
.length
- 1;
1476 rightI
> leftI
&& (!sameUnits
[rightI
] || unzonedDate1
.isSame(unzonedDate2
, sameUnits
[rightI
]));
1479 // If current chunk is on the boundary of unique date-content, and is a special-case
1480 // date-formatting postfix character, then don't consume it. Consider it unique date-content.
1481 // TODO: make configurable
1482 if (rightI
- 1 === leftI
&& renderedParts1
[rightI
] === '.') {
1486 rightStr
= renderedParts1
[rightI
] + rightStr
;
1489 // The area in the middle is different for both of the dates.
1490 // Collect them distinctly so we can jam them together later.
1491 for (middleI
= leftI
; middleI
<= rightI
; middleI
++) {
1492 middleStr1
+= renderedParts1
[middleI
];
1493 middleStr2
+= renderedParts2
[middleI
];
1496 if (middleStr1
|| middleStr2
) {
1498 middleStr
= middleStr2
+ separator
+ middleStr1
;
1501 middleStr
= middleStr1
+ separator
+ middleStr2
;
1505 return processMaybeMarkers(
1506 leftStr
+ middleStr
+ rightStr
1511 // Format String Parsing
1512 // ---------------------------------------------------------------------------------------------------------------------
1514 var parsedFormatStrCache
= {};
1517 Returns a parsed format string, leveraging a cache.
1519 function getParsedFormatString(formatStr
) {
1520 return parsedFormatStrCache
[formatStr
] ||
1521 (parsedFormatStrCache
[formatStr
] = parseFormatString(formatStr
));
1525 Parses a format string into the following:
1526 - fakeFormatString: a momentJS formatting string, littered with special control characters that get post-processed.
1527 - sameUnits: for every part in fakeFormatString, if the part is a token, the value will be a unit string (like "day"),
1528 that indicates how similar a range's start & end must be in order to share the same formatted text.
1529 If not a token, then the value is null.
1530 Always a flat array (not nested liked "chunks").
1532 function parseFormatString(formatStr
) {
1533 var chunks
= chunkFormatString(formatStr
);
1536 fakeFormatString
: buildFakeFormatString(chunks
),
1537 sameUnits
: buildSameUnits(chunks
)
1542 Break the formatting string into an array of chunks.
1543 A 'maybe' chunk will have nested chunks.
1545 function chunkFormatString(formatStr
) {
1549 // TODO: more descrimination
1550 // \4 is a backreference to the first character of a multi-character set.
1551 var chunker
= /\[([^\]]*)\]|\(([^\)]*)\)|(LTS|LT|(\w)\4*o?)|([^\w\[\(]+)/g;
1553 while ((match
= chunker
.exec(formatStr
))) {
1554 if (match
[1]) { // a literal string inside [ ... ]
1555 chunks
.push
.apply(chunks
, // append
1556 splitStringLiteral(match
[1])
1559 else if (match
[2]) { // non-zero formatting inside ( ... )
1560 chunks
.push({ maybe
: chunkFormatString(match
[2]) });
1562 else if (match
[3]) { // a formatting token
1563 chunks
.push({ token
: match
[3] });
1565 else if (match
[5]) { // an unenclosed literal string
1566 chunks
.push
.apply(chunks
, // append
1567 splitStringLiteral(match
[5])
1576 Potentially splits a literal-text string into multiple parts. For special cases.
1578 function splitStringLiteral(s
) {
1580 return [ '.', ' ' ]; // for locales with periods bound to the end of each year/month/date
1588 Given chunks parsed from a real format string, generate a fake (aka "intermediate") format string with special control
1589 characters that will eventually be given to moment for formatting, and then post-processed.
1591 function buildFakeFormatString(chunks
) {
1595 for (i
= 0; i
< chunks
.length
; i
++) {
1598 if (typeof chunk
=== 'string') {
1599 parts
.push('[' + chunk
+ ']');
1601 else if (chunk
.token
) {
1602 if (chunk
.token
in specialTokens
) {
1604 SPECIAL_TOKEN_MARKER
+ // useful during post-processing
1605 '[' + chunk
.token
+ ']' // preserve as literal text
1609 parts
.push(chunk
.token
); // unprotected text implies a format string
1612 else if (chunk
.maybe
) {
1614 MAYBE_MARKER
+ // useful during post-processing
1615 buildFakeFormatString(chunk
.maybe
) +
1621 return parts
.join(PART_SEPARATOR
);
1625 Given parsed chunks from a real formatting string, generates an array of unit strings (like "day") that indicate
1626 in which regard two dates must be similar in order to share range formatting text.
1627 The `chunks` can be nested (because of "maybe" chunks), however, the returned array will be flat.
1629 function buildSameUnits(chunks
) {
1634 for (i
= 0; i
< chunks
.length
; i
++) {
1638 tokenInfo
= largeTokenMap
[chunk
.token
.charAt(0)];
1639 units
.push(tokenInfo
? tokenInfo
.unit
: 'second'); // default to a very strict same-second
1641 else if (chunk
.maybe
) {
1642 units
.push
.apply(units
, // append
1643 buildSameUnits(chunk
.maybe
)
1655 // Rendering to text
1656 // ---------------------------------------------------------------------------------------------------------------------
1659 Formats a date with a fake format string, post-processes the control characters, then returns.
1661 function renderFakeFormatString(fakeFormatString
, date
) {
1662 return processMaybeMarkers(
1663 renderFakeFormatStringParts(fakeFormatString
, date
).join('')
1668 Formats a date into parts that will have been post-processed, EXCEPT for the "maybe" markers.
1670 function renderFakeFormatStringParts(fakeFormatString
, date
) {
1672 var fakeRender
= oldMomentFormat(date
, fakeFormatString
);
1673 var fakeParts
= fakeRender
.split(PART_SEPARATOR
);
1676 for (i
= 0; i
< fakeParts
.length
; i
++) {
1677 fakePart
= fakeParts
[i
];
1679 if (fakePart
.charAt(0) === SPECIAL_TOKEN_MARKER
) {
1681 // the literal string IS the token's name.
1682 // call special token's registered function.
1683 specialTokens
[fakePart
.substring(1)](date
)
1687 parts
.push(fakePart
);
1695 Accepts an almost-finally-formatted string and processes the "maybe" control characters, returning a new string.
1697 function processMaybeMarkers(s
) {
1698 return s
.replace(MAYBE_REGEXP
, function(m0
, m1
) { // regex assumed to have 'g' flag
1699 if (m1
.match(/[1-9]/)) { // any non-zero numeric characters?
1710 // -------------------------------------------------------------------------------------------------
1713 Returns a unit string, either 'year', 'month', 'day', or null for the most granular formatting token in the string.
1715 function queryMostGranularFormatUnit(formatStr
) {
1716 var chunks
= chunkFormatString(formatStr
);
1721 for (i
= 0; i
< chunks
.length
; i
++) {
1725 candidate
= largeTokenMap
[chunk
.token
.charAt(0)];
1727 if (!best
|| candidate
.value
> best
.value
) {
1743 // quick local references
1744 var formatDate
= FC
.formatDate
;
1745 var formatRange
= FC
.formatRange
;
1746 var oldMomentFormat
= FC
.oldMomentFormat
;
1750 FC
.Class
= Class
; // export
1752 // Class that all other classes will inherit from
1753 function Class() { }
1756 // Called on a class to create a subclass.
1757 // Last argument contains instance methods. Any argument before the last are considered mixins.
1758 Class
.extend = function() {
1759 var len
= arguments
.length
;
1763 for (i
= 0; i
< len
; i
++) {
1764 members
= arguments
[i
];
1765 if (i
< len
- 1) { // not the last argument?
1766 mixIntoClass(this, members
);
1770 return extendClass(this, members
|| {}); // members will be undefined if no arguments
1774 // Adds new member variables/methods to the class's prototype.
1775 // Can be called with another class, or a plain object hash containing new members.
1776 Class
.mixin = function(members
) {
1777 mixIntoClass(this, members
);
1781 function extendClass(superClass
, members
) {
1784 // ensure a constructor for the subclass, forwarding all arguments to the super-constructor if it doesn't exist
1785 if (hasOwnProp(members
, 'constructor')) {
1786 subClass
= members
.constructor;
1788 if (typeof subClass
!== 'function') {
1789 subClass
= members
.constructor = function() {
1790 superClass
.apply(this, arguments
);
1794 // build the base prototype for the subclass, which is an new object chained to the superclass's prototype
1795 subClass
.prototype = createObject(superClass
.prototype);
1797 // copy each member variable/method onto the the subclass's prototype
1798 copyOwnProps(members
, subClass
.prototype);
1800 // copy over all class variables/methods to the subclass, such as `extend` and `mixin`
1801 copyOwnProps(superClass
, subClass
);
1807 function mixIntoClass(theClass
, members
) {
1808 copyOwnProps(members
, theClass
.prototype);
1813 Wrap jQuery's Deferred Promise object to be slightly more Promise/A+ compliant.
1814 With the added non-standard feature of synchronously executing handlers on resolved promises,
1815 which doesn't always happen otherwise (esp with nested .then handlers!?),
1816 so, this makes things a lot easier, esp because jQuery 3 changed the synchronicity for Deferred objects.
1818 TODO: write tests and more comments
1821 function Promise(executor
) {
1822 var deferred
= $.Deferred();
1823 var promise
= deferred
.promise();
1825 if (typeof executor
=== 'function') {
1827 function(value
) { // resolve
1828 if (Promise
.immediate
) {
1829 promise
._value
= value
;
1831 deferred
.resolve(value
);
1833 function() { // reject
1839 if (Promise
.immediate
) {
1840 var origThen
= promise
.then
;
1842 promise
.then = function(onFulfilled
, onRejected
) {
1843 var state
= promise
.state();
1845 if (state
=== 'resolved') {
1846 if (typeof onFulfilled
=== 'function') {
1847 return Promise
.resolve(onFulfilled(promise
._value
));
1850 else if (state
=== 'rejected') {
1851 if (typeof onRejected
=== 'function') {
1853 return promise
; // already rejected
1857 return origThen
.call(promise
, onFulfilled
, onRejected
);
1861 return promise
; // instanceof Promise will break :( TODO: make Promise a real class
1864 FC
.Promise
= Promise
;
1866 Promise
.immediate
= true;
1869 Promise
.resolve = function(value
) {
1870 if (value
&& typeof value
.resolve
=== 'function') {
1871 return value
.promise();
1873 if (value
&& typeof value
.then
=== 'function') {
1877 var deferred
= $.Deferred().resolve(value
);
1878 var promise
= deferred
.promise();
1880 if (Promise
.immediate
) {
1881 var origThen
= promise
.then
;
1883 promise
._value
= value
;
1885 promise
.then = function(onFulfilled
, onRejected
) {
1886 if (typeof onFulfilled
=== 'function') {
1887 return Promise
.resolve(onFulfilled(value
));
1889 return origThen
.call(promise
, onFulfilled
, onRejected
);
1898 Promise
.reject = function() {
1899 return $.Deferred().reject().promise();
1903 Promise
.all = function(inputs
) {
1904 var hasAllValues
= false;
1908 if (Promise
.immediate
) {
1909 hasAllValues
= true;
1912 for (i
= 0; i
< inputs
.length
; i
++) {
1915 if (input
&& typeof input
.state
=== 'function' && input
.state() === 'resolved' && ('_value' in input
)) {
1916 values
.push(input
._value
);
1918 else if (input
&& typeof input
.then
=== 'function') {
1919 hasAllValues
= false;
1929 return Promise
.resolve(values
);
1932 return $.when
.apply($.when
, inputs
).then(function() {
1933 return $.when($.makeArray(arguments
));
1940 // TODO: write tests and clean up code
1942 function TaskQueue(debounceWait
) {
1943 var q
= []; // array of runFuncs
1945 function addTask(taskFunc
) {
1946 return new Promise(function(resolve
) {
1948 // should run this function when it's taskFunc's turn to run.
1949 // responsible for popping itself off the queue.
1950 var runFunc = function() {
1951 Promise
.resolve(taskFunc()) // result might be async, coerce to promise
1952 .then(resolve
) // resolve TaskQueue::push's promise, for the caller. will receive result of taskFunc.
1954 q
.shift(); // pop itself off
1956 // run the next task, if any
1963 // always put the task at the end of the queue, BEFORE running the task
1966 // if it's the only task in the queue, run immediately
1967 if (q
.length
=== 1) {
1973 this.add
= // potentially debounce, for the public method
1974 typeof debounceWait
=== 'number' ?
1975 debounce(addTask
, debounceWait
) :
1976 addTask
; // if not a number (null/undefined/false), no debounce at all
1978 this.addQuickly
= addTask
; // guaranteed no debounce
1981 FC
.TaskQueue
= TaskQueue
;
1984 q = new TaskQueue();
1987 return q.push(function() {
1989 console.log('work' + i);
1995 function trigger() {
2007 var EmitterMixin
= FC
.EmitterMixin
= {
2009 // jQuery-ification via $(this) allows a non-DOM object to have
2010 // the same event handling capabilities (including namespaces).
2013 on: function(types
, handler
) {
2014 $(this).on(types
, this._prepareIntercept(handler
));
2015 return this; // for chaining
2019 one: function(types
, handler
) {
2020 $(this).one(types
, this._prepareIntercept(handler
));
2021 return this; // for chaining
2025 _prepareIntercept: function(handler
) {
2026 // handlers are always called with an "event" object as their first param.
2027 // sneak the `this` context and arguments into the extra parameter object
2028 // and forward them on to the original handler.
2029 var intercept = function(ev
, extra
) {
2030 return handler
.apply(
2031 extra
.context
|| this,
2036 // mimick jQuery's internal "proxy" system (risky, I know)
2037 // causing all functions with the same .guid to appear to be the same.
2038 // https://github.com/jquery/jquery/blob/2.2.4/src/core.js#L448
2039 // this is needed for calling .off with the original non-intercept handler.
2040 if (!handler
.guid
) {
2041 handler
.guid
= $.guid
++;
2043 intercept
.guid
= handler
.guid
;
2049 off: function(types
, handler
) {
2050 $(this).off(types
, handler
);
2052 return this; // for chaining
2056 trigger: function(types
) {
2057 var args
= Array
.prototype.slice
.call(arguments
, 1); // arguments after the first
2059 // pass in "extra" info to the intercept
2060 $(this).triggerHandler(types
, { args
: args
});
2062 return this; // for chaining
2066 triggerWith: function(types
, context
, args
) {
2068 // `triggerHandler` is less reliant on the DOM compared to `trigger`.
2069 // pass in "extra" info to the intercept.
2070 $(this).triggerHandler(types
, { context
: context
, args
: args
});
2072 return this; // for chaining
2080 Utility methods for easily listening to events on another object,
2081 and more importantly, easily unlistening from them.
2083 var ListenerMixin
= FC
.ListenerMixin
= (function() {
2085 var ListenerMixin
= {
2090 Given an `other` object that has on/off methods, bind the given `callback` to an event by the given name.
2091 The `callback` will be called with the `this` context of the object that .listenTo is being called on.
2093 .listenTo(other, eventName, callback)
2096 eventName1: callback1,
2097 eventName2: callback2
2100 listenTo: function(other
, arg
, callback
) {
2101 if (typeof arg
=== 'object') { // given dictionary of callbacks
2102 for (var eventName
in arg
) {
2103 if (arg
.hasOwnProperty(eventName
)) {
2104 this.listenTo(other
, eventName
, arg
[eventName
]);
2108 else if (typeof arg
=== 'string') {
2110 arg
+ '.' + this.getListenerNamespace(), // use event namespacing to identify this object
2111 $.proxy(callback
, this) // always use `this` context
2112 // the usually-undesired jQuery guid behavior doesn't matter,
2113 // because we always unbind via namespace
2119 Causes the current object to stop listening to events on the `other` object.
2120 `eventName` is optional. If omitted, will stop listening to ALL events on `other`.
2122 stopListeningTo: function(other
, eventName
) {
2123 other
.off((eventName
|| '') + '.' + this.getListenerNamespace());
2127 Returns a string, unique to this object, to be used for event namespacing
2129 getListenerNamespace: function() {
2130 if (this.listenerId
== null) {
2131 this.listenerId
= guid
++;
2133 return '_listener' + this.listenerId
;
2137 return ListenerMixin
;
2141 /* A rectangular panel that is absolutely positioned over other content
2142 ------------------------------------------------------------------------------------------------------------------------
2144 - className (string)
2145 - content (HTML string or jQuery element set)
2149 - right (the x coord of where the right edge should be. not a "CSS" right)
2150 - autoHide (boolean)
2155 var Popover
= Class
.extend(ListenerMixin
, {
2159 el
: null, // the container element for the popover. generated by this object
2160 margin
: 10, // the space required between the popover and the edges of the scroll container
2163 constructor: function(options
) {
2164 this.options
= options
|| {};
2168 // Shows the popover on the specified position. Renders it if not already
2170 if (this.isHidden
) {
2176 this.isHidden
= false;
2177 this.trigger('show');
2182 // Hides the popover, through CSS, but does not remove it from the DOM
2184 if (!this.isHidden
) {
2186 this.isHidden
= true;
2187 this.trigger('hide');
2192 // Creates `this.el` and renders content inside of it
2193 render: function() {
2195 var options
= this.options
;
2197 this.el
= $('<div class="fc-popover"/>')
2198 .addClass(options
.className
|| '')
2200 // position initially to the top left to avoid creating scrollbars
2204 .append(options
.content
)
2205 .appendTo(options
.parentEl
);
2207 // when a click happens on anything inside with a 'fc-close' className, hide the popover
2208 this.el
.on('click', '.fc-close', function() {
2212 if (options
.autoHide
) {
2213 this.listenTo($(document
), 'mousedown', this.documentMousedown
);
2218 // Triggered when the user clicks *anywhere* in the document, for the autoHide feature
2219 documentMousedown: function(ev
) {
2220 // only hide the popover if the click happened outside the popover
2221 if (this.el
&& !$(ev
.target
).closest(this.el
).length
) {
2227 // Hides and unregisters any handlers
2228 removeElement: function() {
2236 this.stopListeningTo($(document
), 'mousedown');
2240 // Positions the popover optimally, using the top/left/right options
2241 position: function() {
2242 var options
= this.options
;
2243 var origin
= this.el
.offsetParent().offset();
2244 var width
= this.el
.outerWidth();
2245 var height
= this.el
.outerHeight();
2246 var windowEl
= $(window
);
2247 var viewportEl
= getScrollParent(this.el
);
2251 var top
; // the "position" (not "offset") values for the popover
2254 // compute top and left
2255 top
= options
.top
|| 0;
2256 if (options
.left
!== undefined) {
2257 left
= options
.left
;
2259 else if (options
.right
!== undefined) {
2260 left
= options
.right
- width
; // derive the left value from the right value
2266 if (viewportEl
.is(window
) || viewportEl
.is(document
)) { // normalize getScrollParent's result
2267 viewportEl
= windowEl
;
2268 viewportTop
= 0; // the window is always at the top left
2269 viewportLeft
= 0; // (and .offset() won't work if called here)
2272 viewportOffset
= viewportEl
.offset();
2273 viewportTop
= viewportOffset
.top
;
2274 viewportLeft
= viewportOffset
.left
;
2277 // if the window is scrolled, it causes the visible area to be further down
2278 viewportTop
+= windowEl
.scrollTop();
2279 viewportLeft
+= windowEl
.scrollLeft();
2281 // constrain to the view port. if constrained by two edges, give precedence to top/left
2282 if (options
.viewportConstrain
!== false) {
2283 top
= Math
.min(top
, viewportTop
+ viewportEl
.outerHeight() - height
- this.margin
);
2284 top
= Math
.max(top
, viewportTop
+ this.margin
);
2285 left
= Math
.min(left
, viewportLeft
+ viewportEl
.outerWidth() - width
- this.margin
);
2286 left
= Math
.max(left
, viewportLeft
+ this.margin
);
2290 top
: top
- origin
.top
,
2291 left
: left
- origin
.left
2296 // Triggers a callback. Calls a function in the option hash of the same name.
2297 // Arguments beyond the first `name` are forwarded on.
2298 // TODO: better code reuse for this. Repeat code
2299 trigger: function(name
) {
2300 if (this.options
[name
]) {
2301 this.options
[name
].apply(this, Array
.prototype.slice
.call(arguments
, 1));
2310 A cache for the left/right/top/bottom/width/height values for one or more elements.
2311 Works with both offset (from topleft document) and position (from offsetParent).
2318 var CoordCache
= FC
.CoordCache
= Class
.extend({
2320 els
: null, // jQuery set (assumed to be siblings)
2321 forcedOffsetParentEl
: null, // options can override the natural offsetParent
2322 origin
: null, // {left,top} position of offsetParent of els
2323 boundingRect
: null, // constrain cordinates to this rectangle. {left,right,top,bottom} or null
2324 isHorizontal
: false, // whether to query for left/right/width
2325 isVertical
: false, // whether to query for top/bottom/height
2327 // arrays of coordinates (offsets from topleft of document)
2334 constructor: function(options
) {
2335 this.els
= $(options
.els
);
2336 this.isHorizontal
= options
.isHorizontal
;
2337 this.isVertical
= options
.isVertical
;
2338 this.forcedOffsetParentEl
= options
.offsetParent
? $(options
.offsetParent
) : null;
2342 // Queries the els for coordinates and stores them.
2343 // Call this method before using and of the get* methods below.
2345 var offsetParentEl
= this.forcedOffsetParentEl
;
2346 if (!offsetParentEl
&& this.els
.length
> 0) {
2347 offsetParentEl
= this.els
.eq(0).offsetParent();
2350 this.origin
= offsetParentEl
?
2351 offsetParentEl
.offset() :
2354 this.boundingRect
= this.queryBoundingRect();
2356 if (this.isHorizontal
) {
2357 this.buildElHorizontals();
2359 if (this.isVertical
) {
2360 this.buildElVerticals();
2365 // Destroys all internal data about coordinates, freeing memory
2368 this.boundingRect
= null;
2372 this.bottoms
= null;
2376 // When called, if coord caches aren't built, builds them
2377 ensureBuilt: function() {
2384 // Populates the left/right internal coordinate arrays
2385 buildElHorizontals: function() {
2389 this.els
.each(function(i
, node
) {
2391 var left
= el
.offset().left
;
2392 var width
= el
.outerWidth();
2395 rights
.push(left
+ width
);
2399 this.rights
= rights
;
2403 // Populates the top/bottom internal coordinate arrays
2404 buildElVerticals: function() {
2408 this.els
.each(function(i
, node
) {
2410 var top
= el
.offset().top
;
2411 var height
= el
.outerHeight();
2414 bottoms
.push(top
+ height
);
2418 this.bottoms
= bottoms
;
2422 // Given a left offset (from document left), returns the index of the el that it horizontally intersects.
2423 // If no intersection is made, returns undefined.
2424 getHorizontalIndex: function(leftOffset
) {
2427 var lefts
= this.lefts
;
2428 var rights
= this.rights
;
2429 var len
= lefts
.length
;
2432 for (i
= 0; i
< len
; i
++) {
2433 if (leftOffset
>= lefts
[i
] && leftOffset
< rights
[i
]) {
2440 // Given a top offset (from document top), returns the index of the el that it vertically intersects.
2441 // If no intersection is made, returns undefined.
2442 getVerticalIndex: function(topOffset
) {
2445 var tops
= this.tops
;
2446 var bottoms
= this.bottoms
;
2447 var len
= tops
.length
;
2450 for (i
= 0; i
< len
; i
++) {
2451 if (topOffset
>= tops
[i
] && topOffset
< bottoms
[i
]) {
2458 // Gets the left offset (from document left) of the element at the given index
2459 getLeftOffset: function(leftIndex
) {
2461 return this.lefts
[leftIndex
];
2465 // Gets the left position (from offsetParent left) of the element at the given index
2466 getLeftPosition: function(leftIndex
) {
2468 return this.lefts
[leftIndex
] - this.origin
.left
;
2472 // Gets the right offset (from document left) of the element at the given index.
2473 // This value is NOT relative to the document's right edge, like the CSS concept of "right" would be.
2474 getRightOffset: function(leftIndex
) {
2476 return this.rights
[leftIndex
];
2480 // Gets the right position (from offsetParent left) of the element at the given index.
2481 // This value is NOT relative to the offsetParent's right edge, like the CSS concept of "right" would be.
2482 getRightPosition: function(leftIndex
) {
2484 return this.rights
[leftIndex
] - this.origin
.left
;
2488 // Gets the width of the element at the given index
2489 getWidth: function(leftIndex
) {
2491 return this.rights
[leftIndex
] - this.lefts
[leftIndex
];
2495 // Gets the top offset (from document top) of the element at the given index
2496 getTopOffset: function(topIndex
) {
2498 return this.tops
[topIndex
];
2502 // Gets the top position (from offsetParent top) of the element at the given position
2503 getTopPosition: function(topIndex
) {
2505 return this.tops
[topIndex
] - this.origin
.top
;
2508 // Gets the bottom offset (from the document top) of the element at the given index.
2509 // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be.
2510 getBottomOffset: function(topIndex
) {
2512 return this.bottoms
[topIndex
];
2516 // Gets the bottom position (from the offsetParent top) of the element at the given index.
2517 // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be.
2518 getBottomPosition: function(topIndex
) {
2520 return this.bottoms
[topIndex
] - this.origin
.top
;
2524 // Gets the height of the element at the given index
2525 getHeight: function(topIndex
) {
2527 return this.bottoms
[topIndex
] - this.tops
[topIndex
];
2532 // TODO: decouple this from CoordCache
2534 // Compute and return what the elements' bounding rectangle is, from the user's perspective.
2535 // Right now, only returns a rectangle if constrained by an overflow:scroll element.
2536 // Returns null if there are no elements
2537 queryBoundingRect: function() {
2540 if (this.els
.length
> 0) {
2541 scrollParentEl
= getScrollParent(this.els
.eq(0));
2543 if (!scrollParentEl
.is(document
)) {
2544 return getClientRect(scrollParentEl
);
2551 isPointInBounds: function(leftOffset
, topOffset
) {
2552 return this.isLeftInBounds(leftOffset
) && this.isTopInBounds(topOffset
);
2555 isLeftInBounds: function(leftOffset
) {
2556 return !this.boundingRect
|| (leftOffset
>= this.boundingRect
.left
&& leftOffset
< this.boundingRect
.right
);
2559 isTopInBounds: function(topOffset
) {
2560 return !this.boundingRect
|| (topOffset
>= this.boundingRect
.top
&& topOffset
< this.boundingRect
.bottom
);
2567 /* Tracks a drag's mouse movement, firing various handlers
2568 ----------------------------------------------------------------------------------------------------------------------*/
2569 // TODO: use Emitter
2571 var DragListener
= FC
.DragListener
= Class
.extend(ListenerMixin
, {
2576 // coordinates of the initial mousedown
2580 // the wrapping element that scrolls, or MIGHT scroll if there's overflow.
2581 // TODO: do this for wrappers that have overflow:hidden as well.
2584 isInteracting
: false,
2585 isDistanceSurpassed
: false,
2586 isDelayEnded
: false,
2591 delayTimeoutId
: null,
2594 shouldCancelTouchScroll
: true,
2595 scrollAlwaysKills
: false,
2598 constructor: function(options
) {
2599 this.options
= options
|| {};
2603 // Interaction (high-level)
2604 // -----------------------------------------------------------------------------------------------------------------
2607 startInteraction: function(ev
, extraOptions
) {
2608 var isTouch
= getEvIsTouch(ev
);
2610 if (ev
.type
=== 'mousedown') {
2611 if (GlobalEmitter
.get().shouldIgnoreMouse()) {
2614 else if (!isPrimaryMouseButton(ev
)) {
2618 ev
.preventDefault(); // prevents native selection in most browsers
2622 if (!this.isInteracting
) {
2625 extraOptions
= extraOptions
|| {};
2626 this.delay
= firstDefined(extraOptions
.delay
, this.options
.delay
, 0);
2627 this.minDistance
= firstDefined(extraOptions
.distance
, this.options
.distance
, 0);
2628 this.subjectEl
= this.options
.subjectEl
;
2630 preventSelection($('body'));
2632 this.isInteracting
= true;
2633 this.isTouch
= isTouch
;
2634 this.isDelayEnded
= false;
2635 this.isDistanceSurpassed
= false;
2637 this.originX
= getEvX(ev
);
2638 this.originY
= getEvY(ev
);
2639 this.scrollEl
= getScrollParent($(ev
.target
));
2641 this.bindHandlers();
2642 this.initAutoScroll();
2643 this.handleInteractionStart(ev
);
2644 this.startDelay(ev
);
2646 if (!this.minDistance
) {
2647 this.handleDistanceSurpassed(ev
);
2653 handleInteractionStart: function(ev
) {
2654 this.trigger('interactionStart', ev
);
2658 endInteraction: function(ev
, isCancelled
) {
2659 if (this.isInteracting
) {
2662 if (this.delayTimeoutId
) {
2663 clearTimeout(this.delayTimeoutId
);
2664 this.delayTimeoutId
= null;
2667 this.destroyAutoScroll();
2668 this.unbindHandlers();
2670 this.isInteracting
= false;
2671 this.handleInteractionEnd(ev
, isCancelled
);
2673 allowSelection($('body'));
2678 handleInteractionEnd: function(ev
, isCancelled
) {
2679 this.trigger('interactionEnd', ev
, isCancelled
|| false);
2684 // -----------------------------------------------------------------------------------------------------------------
2687 bindHandlers: function() {
2688 // some browsers (Safari in iOS 10) don't allow preventDefault on touch events that are bound after touchstart,
2689 // so listen to the GlobalEmitter singleton, which is always bound, instead of the document directly.
2690 var globalEmitter
= GlobalEmitter
.get();
2693 this.listenTo(globalEmitter
, {
2694 touchmove
: this.handleTouchMove
,
2695 touchend
: this.endInteraction
,
2696 scroll
: this.handleTouchScroll
2700 this.listenTo(globalEmitter
, {
2701 mousemove
: this.handleMouseMove
,
2702 mouseup
: this.endInteraction
2706 this.listenTo(globalEmitter
, {
2707 selectstart
: preventDefault
, // don't allow selection while dragging
2708 contextmenu
: preventDefault
// long taps would open menu on Chrome dev tools
2713 unbindHandlers: function() {
2714 this.stopListeningTo(GlobalEmitter
.get());
2718 // Drag (high-level)
2719 // -----------------------------------------------------------------------------------------------------------------
2722 // extraOptions ignored if drag already started
2723 startDrag: function(ev
, extraOptions
) {
2724 this.startInteraction(ev
, extraOptions
); // ensure interaction began
2726 if (!this.isDragging
) {
2727 this.isDragging
= true;
2728 this.handleDragStart(ev
);
2733 handleDragStart: function(ev
) {
2734 this.trigger('dragStart', ev
);
2738 handleMove: function(ev
) {
2739 var dx
= getEvX(ev
) - this.originX
;
2740 var dy
= getEvY(ev
) - this.originY
;
2741 var minDistance
= this.minDistance
;
2742 var distanceSq
; // current distance from the origin, squared
2744 if (!this.isDistanceSurpassed
) {
2745 distanceSq
= dx
* dx
+ dy
* dy
;
2746 if (distanceSq
>= minDistance
* minDistance
) { // use pythagorean theorem
2747 this.handleDistanceSurpassed(ev
);
2751 if (this.isDragging
) {
2752 this.handleDrag(dx
, dy
, ev
);
2757 // Called while the mouse is being moved and when we know a legitimate drag is taking place
2758 handleDrag: function(dx
, dy
, ev
) {
2759 this.trigger('drag', dx
, dy
, ev
);
2760 this.updateAutoScroll(ev
); // will possibly cause scrolling
2764 endDrag: function(ev
) {
2765 if (this.isDragging
) {
2766 this.isDragging
= false;
2767 this.handleDragEnd(ev
);
2772 handleDragEnd: function(ev
) {
2773 this.trigger('dragEnd', ev
);
2778 // -----------------------------------------------------------------------------------------------------------------
2781 startDelay: function(initialEv
) {
2785 this.delayTimeoutId
= setTimeout(function() {
2786 _this
.handleDelayEnd(initialEv
);
2790 this.handleDelayEnd(initialEv
);
2795 handleDelayEnd: function(initialEv
) {
2796 this.isDelayEnded
= true;
2798 if (this.isDistanceSurpassed
) {
2799 this.startDrag(initialEv
);
2805 // -----------------------------------------------------------------------------------------------------------------
2808 handleDistanceSurpassed: function(ev
) {
2809 this.isDistanceSurpassed
= true;
2811 if (this.isDelayEnded
) {
2818 // -----------------------------------------------------------------------------------------------------------------
2821 handleTouchMove: function(ev
) {
2823 // prevent inertia and touchmove-scrolling while dragging
2824 if (this.isDragging
&& this.shouldCancelTouchScroll
) {
2825 ev
.preventDefault();
2828 this.handleMove(ev
);
2832 handleMouseMove: function(ev
) {
2833 this.handleMove(ev
);
2837 // Scrolling (unrelated to auto-scroll)
2838 // -----------------------------------------------------------------------------------------------------------------
2841 handleTouchScroll: function(ev
) {
2842 // if the drag is being initiated by touch, but a scroll happens before
2843 // the drag-initiating delay is over, cancel the drag
2844 if (!this.isDragging
|| this.scrollAlwaysKills
) {
2845 this.endInteraction(ev
, true); // isCancelled=true
2851 // -----------------------------------------------------------------------------------------------------------------
2854 // Triggers a callback. Calls a function in the option hash of the same name.
2855 // Arguments beyond the first `name` are forwarded on.
2856 trigger: function(name
) {
2857 if (this.options
[name
]) {
2858 this.options
[name
].apply(this, Array
.prototype.slice
.call(arguments
, 1));
2860 // makes _methods callable by event name. TODO: kill this
2861 if (this['_' + name
]) {
2862 this['_' + name
].apply(this, Array
.prototype.slice
.call(arguments
, 1));
2871 this.scrollEl is set in DragListener
2873 DragListener
.mixin({
2875 isAutoScroll
: false,
2877 scrollBounds
: null, // { top, bottom, left, right }
2878 scrollTopVel
: null, // pixels per second
2879 scrollLeftVel
: null, // pixels per second
2880 scrollIntervalId
: null, // ID of setTimeout for scrolling animation loop
2883 scrollSensitivity
: 30, // pixels from edge for scrolling to start
2884 scrollSpeed
: 200, // pixels per second, at maximum speed
2885 scrollIntervalMs
: 50, // millisecond wait between scroll increment
2888 initAutoScroll: function() {
2889 var scrollEl
= this.scrollEl
;
2892 this.options
.scroll
&&
2894 !scrollEl
.is(window
) &&
2895 !scrollEl
.is(document
);
2897 if (this.isAutoScroll
) {
2898 // debounce makes sure rapid calls don't happen
2899 this.listenTo(scrollEl
, 'scroll', debounce(this.handleDebouncedScroll
, 100));
2904 destroyAutoScroll: function() {
2905 this.endAutoScroll(); // kill any animation loop
2907 // remove the scroll handler if there is a scrollEl
2908 if (this.isAutoScroll
) {
2909 this.stopListeningTo(this.scrollEl
, 'scroll'); // will probably get removed by unbindHandlers too :(
2914 // Computes and stores the bounding rectangle of scrollEl
2915 computeScrollBounds: function() {
2916 if (this.isAutoScroll
) {
2917 this.scrollBounds
= getOuterRect(this.scrollEl
);
2918 // TODO: use getClientRect in future. but prevents auto scrolling when on top of scrollbars
2923 // Called when the dragging is in progress and scrolling should be updated
2924 updateAutoScroll: function(ev
) {
2925 var sensitivity
= this.scrollSensitivity
;
2926 var bounds
= this.scrollBounds
;
2927 var topCloseness
, bottomCloseness
;
2928 var leftCloseness
, rightCloseness
;
2932 if (bounds
) { // only scroll if scrollEl exists
2934 // compute closeness to edges. valid range is from 0.0 - 1.0
2935 topCloseness
= (sensitivity
- (getEvY(ev
) - bounds
.top
)) / sensitivity
;
2936 bottomCloseness
= (sensitivity
- (bounds
.bottom
- getEvY(ev
))) / sensitivity
;
2937 leftCloseness
= (sensitivity
- (getEvX(ev
) - bounds
.left
)) / sensitivity
;
2938 rightCloseness
= (sensitivity
- (bounds
.right
- getEvX(ev
))) / sensitivity
;
2940 // translate vertical closeness into velocity.
2941 // mouse must be completely in bounds for velocity to happen.
2942 if (topCloseness
>= 0 && topCloseness
<= 1) {
2943 topVel
= topCloseness
* this.scrollSpeed
* -1; // negative. for scrolling up
2945 else if (bottomCloseness
>= 0 && bottomCloseness
<= 1) {
2946 topVel
= bottomCloseness
* this.scrollSpeed
;
2949 // translate horizontal closeness into velocity
2950 if (leftCloseness
>= 0 && leftCloseness
<= 1) {
2951 leftVel
= leftCloseness
* this.scrollSpeed
* -1; // negative. for scrolling left
2953 else if (rightCloseness
>= 0 && rightCloseness
<= 1) {
2954 leftVel
= rightCloseness
* this.scrollSpeed
;
2958 this.setScrollVel(topVel
, leftVel
);
2962 // Sets the speed-of-scrolling for the scrollEl
2963 setScrollVel: function(topVel
, leftVel
) {
2965 this.scrollTopVel
= topVel
;
2966 this.scrollLeftVel
= leftVel
;
2968 this.constrainScrollVel(); // massages into realistic values
2970 // if there is non-zero velocity, and an animation loop hasn't already started, then START
2971 if ((this.scrollTopVel
|| this.scrollLeftVel
) && !this.scrollIntervalId
) {
2972 this.scrollIntervalId
= setInterval(
2973 proxy(this, 'scrollIntervalFunc'), // scope to `this`
2974 this.scrollIntervalMs
2980 // Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way
2981 constrainScrollVel: function() {
2982 var el
= this.scrollEl
;
2984 if (this.scrollTopVel
< 0) { // scrolling up?
2985 if (el
.scrollTop() <= 0) { // already scrolled all the way up?
2986 this.scrollTopVel
= 0;
2989 else if (this.scrollTopVel
> 0) { // scrolling down?
2990 if (el
.scrollTop() + el
[0].clientHeight
>= el
[0].scrollHeight
) { // already scrolled all the way down?
2991 this.scrollTopVel
= 0;
2995 if (this.scrollLeftVel
< 0) { // scrolling left?
2996 if (el
.scrollLeft() <= 0) { // already scrolled all the left?
2997 this.scrollLeftVel
= 0;
3000 else if (this.scrollLeftVel
> 0) { // scrolling right?
3001 if (el
.scrollLeft() + el
[0].clientWidth
>= el
[0].scrollWidth
) { // already scrolled all the way right?
3002 this.scrollLeftVel
= 0;
3008 // This function gets called during every iteration of the scrolling animation loop
3009 scrollIntervalFunc: function() {
3010 var el
= this.scrollEl
;
3011 var frac
= this.scrollIntervalMs
/ 1000; // considering animation frequency, what the vel should be mult'd by
3013 // change the value of scrollEl's scroll
3014 if (this.scrollTopVel
) {
3015 el
.scrollTop(el
.scrollTop() + this.scrollTopVel
* frac
);
3017 if (this.scrollLeftVel
) {
3018 el
.scrollLeft(el
.scrollLeft() + this.scrollLeftVel
* frac
);
3021 this.constrainScrollVel(); // since the scroll values changed, recompute the velocities
3023 // if scrolled all the way, which causes the vels to be zero, stop the animation loop
3024 if (!this.scrollTopVel
&& !this.scrollLeftVel
) {
3025 this.endAutoScroll();
3030 // Kills any existing scrolling animation loop
3031 endAutoScroll: function() {
3032 if (this.scrollIntervalId
) {
3033 clearInterval(this.scrollIntervalId
);
3034 this.scrollIntervalId
= null;
3036 this.handleScrollEnd();
3041 // Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce)
3042 handleDebouncedScroll: function() {
3043 // recompute all coordinates, but *only* if this is *not* part of our scrolling animation
3044 if (!this.scrollIntervalId
) {
3045 this.handleScrollEnd();
3050 // Called when scrolling has stopped, whether through auto scroll, or the user scrolling
3051 handleScrollEnd: function() {
3057 /* Tracks mouse movements over a component and raises events about which hit the mouse is over.
3058 ------------------------------------------------------------------------------------------------------------------------
3064 var HitDragListener
= DragListener
.extend({
3066 component
: null, // converts coordinates to hits
3067 // methods: hitsNeeded, hitsNotNeeded, queryHit
3069 origHit
: null, // the hit the mouse was over when listening started
3070 hit
: null, // the hit the mouse is over
3071 coordAdjust
: null, // delta that will be added to the mouse coordinates when computing collisions
3074 constructor: function(component
, options
) {
3075 DragListener
.call(this, options
); // call the super-constructor
3077 this.component
= component
;
3081 // Called when drag listening starts (but a real drag has not necessarily began).
3082 // ev might be undefined if dragging was started manually.
3083 handleInteractionStart: function(ev
) {
3084 var subjectEl
= this.subjectEl
;
3089 this.component
.hitsNeeded();
3090 this.computeScrollBounds(); // for autoscroll
3093 origPoint
= { left
: getEvX(ev
), top
: getEvY(ev
) };
3096 // constrain the point to bounds of the element being dragged
3098 subjectRect
= getOuterRect(subjectEl
); // used for centering as well
3099 point
= constrainPoint(point
, subjectRect
);
3102 this.origHit
= this.queryHit(point
.left
, point
.top
);
3104 // treat the center of the subject as the collision point?
3105 if (subjectEl
&& this.options
.subjectCenter
) {
3107 // only consider the area the subject overlaps the hit. best for large subjects.
3108 // TODO: skip this if hit didn't supply left/right/top/bottom
3110 subjectRect
= intersectRects(this.origHit
, subjectRect
) ||
3111 subjectRect
; // in case there is no intersection
3114 point
= getRectCenter(subjectRect
);
3117 this.coordAdjust
= diffPoints(point
, origPoint
); // point - origPoint
3120 this.origHit
= null;
3121 this.coordAdjust
= null;
3124 // call the super-method. do it after origHit has been computed
3125 DragListener
.prototype.handleInteractionStart
.apply(this, arguments
);
3129 // Called when the actual drag has started
3130 handleDragStart: function(ev
) {
3133 DragListener
.prototype.handleDragStart
.apply(this, arguments
); // call the super-method
3135 // might be different from this.origHit if the min-distance is large
3136 hit
= this.queryHit(getEvX(ev
), getEvY(ev
));
3138 // report the initial hit the mouse is over
3139 // especially important if no min-distance and drag starts immediately
3141 this.handleHitOver(hit
);
3146 // Called when the drag moves
3147 handleDrag: function(dx
, dy
, ev
) {
3150 DragListener
.prototype.handleDrag
.apply(this, arguments
); // call the super-method
3152 hit
= this.queryHit(getEvX(ev
), getEvY(ev
));
3154 if (!isHitsEqual(hit
, this.hit
)) { // a different hit than before?
3156 this.handleHitOut();
3159 this.handleHitOver(hit
);
3165 // Called when dragging has been stopped
3166 handleDragEnd: function() {
3167 this.handleHitDone();
3168 DragListener
.prototype.handleDragEnd
.apply(this, arguments
); // call the super-method
3172 // Called when a the mouse has just moved over a new hit
3173 handleHitOver: function(hit
) {
3174 var isOrig
= isHitsEqual(hit
, this.origHit
);
3178 this.trigger('hitOver', this.hit
, isOrig
, this.origHit
);
3182 // Called when the mouse has just moved out of a hit
3183 handleHitOut: function() {
3185 this.trigger('hitOut', this.hit
);
3186 this.handleHitDone();
3192 // Called after a hitOut. Also called before a dragStop
3193 handleHitDone: function() {
3195 this.trigger('hitDone', this.hit
);
3200 // Called when the interaction ends, whether there was a real drag or not
3201 handleInteractionEnd: function() {
3202 DragListener
.prototype.handleInteractionEnd
.apply(this, arguments
); // call the super-method
3204 this.origHit
= null;
3207 this.component
.hitsNotNeeded();
3211 // Called when scrolling has stopped, whether through auto scroll, or the user scrolling
3212 handleScrollEnd: function() {
3213 DragListener
.prototype.handleScrollEnd
.apply(this, arguments
); // call the super-method
3215 // hits' absolute positions will be in new places after a user's scroll.
3216 // HACK for recomputing.
3217 if (this.isDragging
) {
3218 this.component
.releaseHits();
3219 this.component
.prepareHits();
3224 // Gets the hit underneath the coordinates for the given mouse event
3225 queryHit: function(left
, top
) {
3227 if (this.coordAdjust
) {
3228 left
+= this.coordAdjust
.left
;
3229 top
+= this.coordAdjust
.top
;
3232 return this.component
.queryHit(left
, top
);
3238 // Returns `true` if the hits are identically equal. `false` otherwise. Must be from the same component.
3239 // Two null values will be considered equal, as two "out of the component" states are the same.
3240 function isHitsEqual(hit0
, hit1
) {
3242 if (!hit0
&& !hit1
) {
3247 return hit0
.component
=== hit1
.component
&&
3248 isHitPropsWithin(hit0
, hit1
) &&
3249 isHitPropsWithin(hit1
, hit0
); // ensures all props are identical
3256 // Returns true if all of subHit's non-standard properties are within superHit
3257 function isHitPropsWithin(subHit
, superHit
) {
3258 for (var propName
in subHit
) {
3259 if (!/^(component|left|right|top|bottom)$/.test(propName
)) {
3260 if (subHit
[propName
] !== superHit
[propName
]) {
3271 Listens to document and window-level user-interaction events, like touch events and mouse events,
3272 and fires these events as-is to whoever is observing a GlobalEmitter.
3273 Best when used as a singleton via GlobalEmitter.get()
3275 Normalizes mouse/touch events. For examples:
3276 - ignores the the simulated mouse events that happen after a quick tap: mousemove+mousedown+mouseup+click
3277 - compensates for various buggy scenarios where a touchend does not fire
3280 FC
.touchMouseIgnoreWait
= 500;
3282 var GlobalEmitter
= Class
.extend(ListenerMixin
, EmitterMixin
, {
3285 mouseIgnoreDepth
: 0,
3286 handleScrollProxy
: null,
3292 this.listenTo($(document
), {
3293 touchstart
: this.handleTouchStart
,
3294 touchcancel
: this.handleTouchCancel
,
3295 touchend
: this.handleTouchEnd
,
3296 mousedown
: this.handleMouseDown
,
3297 mousemove
: this.handleMouseMove
,
3298 mouseup
: this.handleMouseUp
,
3299 click
: this.handleClick
,
3300 selectstart
: this.handleSelectStart
,
3301 contextmenu
: this.handleContextMenu
3304 // because we need to call preventDefault
3305 // because https://www.chromestatus.com/features/5093566007214080
3306 // TODO: investigate performance because this is a global handler
3307 window
.addEventListener(
3309 this.handleTouchMoveProxy = function(ev
) {
3310 _this
.handleTouchMove($.Event(ev
));
3312 { passive
: false } // allows preventDefault()
3315 // attach a handler to get called when ANY scroll action happens on the page.
3316 // this was impossible to do with normal on/off because 'scroll' doesn't bubble.
3317 // http://stackoverflow.com/a/32954565/96342
3318 window
.addEventListener(
3320 this.handleScrollProxy = function(ev
) {
3321 _this
.handleScroll($.Event(ev
));
3327 unbind: function() {
3328 this.stopListeningTo($(document
));
3330 window
.removeEventListener(
3332 this.handleTouchMoveProxy
3335 window
.removeEventListener(
3337 this.handleScrollProxy
,
3344 // -----------------------------------------------------------------------------------------------------------------
3346 handleTouchStart: function(ev
) {
3348 // if a previous touch interaction never ended with a touchend, then implicitly end it,
3349 // but since a new touch interaction is about to begin, don't start the mouse ignore period.
3350 this.stopTouch(ev
, true); // skipMouseIgnore=true
3352 this.isTouching
= true;
3353 this.trigger('touchstart', ev
);
3356 handleTouchMove: function(ev
) {
3357 if (this.isTouching
) {
3358 this.trigger('touchmove', ev
);
3362 handleTouchCancel: function(ev
) {
3363 if (this.isTouching
) {
3364 this.trigger('touchcancel', ev
);
3366 // Have touchcancel fire an artificial touchend. That way, handlers won't need to listen to both.
3367 // If touchend fires later, it won't have any effect b/c isTouching will be false.
3372 handleTouchEnd: function(ev
) {
3378 // -----------------------------------------------------------------------------------------------------------------
3380 handleMouseDown: function(ev
) {
3381 if (!this.shouldIgnoreMouse()) {
3382 this.trigger('mousedown', ev
);
3386 handleMouseMove: function(ev
) {
3387 if (!this.shouldIgnoreMouse()) {
3388 this.trigger('mousemove', ev
);
3392 handleMouseUp: function(ev
) {
3393 if (!this.shouldIgnoreMouse()) {
3394 this.trigger('mouseup', ev
);
3398 handleClick: function(ev
) {
3399 if (!this.shouldIgnoreMouse()) {
3400 this.trigger('click', ev
);
3406 // -----------------------------------------------------------------------------------------------------------------
3408 handleSelectStart: function(ev
) {
3409 this.trigger('selectstart', ev
);
3412 handleContextMenu: function(ev
) {
3413 this.trigger('contextmenu', ev
);
3416 handleScroll: function(ev
) {
3417 this.trigger('scroll', ev
);
3422 // -----------------------------------------------------------------------------------------------------------------
3424 stopTouch: function(ev
, skipMouseIgnore
) {
3425 if (this.isTouching
) {
3426 this.isTouching
= false;
3427 this.trigger('touchend', ev
);
3429 if (!skipMouseIgnore
) {
3430 this.startTouchMouseIgnore();
3435 startTouchMouseIgnore: function() {
3437 var wait
= FC
.touchMouseIgnoreWait
;
3440 this.mouseIgnoreDepth
++;
3441 setTimeout(function() {
3442 _this
.mouseIgnoreDepth
--;
3447 shouldIgnoreMouse: function() {
3448 return this.isTouching
|| Boolean(this.mouseIgnoreDepth
);
3455 // ---------------------------------------------------------------------------------------------------------------------
3458 var globalEmitter
= null;
3459 var neededCount
= 0;
3462 // gets the singleton
3463 GlobalEmitter
.get = function() {
3465 if (!globalEmitter
) {
3466 globalEmitter
= new GlobalEmitter();
3467 globalEmitter
.bind();
3470 return globalEmitter
;
3474 // called when an object knows it will need a GlobalEmitter in the near future.
3475 GlobalEmitter
.needed = function() {
3476 GlobalEmitter
.get(); // ensures globalEmitter
3481 // called when the object that originally called needed() doesn't need a GlobalEmitter anymore.
3482 GlobalEmitter
.unneeded = function() {
3485 if (!neededCount
) { // nobody else needs it
3486 globalEmitter
.unbind();
3487 globalEmitter
= null;
3495 /* Creates a clone of an element and lets it track the mouse as it moves
3496 ----------------------------------------------------------------------------------------------------------------------*/
3498 var MouseFollower
= Class
.extend(ListenerMixin
, {
3502 sourceEl
: null, // the element that will be cloned and made to look like it is dragging
3503 el
: null, // the clone of `sourceEl` that will track the mouse
3504 parentEl
: null, // the element that `el` (the clone) will be attached to
3506 // the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl
3510 // the absolute coordinates of the initiating touch/mouse action
3514 // the number of pixels the mouse has moved from its initial position
3520 isAnimating
: false, // doing the revert animation?
3522 constructor: function(sourceEl
, options
) {
3523 this.options
= options
= options
|| {};
3524 this.sourceEl
= sourceEl
;
3525 this.parentEl
= options
.parentEl
? $(options
.parentEl
) : sourceEl
.parent(); // default to sourceEl's parent
3529 // Causes the element to start following the mouse
3530 start: function(ev
) {
3531 if (!this.isFollowing
) {
3532 this.isFollowing
= true;
3534 this.y0
= getEvY(ev
);
3535 this.x0
= getEvX(ev
);
3539 if (!this.isHidden
) {
3540 this.updatePosition();
3543 if (getEvIsTouch(ev
)) {
3544 this.listenTo($(document
), 'touchmove', this.handleMove
);
3547 this.listenTo($(document
), 'mousemove', this.handleMove
);
3553 // Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position.
3554 // `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately.
3555 stop: function(shouldRevert
, callback
) {
3557 var revertDuration
= this.options
.revertDuration
;
3559 function complete() { // might be called by .animate(), which might change `this` context
3560 _this
.isAnimating
= false;
3561 _this
.removeElement();
3563 _this
.top0
= _this
.left0
= null; // reset state for future updatePosition calls
3570 if (this.isFollowing
&& !this.isAnimating
) { // disallow more than one stop animation at a time
3571 this.isFollowing
= false;
3573 this.stopListeningTo($(document
));
3575 if (shouldRevert
&& revertDuration
&& !this.isHidden
) { // do a revert animation?
3576 this.isAnimating
= true;
3581 duration
: revertDuration
,
3592 // Gets the tracking element. Create it if necessary
3597 el
= this.el
= this.sourceEl
.clone()
3598 .addClass(this.options
.additionalClass
|| '')
3600 position
: 'absolute',
3601 visibility
: '', // in case original element was hidden (commonly through hideEvents())
3602 display
: this.isHidden
? 'none' : '', // for when initially hidden
3604 right
: 'auto', // erase and set width instead
3605 bottom
: 'auto', // erase and set height instead
3606 width
: this.sourceEl
.width(), // explicit height in case there was a 'right' value
3607 height
: this.sourceEl
.height(), // explicit width in case there was a 'bottom' value
3608 opacity
: this.options
.opacity
|| '',
3609 zIndex
: this.options
.zIndex
3612 // we don't want long taps or any mouse interaction causing selection/menus.
3613 // would use preventSelection(), but that prevents selectstart, causing problems.
3614 el
.addClass('fc-unselectable');
3616 el
.appendTo(this.parentEl
);
3623 // Removes the tracking element if it has already been created
3624 removeElement: function() {
3632 // Update the CSS position of the tracking element
3633 updatePosition: function() {
3637 this.getEl(); // ensure this.el
3639 // make sure origin info was computed
3640 if (this.top0
=== null) {
3641 sourceOffset
= this.sourceEl
.offset();
3642 origin
= this.el
.offsetParent().offset();
3643 this.top0
= sourceOffset
.top
- origin
.top
;
3644 this.left0
= sourceOffset
.left
- origin
.left
;
3648 top
: this.top0
+ this.topDelta
,
3649 left
: this.left0
+ this.leftDelta
3654 // Gets called when the user moves the mouse
3655 handleMove: function(ev
) {
3656 this.topDelta
= getEvY(ev
) - this.y0
;
3657 this.leftDelta
= getEvX(ev
) - this.x0
;
3659 if (!this.isHidden
) {
3660 this.updatePosition();
3665 // Temporarily makes the tracking element invisible. Can be called before following starts
3667 if (!this.isHidden
) {
3668 this.isHidden
= true;
3676 // Show the tracking element after it has been temporarily hidden
3678 if (this.isHidden
) {
3679 this.isHidden
= false;
3680 this.updatePosition();
3681 this.getEl().show();
3689 /* An abstract class comprised of a "grid" of areas that each represent a specific datetime
3690 ----------------------------------------------------------------------------------------------------------------------*/
3692 var Grid
= FC
.Grid
= Class
.extend(ListenerMixin
, {
3694 // self-config, overridable by subclasses
3695 hasDayInteractions
: true, // can user click/select ranges of time?
3697 view
: null, // a View object
3698 isRTL
: null, // shortcut to the view's isRTL option
3703 el
: null, // the containing element
3704 elsByFill
: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name.
3706 // derived from options
3707 eventTimeFormat
: null,
3708 displayEventTime
: null,
3709 displayEventEnd
: null,
3711 minResizeDuration
: null, // TODO: hack. set by subclasses. minumum event resize duration
3713 // if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity
3714 // of the date areas. if not defined, assumes to be day and time granularity.
3715 // TODO: port isTimeScale into same system?
3718 dayClickListener
: null,
3719 daySelectListener
: null,
3720 segDragListener
: null,
3721 segResizeListener
: null,
3722 externalDragListener
: null,
3725 constructor: function(view
) {
3727 this.isRTL
= view
.opt('isRTL');
3728 this.elsByFill
= {};
3730 this.dayClickListener
= this.buildDayClickListener();
3731 this.daySelectListener
= this.buildDaySelectListener();
3736 ------------------------------------------------------------------------------------------------------------------*/
3739 // Generates the format string used for event time text, if not explicitly defined by 'timeFormat'
3740 computeEventTimeFormat: function() {
3741 return this.view
.opt('smallTimeFormat');
3745 // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventTime'.
3746 // Only applies to non-all-day events.
3747 computeDisplayEventTime: function() {
3752 // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd'
3753 computeDisplayEventEnd: function() {
3759 ------------------------------------------------------------------------------------------------------------------*/
3762 // Tells the grid about what period of time to display.
3763 // Any date-related internal data should be generated.
3764 setRange: function(range
) {
3765 this.start
= range
.start
.clone();
3766 this.end
= range
.end
.clone();
3768 this.rangeUpdated();
3769 this.processRangeOptions();
3773 // Called when internal variables that rely on the range should be updated
3774 rangeUpdated: function() {
3778 // Updates values that rely on options and also relate to range
3779 processRangeOptions: function() {
3780 var view
= this.view
;
3781 var displayEventTime
;
3782 var displayEventEnd
;
3784 this.eventTimeFormat
=
3785 view
.opt('eventTimeFormat') ||
3786 view
.opt('timeFormat') || // deprecated
3787 this.computeEventTimeFormat();
3789 displayEventTime
= view
.opt('displayEventTime');
3790 if (displayEventTime
== null) {
3791 displayEventTime
= this.computeDisplayEventTime(); // might be based off of range
3794 displayEventEnd
= view
.opt('displayEventEnd');
3795 if (displayEventEnd
== null) {
3796 displayEventEnd
= this.computeDisplayEventEnd(); // might be based off of range
3799 this.displayEventTime
= displayEventTime
;
3800 this.displayEventEnd
= displayEventEnd
;
3804 // Converts a span (has unzoned start/end and any other grid-specific location information)
3805 // into an array of segments (pieces of events whose format is decided by the grid).
3806 spanToSegs: function(span
) {
3807 // subclasses must implement
3811 // Diffs the two dates, returning a duration, based on granularity of the grid
3812 // TODO: port isTimeScale into this system?
3813 diffDates: function(a
, b
) {
3814 if (this.largeUnit
) {
3815 return diffByUnit(a
, b
, this.largeUnit
);
3818 return diffDayTime(a
, b
);
3824 ------------------------------------------------------------------------------------------------------------------*/
3826 hitsNeededDepth
: 0, // necessary because multiple callers might need the same hits
3828 hitsNeeded: function() {
3829 if (!(this.hitsNeededDepth
++)) {
3834 hitsNotNeeded: function() {
3835 if (this.hitsNeededDepth
&& !(--this.hitsNeededDepth
)) {
3841 // Called before one or more queryHit calls might happen. Should prepare any cached coordinates for queryHit
3842 prepareHits: function() {
3846 // Called when queryHit calls have subsided. Good place to clear any coordinate caches.
3847 releaseHits: function() {
3851 // Given coordinates from the topleft of the document, return data about the date-related area underneath.
3852 // Can return an object with arbitrary properties (although top/right/left/bottom are encouraged).
3853 // Must have a `grid` property, a reference to this current grid. TODO: avoid this
3854 // The returned object will be processed by getHitSpan and getHitEl.
3855 queryHit: function(leftOffset
, topOffset
) {
3859 // Given position-level information about a date-related area within the grid,
3860 // should return an object with at least a start/end date. Can provide other information as well.
3861 getHitSpan: function(hit
) {
3865 // Given position-level information about a date-related area within the grid,
3866 // should return a jQuery element that best represents it. passed to dayClick callback.
3867 getHitEl: function(hit
) {
3872 ------------------------------------------------------------------------------------------------------------------*/
3875 // Sets the container element that the grid should render inside of.
3876 // Does other DOM-related initializations.
3877 setElement: function(el
) {
3880 if (this.hasDayInteractions
) {
3881 preventSelection(el
);
3883 this.bindDayHandler('touchstart', this.dayTouchStart
);
3884 this.bindDayHandler('mousedown', this.dayMousedown
);
3887 // attach event-element-related handlers. in Grid.events
3888 // same garbage collection note as above.
3889 this.bindSegHandlers();
3891 this.bindGlobalHandlers();
3895 bindDayHandler: function(name
, handler
) {
3898 // attach a handler to the grid's root element.
3899 // jQuery will take care of unregistering them when removeElement gets called.
3900 this.el
.on(name
, function(ev
) {
3903 _this
.segSelector
+ ',' + // directly on an event element
3904 _this
.segSelector
+ ' *,' + // within an event element
3905 '.fc-more,' + // a "more.." link
3906 'a[data-goto]' // a clickable nav link
3909 return handler
.call(_this
, ev
);
3915 // Removes the grid's container element from the DOM. Undoes any other DOM-related attachments.
3916 // DOES NOT remove any content beforehand (doesn't clear events or call unrenderDates), unlike View
3917 removeElement: function() {
3918 this.unbindGlobalHandlers();
3919 this.clearDragListeners();
3923 // NOTE: we don't null-out this.el for the same reasons we don't do it within View::removeElement
3927 // Renders the basic structure of grid view before any content is rendered
3928 renderSkeleton: function() {
3929 // subclasses should implement
3933 // Renders the grid's date-related content (like areas that represent days/times).
3934 // Assumes setRange has already been called and the skeleton has already been rendered.
3935 renderDates: function() {
3936 // subclasses should implement
3940 // Unrenders the grid's date-related content
3941 unrenderDates: function() {
3942 // subclasses should implement
3947 ------------------------------------------------------------------------------------------------------------------*/
3950 // Binds DOM handlers to elements that reside outside the grid, such as the document
3951 bindGlobalHandlers: function() {
3952 this.listenTo($(document
), {
3953 dragstart
: this.externalDragStart
, // jqui
3954 sortstart
: this.externalDragStart
// jqui
3959 // Unbinds DOM handlers from elements that reside outside the grid
3960 unbindGlobalHandlers: function() {
3961 this.stopListeningTo($(document
));
3965 // Process a mousedown on an element that represents a day. For day clicking and selecting.
3966 dayMousedown: function(ev
) {
3967 var view
= this.view
;
3969 // prevent a user's clickaway for unselecting a range or an event from
3970 // causing a dayClick or starting an immediate new selection.
3971 if (view
.isSelected
|| view
.selectedEvent
) {
3975 this.dayClickListener
.startInteraction(ev
);
3977 if (view
.opt('selectable')) {
3978 this.daySelectListener
.startInteraction(ev
, {
3979 distance
: view
.opt('selectMinDistance')
3985 dayTouchStart: function(ev
) {
3986 var view
= this.view
;
3987 var selectLongPressDelay
;
3989 // prevent a user's clickaway for unselecting a range or an event from
3990 // causing a dayClick or starting an immediate new selection.
3991 if (view
.isSelected
|| view
.selectedEvent
) {
3995 selectLongPressDelay
= view
.opt('selectLongPressDelay');
3996 if (selectLongPressDelay
== null) {
3997 selectLongPressDelay
= view
.opt('longPressDelay'); // fallback
4000 this.dayClickListener
.startInteraction(ev
);
4002 if (view
.opt('selectable')) {
4003 this.daySelectListener
.startInteraction(ev
, {
4004 delay
: selectLongPressDelay
4010 // Creates a listener that tracks the user's drag across day elements, for day clicking.
4011 buildDayClickListener: function() {
4013 var view
= this.view
;
4014 var dayClickHit
; // null if invalid dayClick
4016 var dragListener
= new HitDragListener(this, {
4017 scroll
: view
.opt('dragScroll'),
4018 interactionStart: function() {
4019 dayClickHit
= dragListener
.origHit
;
4021 hitOver: function(hit
, isOrig
, origHit
) {
4022 // if user dragged to another cell at any point, it can no longer be a dayClick
4027 hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
4030 interactionEnd: function(ev
, isCancelled
) {
4031 if (!isCancelled
&& dayClickHit
) {
4032 view
.triggerDayClick(
4033 _this
.getHitSpan(dayClickHit
),
4034 _this
.getHitEl(dayClickHit
),
4041 // because dayClickListener won't be called with any time delay, "dragging" will begin immediately,
4042 // which will kill any touchmoving/scrolling. Prevent this.
4043 dragListener
.shouldCancelTouchScroll
= false;
4045 dragListener
.scrollAlwaysKills
= true;
4047 return dragListener
;
4051 // Creates a listener that tracks the user's drag across day elements, for day selecting.
4052 buildDaySelectListener: function() {
4054 var view
= this.view
;
4055 var selectionSpan
; // null if invalid selection
4057 var dragListener
= new HitDragListener(this, {
4058 scroll
: view
.opt('dragScroll'),
4059 interactionStart: function() {
4060 selectionSpan
= null;
4062 dragStart: function() {
4063 view
.unselect(); // since we could be rendering a new selection, we want to clear any old one
4065 hitOver: function(hit
, isOrig
, origHit
) {
4066 if (origHit
) { // click needs to have started on a hit
4068 selectionSpan
= _this
.computeSelection(
4069 _this
.getHitSpan(origHit
),
4070 _this
.getHitSpan(hit
)
4073 if (selectionSpan
) {
4074 _this
.renderSelection(selectionSpan
);
4076 else if (selectionSpan
=== false) {
4081 hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
4082 selectionSpan
= null;
4083 _this
.unrenderSelection();
4085 hitDone: function() { // called after a hitOut OR before a dragEnd
4088 interactionEnd: function(ev
, isCancelled
) {
4089 if (!isCancelled
&& selectionSpan
) {
4090 // the selection will already have been rendered. just report it
4091 view
.reportSelection(selectionSpan
, ev
);
4096 return dragListener
;
4100 // Kills all in-progress dragging.
4101 // Useful for when public API methods that result in re-rendering are invoked during a drag.
4102 // Also useful for when touch devices misbehave and don't fire their touchend.
4103 clearDragListeners: function() {
4104 this.dayClickListener
.endInteraction();
4105 this.daySelectListener
.endInteraction();
4107 if (this.segDragListener
) {
4108 this.segDragListener
.endInteraction(); // will clear this.segDragListener
4110 if (this.segResizeListener
) {
4111 this.segResizeListener
.endInteraction(); // will clear this.segResizeListener
4113 if (this.externalDragListener
) {
4114 this.externalDragListener
.endInteraction(); // will clear this.externalDragListener
4120 ------------------------------------------------------------------------------------------------------------------*/
4121 // TODO: should probably move this to Grid.events, like we did event dragging / resizing
4124 // Renders a mock event at the given event location, which contains zoned start/end properties.
4125 // Returns all mock event elements.
4126 renderEventLocationHelper: function(eventLocation
, sourceSeg
) {
4127 var fakeEvent
= this.fabricateHelperEvent(eventLocation
, sourceSeg
);
4129 return this.renderHelper(fakeEvent
, sourceSeg
); // do the actual rendering
4133 // Builds a fake event given zoned event date properties and a segment is should be inspired from.
4134 // The range's end can be null, in which case the mock event that is rendered will have a null end time.
4135 // `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging.
4136 fabricateHelperEvent: function(eventLocation
, sourceSeg
) {
4137 var fakeEvent
= sourceSeg
? createObject(sourceSeg
.event
) : {}; // mask the original event object if possible
4139 fakeEvent
.start
= eventLocation
.start
.clone();
4140 fakeEvent
.end
= eventLocation
.end
? eventLocation
.end
.clone() : null;
4141 fakeEvent
.allDay
= null; // force it to be freshly computed by normalizeEventDates
4142 this.view
.calendar
.normalizeEventDates(fakeEvent
);
4144 // this extra className will be useful for differentiating real events from mock events in CSS
4145 fakeEvent
.className
= (fakeEvent
.className
|| []).concat('fc-helper');
4147 // if something external is being dragged in, don't render a resizer
4149 fakeEvent
.editable
= false;
4156 // Renders a mock event. Given zoned event date properties.
4157 // Must return all mock event elements.
4158 renderHelper: function(eventLocation
, sourceSeg
) {
4159 // subclasses must implement
4163 // Unrenders a mock event
4164 unrenderHelper: function() {
4165 // subclasses must implement
4170 ------------------------------------------------------------------------------------------------------------------*/
4173 // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses.
4174 // Given a span (unzoned start/end and other misc data)
4175 renderSelection: function(span
) {
4176 this.renderHighlight(span
);
4180 // Unrenders any visual indications of a selection. Will unrender a highlight by default.
4181 unrenderSelection: function() {
4182 this.unrenderHighlight();
4186 // Given the first and last date-spans of a selection, returns another date-span object.
4187 // Subclasses can override and provide additional data in the span object. Will be passed to renderSelection().
4188 // Will return false if the selection is invalid and this should be indicated to the user.
4189 // Will return null/undefined if a selection invalid but no error should be reported.
4190 computeSelection: function(span0
, span1
) {
4191 var span
= this.computeSelectionSpan(span0
, span1
);
4193 if (span
&& !this.view
.calendar
.isSelectionSpanAllowed(span
)) {
4201 // Given two spans, must return the combination of the two.
4202 // TODO: do this separation of concerns (combining VS validation) for event dnd/resize too.
4203 computeSelectionSpan: function(span0
, span1
) {
4204 var dates
= [ span0
.start
, span0
.end
, span1
.start
, span1
.end
];
4206 dates
.sort(compareNumbers
); // sorts chronologically. works with Moments
4208 return { start
: dates
[0].clone(), end
: dates
[3].clone() };
4213 ------------------------------------------------------------------------------------------------------------------*/
4216 // Renders an emphasis on the given date range. Given a span (unzoned start/end and other misc data)
4217 renderHighlight: function(span
) {
4218 this.renderFill('highlight', this.spanToSegs(span
));
4222 // Unrenders the emphasis on a date range
4223 unrenderHighlight: function() {
4224 this.unrenderFill('highlight');
4228 // Generates an array of classNames for rendering the highlight. Used by the fill system.
4229 highlightSegClasses: function() {
4230 return [ 'fc-highlight' ];
4235 ------------------------------------------------------------------------------------------------------------------*/
4238 renderBusinessHours: function() {
4242 unrenderBusinessHours: function() {
4247 ------------------------------------------------------------------------------------------------------------------*/
4250 getNowIndicatorUnit: function() {
4254 renderNowIndicator: function(date
) {
4258 unrenderNowIndicator: function() {
4262 /* Fill System (highlight, background events, business hours)
4263 --------------------------------------------------------------------------------------------------------------------
4264 TODO: remove this system. like we did in TimeGrid
4268 // Renders a set of rectangles over the given segments of time.
4269 // MUST RETURN a subset of segs, the segs that were actually rendered.
4270 // Responsible for populating this.elsByFill. TODO: better API for expressing this requirement
4271 renderFill: function(type
, segs
) {
4272 // subclasses must implement
4276 // Unrenders a specific type of fill that is currently rendered on the grid
4277 unrenderFill: function(type
) {
4278 var el
= this.elsByFill
[type
];
4282 delete this.elsByFill
[type
];
4287 // Renders and assigns an `el` property for each fill segment. Generic enough to work with different types.
4288 // Only returns segments that successfully rendered.
4289 // To be harnessed by renderFill (implemented by subclasses).
4290 // Analagous to renderFgSegEls.
4291 renderFillSegEls: function(type
, segs
) {
4293 var segElMethod
= this[type
+ 'SegEl'];
4295 var renderedSegs
= [];
4300 // build a large concatenation of segment HTML
4301 for (i
= 0; i
< segs
.length
; i
++) {
4302 html
+= this.fillSegHtml(type
, segs
[i
]);
4305 // Grab individual elements from the combined HTML string. Use each as the default rendering.
4306 // Then, compute the 'el' for each segment.
4307 $(html
).each(function(i
, node
) {
4311 // allow custom filter methods per-type
4313 el
= segElMethod
.call(_this
, seg
, el
);
4316 if (el
) { // custom filters did not cancel the render
4317 el
= $(el
); // allow custom filter to return raw DOM node
4319 // correct element type? (would be bad if a non-TD were inserted into a table for example)
4320 if (el
.is(_this
.fillSegTag
)) {
4322 renderedSegs
.push(seg
);
4328 return renderedSegs
;
4332 fillSegTag
: 'div', // subclasses can override
4335 // Builds the HTML needed for one fill segment. Generic enough to work with different types.
4336 fillSegHtml: function(type
, seg
) {
4338 // custom hooks per-type
4339 var classesMethod
= this[type
+ 'SegClasses'];
4340 var cssMethod
= this[type
+ 'SegCss'];
4342 var classes
= classesMethod
? classesMethod
.call(this, seg
) : [];
4343 var css
= cssToStr(cssMethod
? cssMethod
.call(this, seg
) : {});
4345 return '<' + this.fillSegTag
+
4346 (classes
.length
? ' class="' + classes
.join(' ') + '"' : '') +
4347 (css
? ' style="' + css
+ '"' : '') +
4353 /* Generic rendering utilities for subclasses
4354 ------------------------------------------------------------------------------------------------------------------*/
4357 // Computes HTML classNames for a single-day element
4358 getDayClasses: function(date
, noThemeHighlight
) {
4359 var view
= this.view
;
4360 var today
= view
.calendar
.getNow();
4361 var classes
= [ 'fc-' + dayIDs
[date
.day()] ];
4364 view
.intervalDuration
.as('months') == 1 &&
4365 date
.month() != view
.intervalStart
.month()
4367 classes
.push('fc-other-month');
4370 if (date
.isSame(today
, 'day')) {
4371 classes
.push('fc-today');
4373 if (noThemeHighlight
!== true) {
4374 classes
.push(view
.highlightStateClass
);
4377 else if (date
< today
) {
4378 classes
.push('fc-past');
4381 classes
.push('fc-future');
4391 /* Event-rendering and event-interaction methods for the abstract Grid class
4392 ----------------------------------------------------------------------------------------------------------------------*/
4396 // self-config, overridable by subclasses
4397 segSelector
: '.fc-event-container > *', // what constitutes an event element?
4399 mousedOverSeg
: null, // the segment object the user's mouse is over. null if over nothing
4400 isDraggingSeg
: false, // is a segment being dragged? boolean
4401 isResizingSeg
: false, // is a segment being resized? boolean
4402 isDraggingExternal
: false, // jqui-dragging an external element? boolean
4403 segs
: null, // the *event* segments currently rendered in the grid. TODO: rename to `eventSegs`
4406 // Renders the given events onto the grid
4407 renderEvents: function(events
) {
4412 for (i
= 0; i
< events
.length
; i
++) {
4413 (isBgEvent(events
[i
]) ? bgEvents
: fgEvents
).push(events
[i
]);
4416 this.segs
= [].concat( // record all segs
4417 this.renderBgEvents(bgEvents
),
4418 this.renderFgEvents(fgEvents
)
4423 renderBgEvents: function(events
) {
4424 var segs
= this.eventsToSegs(events
);
4426 // renderBgSegs might return a subset of segs, segs that were actually rendered
4427 return this.renderBgSegs(segs
) || segs
;
4431 renderFgEvents: function(events
) {
4432 var segs
= this.eventsToSegs(events
);
4434 // renderFgSegs might return a subset of segs, segs that were actually rendered
4435 return this.renderFgSegs(segs
) || segs
;
4439 // Unrenders all events currently rendered on the grid
4440 unrenderEvents: function() {
4441 this.handleSegMouseout(); // trigger an eventMouseout if user's mouse is over an event
4442 this.clearDragListeners();
4444 this.unrenderFgSegs();
4445 this.unrenderBgSegs();
4451 // Retrieves all rendered segment objects currently rendered on the grid
4452 getEventSegs: function() {
4453 return this.segs
|| [];
4457 /* Foreground Segment Rendering
4458 ------------------------------------------------------------------------------------------------------------------*/
4461 // Renders foreground event segments onto the grid. May return a subset of segs that were rendered.
4462 renderFgSegs: function(segs
) {
4463 // subclasses must implement
4467 // Unrenders all currently rendered foreground segments
4468 unrenderFgSegs: function() {
4469 // subclasses must implement
4473 // Renders and assigns an `el` property for each foreground event segment.
4474 // Only returns segments that successfully rendered.
4475 // A utility that subclasses may use.
4476 renderFgSegEls: function(segs
, disableResizing
) {
4477 var view
= this.view
;
4479 var renderedSegs
= [];
4482 if (segs
.length
) { // don't build an empty html string
4484 // build a large concatenation of event segment HTML
4485 for (i
= 0; i
< segs
.length
; i
++) {
4486 html
+= this.fgSegHtml(segs
[i
], disableResizing
);
4489 // Grab individual elements from the combined HTML string. Use each as the default rendering.
4490 // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.
4491 $(html
).each(function(i
, node
) {
4493 var el
= view
.resolveEventEl(seg
.event
, $(node
));
4496 el
.data('fc-seg', seg
); // used by handlers
4498 renderedSegs
.push(seg
);
4503 return renderedSegs
;
4507 // Generates the HTML for the default rendering of a foreground event segment. Used by renderFgSegEls()
4508 fgSegHtml: function(seg
, disableResizing
) {
4509 // subclasses should implement
4513 /* Background Segment Rendering
4514 ------------------------------------------------------------------------------------------------------------------*/
4517 // Renders the given background event segments onto the grid.
4518 // Returns a subset of the segs that were actually rendered.
4519 renderBgSegs: function(segs
) {
4520 return this.renderFill('bgEvent', segs
);
4524 // Unrenders all the currently rendered background event segments
4525 unrenderBgSegs: function() {
4526 this.unrenderFill('bgEvent');
4530 // Renders a background event element, given the default rendering. Called by the fill system.
4531 bgEventSegEl: function(seg
, el
) {
4532 return this.view
.resolveEventEl(seg
.event
, el
); // will filter through eventRender
4536 // Generates an array of classNames to be used for the default rendering of a background event.
4537 // Called by fillSegHtml.
4538 bgEventSegClasses: function(seg
) {
4539 var event
= seg
.event
;
4540 var source
= event
.source
|| {};
4542 return [ 'fc-bgevent' ].concat(
4544 source
.className
|| []
4549 // Generates a semicolon-separated CSS string to be used for the default rendering of a background event.
4550 // Called by fillSegHtml.
4551 bgEventSegCss: function(seg
) {
4553 'background-color': this.getSegSkinCss(seg
)['background-color']
4558 // Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system.
4559 // Called by fillSegHtml.
4560 businessHoursSegClasses: function(seg
) {
4561 return [ 'fc-nonbusiness', 'fc-bgevent' ];
4566 ------------------------------------------------------------------------------------------------------------------*/
4569 // Compute business hour segs for the grid's current date range.
4570 // Caller must ask if whole-day business hours are needed.
4571 // If no `businessHours` configuration value is specified, assumes the calendar default.
4572 buildBusinessHourSegs: function(wholeDay
, businessHours
) {
4573 return this.eventsToSegs(
4574 this.buildBusinessHourEvents(wholeDay
, businessHours
)
4579 // Compute business hour *events* for the grid's current date range.
4580 // Caller must ask if whole-day business hours are needed.
4581 // If no `businessHours` configuration value is specified, assumes the calendar default.
4582 buildBusinessHourEvents: function(wholeDay
, businessHours
) {
4583 var calendar
= this.view
.calendar
;
4586 if (businessHours
== null) {
4588 // access from calendawr. don't access from view. doesn't update with dynamic options.
4589 businessHours
= calendar
.options
.businessHours
;
4592 events
= calendar
.computeBusinessHourEvents(wholeDay
, businessHours
);
4594 // HACK. Eventually refactor business hours "events" system.
4595 // If no events are given, but businessHours is activated, this means the entire visible range should be
4596 // marked as *not* business-hours, via inverse-background rendering.
4597 if (!events
.length
&& businessHours
) {
4599 $.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS
, {
4600 start
: this.view
.end
, // guaranteed out-of-range
4601 end
: this.view
.end
, // "
4612 ------------------------------------------------------------------------------------------------------------------*/
4615 // Attaches event-element-related handlers for *all* rendered event segments of the view.
4616 bindSegHandlers: function() {
4617 this.bindSegHandlersToEl(this.el
);
4621 // Attaches event-element-related handlers to an arbitrary container element. leverages bubbling.
4622 bindSegHandlersToEl: function(el
) {
4623 this.bindSegHandlerToEl(el
, 'touchstart', this.handleSegTouchStart
);
4624 this.bindSegHandlerToEl(el
, 'mouseenter', this.handleSegMouseover
);
4625 this.bindSegHandlerToEl(el
, 'mouseleave', this.handleSegMouseout
);
4626 this.bindSegHandlerToEl(el
, 'mousedown', this.handleSegMousedown
);
4627 this.bindSegHandlerToEl(el
, 'click', this.handleSegClick
);
4631 // Executes a handler for any a user-interaction on a segment.
4632 // Handler gets called with (seg, ev), and with the `this` context of the Grid
4633 bindSegHandlerToEl: function(el
, name
, handler
) {
4636 el
.on(name
, this.segSelector
, function(ev
) {
4637 var seg
= $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents
4639 // only call the handlers if there is not a drag/resize in progress
4640 if (seg
&& !_this
.isDraggingSeg
&& !_this
.isResizingSeg
) {
4641 return handler
.call(_this
, seg
, ev
); // context will be the Grid
4647 handleSegClick: function(seg
, ev
) {
4648 var res
= this.view
.publiclyTrigger('eventClick', seg
.el
[0], seg
.event
, ev
); // can return `false` to cancel
4649 if (res
=== false) {
4650 ev
.preventDefault();
4655 // Updates internal state and triggers handlers for when an event element is moused over
4656 handleSegMouseover: function(seg
, ev
) {
4658 !GlobalEmitter
.get().shouldIgnoreMouse() &&
4661 this.mousedOverSeg
= seg
;
4662 if (this.view
.isEventResizable(seg
.event
)) {
4663 seg
.el
.addClass('fc-allow-mouse-resize');
4665 this.view
.publiclyTrigger('eventMouseover', seg
.el
[0], seg
.event
, ev
);
4670 // Updates internal state and triggers handlers for when an event element is moused out.
4671 // Can be given no arguments, in which case it will mouseout the segment that was previously moused over.
4672 handleSegMouseout: function(seg
, ev
) {
4673 ev
= ev
|| {}; // if given no args, make a mock mouse event
4675 if (this.mousedOverSeg
) {
4676 seg
= seg
|| this.mousedOverSeg
; // if given no args, use the currently moused-over segment
4677 this.mousedOverSeg
= null;
4678 if (this.view
.isEventResizable(seg
.event
)) {
4679 seg
.el
.removeClass('fc-allow-mouse-resize');
4681 this.view
.publiclyTrigger('eventMouseout', seg
.el
[0], seg
.event
, ev
);
4686 handleSegMousedown: function(seg
, ev
) {
4687 var isResizing
= this.startSegResize(seg
, ev
, { distance
: 5 });
4689 if (!isResizing
&& this.view
.isEventDraggable(seg
.event
)) {
4690 this.buildSegDragListener(seg
)
4691 .startInteraction(ev
, {
4698 handleSegTouchStart: function(seg
, ev
) {
4699 var view
= this.view
;
4700 var event
= seg
.event
;
4701 var isSelected
= view
.isEventSelected(event
);
4702 var isDraggable
= view
.isEventDraggable(event
);
4703 var isResizable
= view
.isEventResizable(event
);
4704 var isResizing
= false;
4706 var eventLongPressDelay
;
4708 if (isSelected
&& isResizable
) {
4709 // only allow resizing of the event is selected
4710 isResizing
= this.startSegResize(seg
, ev
);
4713 if (!isResizing
&& (isDraggable
|| isResizable
)) { // allowed to be selected?
4715 eventLongPressDelay
= view
.opt('eventLongPressDelay');
4716 if (eventLongPressDelay
== null) {
4717 eventLongPressDelay
= view
.opt('longPressDelay'); // fallback
4720 dragListener
= isDraggable
?
4721 this.buildSegDragListener(seg
) :
4722 this.buildSegSelectListener(seg
); // seg isn't draggable, but still needs to be selected
4724 dragListener
.startInteraction(ev
, { // won't start if already started
4725 delay
: isSelected
? 0 : eventLongPressDelay
// do delay if not already selected
4731 // returns boolean whether resizing actually started or not.
4732 // assumes the seg allows resizing.
4733 // `dragOptions` are optional.
4734 startSegResize: function(seg
, ev
, dragOptions
) {
4735 if ($(ev
.target
).is('.fc-resizer')) {
4736 this.buildSegResizeListener(seg
, $(ev
.target
).is('.fc-start-resizer'))
4737 .startInteraction(ev
, dragOptions
);
4746 ------------------------------------------------------------------------------------------------------------------*/
4749 // Builds a listener that will track user-dragging on an event segment.
4750 // Generic enough to work with any type of Grid.
4751 // Has side effect of setting/unsetting `segDragListener`
4752 buildSegDragListener: function(seg
) {
4754 var view
= this.view
;
4755 var calendar
= view
.calendar
;
4757 var event
= seg
.event
;
4759 var mouseFollower
; // A clone of the original element that will move with the mouse
4760 var dropLocation
; // zoned event date properties
4762 if (this.segDragListener
) {
4763 return this.segDragListener
;
4766 // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents
4768 var dragListener
= this.segDragListener
= new HitDragListener(view
, {
4769 scroll
: view
.opt('dragScroll'),
4771 subjectCenter
: true,
4772 interactionStart: function(ev
) {
4773 seg
.component
= _this
; // for renderDrag
4775 mouseFollower
= new MouseFollower(seg
.el
, {
4776 additionalClass
: 'fc-dragging',
4778 opacity
: dragListener
.isTouch
? null : view
.opt('dragOpacity'),
4779 revertDuration
: view
.opt('dragRevertDuration'),
4780 zIndex
: 2 // one above the .fc-view
4782 mouseFollower
.hide(); // don't show until we know this is a real drag
4783 mouseFollower
.start(ev
);
4785 dragStart: function(ev
) {
4786 if (dragListener
.isTouch
&& !view
.isEventSelected(event
)) {
4787 // if not previously selected, will fire after a delay. then, select the event
4788 view
.selectEvent(event
);
4791 _this
.handleSegMouseout(seg
, ev
); // ensure a mouseout on the manipulated event has been reported
4792 _this
.segDragStart(seg
, ev
);
4793 view
.hideEvent(event
); // hide all event segments. our mouseFollower will take over
4795 hitOver: function(hit
, isOrig
, origHit
) {
4798 // starting hit could be forced (DayGrid.limit)
4803 // since we are querying the parent view, might not belong to this grid
4804 dropLocation
= _this
.computeEventDrop(
4805 origHit
.component
.getHitSpan(origHit
),
4806 hit
.component
.getHitSpan(hit
),
4810 if (dropLocation
&& !calendar
.isEventSpanAllowed(_this
.eventToSpan(dropLocation
), event
)) {
4812 dropLocation
= null;
4815 // if a valid drop location, have the subclass render a visual indication
4816 if (dropLocation
&& (dragHelperEls
= view
.renderDrag(dropLocation
, seg
))) {
4818 dragHelperEls
.addClass('fc-dragging');
4819 if (!dragListener
.isTouch
) {
4820 _this
.applyDragOpacity(dragHelperEls
);
4823 mouseFollower
.hide(); // if the subclass is already using a mock event "helper", hide our own
4826 mouseFollower
.show(); // otherwise, have the helper follow the mouse (no snapping)
4830 dropLocation
= null; // needs to have moved hits to be a valid drop
4833 hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
4834 view
.unrenderDrag(); // unrender whatever was done in renderDrag
4835 mouseFollower
.show(); // show in case we are moving out of all hits
4836 dropLocation
= null;
4838 hitDone: function() { // Called after a hitOut OR before a dragEnd
4841 interactionEnd: function(ev
) {
4842 delete seg
.component
; // prevent side effects
4844 // do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
4845 mouseFollower
.stop(!dropLocation
, function() {
4847 view
.unrenderDrag();
4848 _this
.segDragStop(seg
, ev
);
4852 // no need to re-show original, will rerender all anyways. esp important if eventRenderWait
4853 view
.reportSegDrop(seg
, dropLocation
, _this
.largeUnit
, el
, ev
);
4856 view
.showEvent(event
);
4859 _this
.segDragListener
= null;
4863 return dragListener
;
4867 // seg isn't draggable, but let's use a generic DragListener
4868 // simply for the delay, so it can be selected.
4869 // Has side effect of setting/unsetting `segDragListener`
4870 buildSegSelectListener: function(seg
) {
4872 var view
= this.view
;
4873 var event
= seg
.event
;
4875 if (this.segDragListener
) {
4876 return this.segDragListener
;
4879 var dragListener
= this.segDragListener
= new DragListener({
4880 dragStart: function(ev
) {
4881 if (dragListener
.isTouch
&& !view
.isEventSelected(event
)) {
4882 // if not previously selected, will fire after a delay. then, select the event
4883 view
.selectEvent(event
);
4886 interactionEnd: function(ev
) {
4887 _this
.segDragListener
= null;
4891 return dragListener
;
4895 // Called before event segment dragging starts
4896 segDragStart: function(seg
, ev
) {
4897 this.isDraggingSeg
= true;
4898 this.view
.publiclyTrigger('eventDragStart', seg
.el
[0], seg
.event
, ev
, {}); // last argument is jqui dummy
4902 // Called after event segment dragging stops
4903 segDragStop: function(seg
, ev
) {
4904 this.isDraggingSeg
= false;
4905 this.view
.publiclyTrigger('eventDragStop', seg
.el
[0], seg
.event
, ev
, {}); // last argument is jqui dummy
4909 // Given the spans an event drag began, and the span event was dropped, calculates the new zoned start/end/allDay
4910 // values for the event. Subclasses may override and set additional properties to be used by renderDrag.
4911 // A falsy returned value indicates an invalid drop.
4912 // DOES NOT consider overlap/constraint.
4913 computeEventDrop: function(startSpan
, endSpan
, event
) {
4914 var calendar
= this.view
.calendar
;
4915 var dragStart
= startSpan
.start
;
4916 var dragEnd
= endSpan
.start
;
4918 var dropLocation
; // zoned event date properties
4920 if (dragStart
.hasTime() === dragEnd
.hasTime()) {
4921 delta
= this.diffDates(dragEnd
, dragStart
);
4923 // if an all-day event was in a timed area and it was dragged to a different time,
4924 // guarantee an end and adjust start/end to have times
4925 if (event
.allDay
&& durationHasTime(delta
)) {
4927 start
: event
.start
.clone(),
4928 end
: calendar
.getEventEnd(event
), // will be an ambig day
4929 allDay
: false // for normalizeEventTimes
4931 calendar
.normalizeEventTimes(dropLocation
);
4933 // othewise, work off existing values
4935 dropLocation
= pluckEventDateProps(event
);
4938 dropLocation
.start
.add(delta
);
4939 if (dropLocation
.end
) {
4940 dropLocation
.end
.add(delta
);
4944 // if switching from day <-> timed, start should be reset to the dropped date, and the end cleared
4946 start
: dragEnd
.clone(),
4947 end
: null, // end should be cleared
4948 allDay
: !dragEnd
.hasTime()
4952 return dropLocation
;
4956 // Utility for apply dragOpacity to a jQuery set
4957 applyDragOpacity: function(els
) {
4958 var opacity
= this.view
.opt('dragOpacity');
4960 if (opacity
!= null) {
4961 els
.css('opacity', opacity
);
4966 /* External Element Dragging
4967 ------------------------------------------------------------------------------------------------------------------*/
4970 // Called when a jQuery UI drag is initiated anywhere in the DOM
4971 externalDragStart: function(ev
, ui
) {
4972 var view
= this.view
;
4976 if (view
.opt('droppable')) { // only listen if this setting is on
4977 el
= $((ui
? ui
.item
: null) || ev
.target
);
4979 // Test that the dragged element passes the dropAccept selector or filter function.
4980 // FYI, the default is "*" (matches all)
4981 accept
= view
.opt('dropAccept');
4982 if ($.isFunction(accept
) ? accept
.call(el
[0], el
) : el
.is(accept
)) {
4983 if (!this.isDraggingExternal
) { // prevent double-listening if fired twice
4984 this.listenToExternalDrag(el
, ev
, ui
);
4991 // Called when a jQuery UI drag starts and it needs to be monitored for dropping
4992 listenToExternalDrag: function(el
, ev
, ui
) {
4994 var calendar
= this.view
.calendar
;
4995 var meta
= getDraggedElMeta(el
); // extra data about event drop, including possible event to create
4996 var dropLocation
; // a null value signals an unsuccessful drag
4998 // listener that tracks mouse movement over date-associated pixel regions
4999 var dragListener
= _this
.externalDragListener
= new HitDragListener(this, {
5000 interactionStart: function() {
5001 _this
.isDraggingExternal
= true;
5003 hitOver: function(hit
) {
5004 dropLocation
= _this
.computeExternalDrop(
5005 hit
.component
.getHitSpan(hit
), // since we are querying the parent view, might not belong to this grid
5009 if ( // invalid hit?
5011 !calendar
.isExternalSpanAllowed(_this
.eventToSpan(dropLocation
), dropLocation
, meta
.eventProps
)
5014 dropLocation
= null;
5018 _this
.renderDrag(dropLocation
); // called without a seg parameter
5021 hitOut: function() {
5022 dropLocation
= null; // signal unsuccessful
5024 hitDone: function() { // Called after a hitOut OR before a dragEnd
5026 _this
.unrenderDrag();
5028 interactionEnd: function(ev
) {
5029 if (dropLocation
) { // element was dropped on a valid hit
5030 _this
.view
.reportExternalDrop(meta
, dropLocation
, el
, ev
, ui
);
5032 _this
.isDraggingExternal
= false;
5033 _this
.externalDragListener
= null;
5037 dragListener
.startDrag(ev
); // start listening immediately
5041 // Given a hit to be dropped upon, and misc data associated with the jqui drag (guaranteed to be a plain object),
5042 // returns the zoned start/end dates for the event that would result from the hypothetical drop. end might be null.
5043 // Returning a null value signals an invalid drop hit.
5044 // DOES NOT consider overlap/constraint.
5045 computeExternalDrop: function(span
, meta
) {
5046 var calendar
= this.view
.calendar
;
5047 var dropLocation
= {
5048 start
: calendar
.applyTimezone(span
.start
), // simulate a zoned event start date
5052 // if dropped on an all-day span, and element's metadata specified a time, set it
5053 if (meta
.startTime
&& !dropLocation
.start
.hasTime()) {
5054 dropLocation
.start
.time(meta
.startTime
);
5057 if (meta
.duration
) {
5058 dropLocation
.end
= dropLocation
.start
.clone().add(meta
.duration
);
5061 return dropLocation
;
5066 /* Drag Rendering (for both events and an external elements)
5067 ------------------------------------------------------------------------------------------------------------------*/
5070 // Renders a visual indication of an event or external element being dragged.
5071 // `dropLocation` contains hypothetical start/end/allDay values the event would have if dropped. end can be null.
5072 // `seg` is the internal segment object that is being dragged. If dragging an external element, `seg` is null.
5073 // A truthy returned value indicates this method has rendered a helper element.
5074 // Must return elements used for any mock events.
5075 renderDrag: function(dropLocation
, seg
) {
5076 // subclasses must implement
5080 // Unrenders a visual indication of an event or external element being dragged
5081 unrenderDrag: function() {
5082 // subclasses must implement
5087 ------------------------------------------------------------------------------------------------------------------*/
5090 // Creates a listener that tracks the user as they resize an event segment.
5091 // Generic enough to work with any type of Grid.
5092 buildSegResizeListener: function(seg
, isStart
) {
5094 var view
= this.view
;
5095 var calendar
= view
.calendar
;
5097 var event
= seg
.event
;
5098 var eventEnd
= calendar
.getEventEnd(event
);
5100 var resizeLocation
; // zoned event date properties. falsy if invalid resize
5102 // Tracks mouse movement over the *grid's* coordinate map
5103 var dragListener
= this.segResizeListener
= new HitDragListener(this, {
5104 scroll
: view
.opt('dragScroll'),
5106 interactionStart: function() {
5109 dragStart: function(ev
) {
5111 _this
.handleSegMouseout(seg
, ev
); // ensure a mouseout on the manipulated event has been reported
5112 _this
.segResizeStart(seg
, ev
);
5114 hitOver: function(hit
, isOrig
, origHit
) {
5115 var origHitSpan
= _this
.getHitSpan(origHit
);
5116 var hitSpan
= _this
.getHitSpan(hit
);
5118 resizeLocation
= isStart
?
5119 _this
.computeEventStartResize(origHitSpan
, hitSpan
, event
) :
5120 _this
.computeEventEndResize(origHitSpan
, hitSpan
, event
);
5122 if (resizeLocation
) {
5123 if (!calendar
.isEventSpanAllowed(_this
.eventToSpan(resizeLocation
), event
)) {
5125 resizeLocation
= null;
5127 // no change? (FYI, event dates might have zones)
5129 resizeLocation
.start
.isSame(event
.start
.clone().stripZone()) &&
5130 resizeLocation
.end
.isSame(eventEnd
.clone().stripZone())
5132 resizeLocation
= null;
5136 if (resizeLocation
) {
5137 view
.hideEvent(event
);
5138 _this
.renderEventResize(resizeLocation
, seg
);
5141 hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
5142 resizeLocation
= null;
5143 view
.showEvent(event
); // for when out-of-bounds. show original
5145 hitDone: function() { // resets the rendering to show the original event
5146 _this
.unrenderEventResize();
5149 interactionEnd: function(ev
) {
5151 _this
.segResizeStop(seg
, ev
);
5154 if (resizeLocation
) { // valid date to resize to?
5155 // no need to re-show original, will rerender all anyways. esp important if eventRenderWait
5156 view
.reportSegResize(seg
, resizeLocation
, _this
.largeUnit
, el
, ev
);
5159 view
.showEvent(event
);
5161 _this
.segResizeListener
= null;
5165 return dragListener
;
5169 // Called before event segment resizing starts
5170 segResizeStart: function(seg
, ev
) {
5171 this.isResizingSeg
= true;
5172 this.view
.publiclyTrigger('eventResizeStart', seg
.el
[0], seg
.event
, ev
, {}); // last argument is jqui dummy
5176 // Called after event segment resizing stops
5177 segResizeStop: function(seg
, ev
) {
5178 this.isResizingSeg
= false;
5179 this.view
.publiclyTrigger('eventResizeStop', seg
.el
[0], seg
.event
, ev
, {}); // last argument is jqui dummy
5183 // Returns new date-information for an event segment being resized from its start
5184 computeEventStartResize: function(startSpan
, endSpan
, event
) {
5185 return this.computeEventResize('start', startSpan
, endSpan
, event
);
5189 // Returns new date-information for an event segment being resized from its end
5190 computeEventEndResize: function(startSpan
, endSpan
, event
) {
5191 return this.computeEventResize('end', startSpan
, endSpan
, event
);
5195 // Returns new zoned date information for an event segment being resized from its start OR end
5196 // `type` is either 'start' or 'end'.
5197 // DOES NOT consider overlap/constraint.
5198 computeEventResize: function(type
, startSpan
, endSpan
, event
) {
5199 var calendar
= this.view
.calendar
;
5200 var delta
= this.diffDates(endSpan
[type
], startSpan
[type
]);
5201 var resizeLocation
; // zoned event date properties
5202 var defaultDuration
;
5204 // build original values to work from, guaranteeing a start and end
5206 start
: event
.start
.clone(),
5207 end
: calendar
.getEventEnd(event
),
5208 allDay
: event
.allDay
5211 // if an all-day event was in a timed area and was resized to a time, adjust start/end to have times
5212 if (resizeLocation
.allDay
&& durationHasTime(delta
)) {
5213 resizeLocation
.allDay
= false;
5214 calendar
.normalizeEventTimes(resizeLocation
);
5217 resizeLocation
[type
].add(delta
); // apply delta to start or end
5219 // if the event was compressed too small, find a new reasonable duration for it
5220 if (!resizeLocation
.start
.isBefore(resizeLocation
.end
)) {
5223 this.minResizeDuration
|| // TODO: hack
5225 calendar
.defaultAllDayEventDuration
:
5226 calendar
.defaultTimedEventDuration
);
5228 if (type
== 'start') { // resizing the start?
5229 resizeLocation
.start
= resizeLocation
.end
.clone().subtract(defaultDuration
);
5231 else { // resizing the end?
5232 resizeLocation
.end
= resizeLocation
.start
.clone().add(defaultDuration
);
5236 return resizeLocation
;
5240 // Renders a visual indication of an event being resized.
5241 // `range` has the updated dates of the event. `seg` is the original segment object involved in the drag.
5242 // Must return elements used for any mock events.
5243 renderEventResize: function(range
, seg
) {
5244 // subclasses must implement
5248 // Unrenders a visual indication of an event being resized.
5249 unrenderEventResize: function() {
5250 // subclasses must implement
5255 ------------------------------------------------------------------------------------------------------------------*/
5258 // Compute the text that should be displayed on an event's element.
5259 // `range` can be the Event object itself, or something range-like, with at least a `start`.
5260 // If event times are disabled, or the event has no time, will return a blank string.
5261 // If not specified, formatStr will default to the eventTimeFormat setting,
5262 // and displayEnd will default to the displayEventEnd setting.
5263 getEventTimeText: function(range
, formatStr
, displayEnd
) {
5265 if (formatStr
== null) {
5266 formatStr
= this.eventTimeFormat
;
5269 if (displayEnd
== null) {
5270 displayEnd
= this.displayEventEnd
;
5273 if (this.displayEventTime
&& range
.start
.hasTime()) {
5274 if (displayEnd
&& range
.end
) {
5275 return this.view
.formatRange(range
, formatStr
);
5278 return range
.start
.format(formatStr
);
5286 // Generic utility for generating the HTML classNames for an event segment's element
5287 getSegClasses: function(seg
, isDraggable
, isResizable
) {
5288 var view
= this.view
;
5291 seg
.isStart
? 'fc-start' : 'fc-not-start',
5292 seg
.isEnd
? 'fc-end' : 'fc-not-end'
5293 ].concat(this.getSegCustomClasses(seg
));
5296 classes
.push('fc-draggable');
5299 classes
.push('fc-resizable');
5302 // event is currently selected? attach a className.
5303 if (view
.isEventSelected(seg
.event
)) {
5304 classes
.push('fc-selected');
5311 // List of classes that were defined by the caller of the API in some way
5312 getSegCustomClasses: function(seg
) {
5313 var event
= seg
.event
;
5316 event
.className
, // guaranteed to be an array
5317 event
.source
? event
.source
.className
: []
5322 // Utility for generating event skin-related CSS properties
5323 getSegSkinCss: function(seg
) {
5325 'background-color': this.getSegBackgroundColor(seg
),
5326 'border-color': this.getSegBorderColor(seg
),
5327 color
: this.getSegTextColor(seg
)
5332 // Queries for caller-specified color, then falls back to default
5333 getSegBackgroundColor: function(seg
) {
5334 return seg
.event
.backgroundColor
||
5336 this.getSegDefaultBackgroundColor(seg
);
5340 getSegDefaultBackgroundColor: function(seg
) {
5341 var source
= seg
.event
.source
|| {};
5343 return source
.backgroundColor
||
5345 this.view
.opt('eventBackgroundColor') ||
5346 this.view
.opt('eventColor');
5350 // Queries for caller-specified color, then falls back to default
5351 getSegBorderColor: function(seg
) {
5352 return seg
.event
.borderColor
||
5354 this.getSegDefaultBorderColor(seg
);
5358 getSegDefaultBorderColor: function(seg
) {
5359 var source
= seg
.event
.source
|| {};
5361 return source
.borderColor
||
5363 this.view
.opt('eventBorderColor') ||
5364 this.view
.opt('eventColor');
5368 // Queries for caller-specified color, then falls back to default
5369 getSegTextColor: function(seg
) {
5370 return seg
.event
.textColor
||
5371 this.getSegDefaultTextColor(seg
);
5375 getSegDefaultTextColor: function(seg
) {
5376 var source
= seg
.event
.source
|| {};
5378 return source
.textColor
||
5379 this.view
.opt('eventTextColor');
5383 /* Converting events -> eventRange -> eventSpan -> eventSegs
5384 ------------------------------------------------------------------------------------------------------------------*/
5387 // Generates an array of segments for the given single event
5388 // Can accept an event "location" as well (which only has start/end and no allDay)
5389 eventToSegs: function(event
) {
5390 return this.eventsToSegs([ event
]);
5394 eventToSpan: function(event
) {
5395 return this.eventToSpans(event
)[0];
5399 // Generates spans (always unzoned) for the given event.
5400 // Does not do any inverting for inverse-background events.
5401 // Can accept an event "location" as well (which only has start/end and no allDay)
5402 eventToSpans: function(event
) {
5403 var range
= this.eventToRange(event
);
5404 return this.eventRangeToSpans(range
, event
);
5409 // Converts an array of event objects into an array of event segment objects.
5410 // A custom `segSliceFunc` may be given for arbitrarily slicing up events.
5411 // Doesn't guarantee an order for the resulting array.
5412 eventsToSegs: function(allEvents
, segSliceFunc
) {
5414 var eventsById
= groupEventsById(allEvents
);
5417 $.each(eventsById
, function(id
, events
) {
5421 for (i
= 0; i
< events
.length
; i
++) {
5422 ranges
.push(_this
.eventToRange(events
[i
]));
5425 // inverse-background events (utilize only the first event in calculations)
5426 if (isInverseBgEvent(events
[0])) {
5427 ranges
= _this
.invertRanges(ranges
);
5429 for (i
= 0; i
< ranges
.length
; i
++) {
5430 segs
.push
.apply(segs
, // append to
5431 _this
.eventRangeToSegs(ranges
[i
], events
[0], segSliceFunc
));
5434 // normal event ranges
5436 for (i
= 0; i
< ranges
.length
; i
++) {
5437 segs
.push
.apply(segs
, // append to
5438 _this
.eventRangeToSegs(ranges
[i
], events
[i
], segSliceFunc
));
5447 // Generates the unzoned start/end dates an event appears to occupy
5448 // Can accept an event "location" as well (which only has start/end and no allDay)
5449 eventToRange: function(event
) {
5450 var calendar
= this.view
.calendar
;
5451 var start
= event
.start
.clone().stripZone();
5455 // derive the end from the start and allDay. compute allDay if necessary
5456 calendar
.getDefaultEventEnd(
5457 event
.allDay
!= null ?
5459 !event
.start
.hasTime(),
5464 // hack: dynamic locale change forgets to upate stored event localed
5465 calendar
.localizeMoment(start
);
5466 calendar
.localizeMoment(end
);
5468 return { start
: start
, end
: end
};
5472 // Given an event's range (unzoned start/end), and the event itself,
5473 // slice into segments (using the segSliceFunc function if specified)
5474 eventRangeToSegs: function(range
, event
, segSliceFunc
) {
5475 var spans
= this.eventRangeToSpans(range
, event
);
5479 for (i
= 0; i
< spans
.length
; i
++) {
5480 segs
.push
.apply(segs
, // append to
5481 this.eventSpanToSegs(spans
[i
], event
, segSliceFunc
));
5488 // Given an event's unzoned date range, return an array of "span" objects.
5489 // Subclasses can override.
5490 eventRangeToSpans: function(range
, event
) {
5491 return [ $.extend({}, range
) ]; // copy into a single-item array
5495 // Given an event's span (unzoned start/end and other misc data), and the event itself,
5496 // slices into segments and attaches event-derived properties to them.
5497 eventSpanToSegs: function(span
, event
, segSliceFunc
) {
5498 var segs
= segSliceFunc
? segSliceFunc(span
) : this.spanToSegs(span
);
5501 for (i
= 0; i
< segs
.length
; i
++) {
5504 seg
.eventStartMS
= +span
.start
; // TODO: not the best name after making spans unzoned
5505 seg
.eventDurationMS
= span
.end
- span
.start
;
5512 // Produces a new array of range objects that will cover all the time NOT covered by the given ranges.
5513 // SIDE EFFECT: will mutate the given array and will use its date references.
5514 invertRanges: function(ranges
) {
5515 var view
= this.view
;
5516 var viewStart
= view
.start
.clone(); // need a copy
5517 var viewEnd
= view
.end
.clone(); // need a copy
5518 var inverseRanges
= [];
5519 var start
= viewStart
; // the end of the previous range. the start of the new range
5522 // ranges need to be in order. required for our date-walking algorithm
5523 ranges
.sort(compareRanges
);
5525 for (i
= 0; i
< ranges
.length
; i
++) {
5528 // add the span of time before the event (if there is any)
5529 if (range
.start
> start
) { // compare millisecond time (skip any ambig logic)
5530 inverseRanges
.push({
5539 // add the span of time after the last event (if there is any)
5540 if (start
< viewEnd
) { // compare millisecond time (skip any ambig logic)
5541 inverseRanges
.push({
5547 return inverseRanges
;
5551 sortEventSegs: function(segs
) {
5552 segs
.sort(proxy(this, 'compareEventSegs'));
5556 // A cmp function for determining which segments should take visual priority
5557 compareEventSegs: function(seg1
, seg2
) {
5558 return seg1
.eventStartMS
- seg2
.eventStartMS
|| // earlier events go first
5559 seg2
.eventDurationMS
- seg1
.eventDurationMS
|| // tie? longer events go first
5560 seg2
.event
.allDay
- seg1
.event
.allDay
|| // tie? put all-day events first (booleans cast to 0/1)
5561 compareByFieldSpecs(seg1
.event
, seg2
.event
, this.view
.eventOrderSpecs
);
5568 ----------------------------------------------------------------------------------------------------------------------*/
5571 function pluckEventDateProps(event
) {
5573 start
: event
.start
.clone(),
5574 end
: event
.end
? event
.end
.clone() : null,
5575 allDay
: event
.allDay
// keep it the same
5578 FC
.pluckEventDateProps
= pluckEventDateProps
;
5581 function isBgEvent(event
) { // returns true if background OR inverse-background
5582 var rendering
= getEventRendering(event
);
5583 return rendering
=== 'background' || rendering
=== 'inverse-background';
5585 FC
.isBgEvent
= isBgEvent
; // export
5588 function isInverseBgEvent(event
) {
5589 return getEventRendering(event
) === 'inverse-background';
5593 function getEventRendering(event
) {
5594 return firstDefined((event
.source
|| {}).rendering
, event
.rendering
);
5598 function groupEventsById(events
) {
5599 var eventsById
= {};
5602 for (i
= 0; i
< events
.length
; i
++) {
5604 (eventsById
[event
._id
] || (eventsById
[event
._id
] = [])).push(event
);
5611 // A cmp function for determining which non-inverted "ranges" (see above) happen earlier
5612 function compareRanges(range1
, range2
) {
5613 return range1
.start
- range2
.start
; // earlier ranges go first
5617 /* External-Dragging-Element Data
5618 ----------------------------------------------------------------------------------------------------------------------*/
5620 // Require all HTML5 data-* attributes used by FullCalendar to have this prefix.
5621 // A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event.
5622 FC
.dataAttrPrefix
= '';
5624 // Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure
5625 // to be used for Event Object creation.
5626 // A defined `.eventProps`, even when empty, indicates that an event should be created.
5627 function getDraggedElMeta(el
) {
5628 var prefix
= FC
.dataAttrPrefix
;
5629 var eventProps
; // properties for creating the event, not related to date/time
5630 var startTime
; // a Duration
5634 if (prefix
) { prefix
+= '-'; }
5635 eventProps
= el
.data(prefix
+ 'event') || null;
5638 if (typeof eventProps
=== 'object') {
5639 eventProps
= $.extend({}, eventProps
); // make a copy
5641 else { // something like 1 or true. still signal event creation
5645 // pluck special-cased date/time properties
5646 startTime
= eventProps
.start
;
5647 if (startTime
== null) { startTime
= eventProps
.time
; } // accept 'time' as well
5648 duration
= eventProps
.duration
;
5649 stick
= eventProps
.stick
;
5650 delete eventProps
.start
;
5651 delete eventProps
.time
;
5652 delete eventProps
.duration
;
5653 delete eventProps
.stick
;
5656 // fallback to standalone attribute values for each of the date/time properties
5657 if (startTime
== null) { startTime
= el
.data(prefix
+ 'start'); }
5658 if (startTime
== null) { startTime
= el
.data(prefix
+ 'time'); } // accept 'time' as well
5659 if (duration
== null) { duration
= el
.data(prefix
+ 'duration'); }
5660 if (stick
== null) { stick
= el
.data(prefix
+ 'stick'); }
5662 // massage into correct data types
5663 startTime
= startTime
!= null ? moment
.duration(startTime
) : null;
5664 duration
= duration
!= null ? moment
.duration(duration
) : null;
5665 stick
= Boolean(stick
);
5667 return { eventProps
: eventProps
, startTime
: startTime
, duration
: duration
, stick
: stick
};
5674 A set of rendering and date-related methods for a visual component comprised of one or more rows of day columns.
5675 Prerequisite: the object being mixed into needs to be a *Grid*
5677 var DayTableMixin
= FC
.DayTableMixin
= {
5679 breakOnWeeks
: false, // should create a new row for each week?
5680 dayDates
: null, // whole-day dates for each column. left to right
5681 dayIndices
: null, // for each day from start, the offset
5685 colHeadFormat
: null,
5688 // Populates internal variables used for date calculation and rendering
5689 updateDayTable: function() {
5690 var view
= this.view
;
5691 var date
= this.start
.clone();
5693 var dayIndices
= [];
5699 while (date
.isBefore(this.end
)) { // loop each day from start to end
5700 if (view
.isHiddenDay(date
)) {
5701 dayIndices
.push(dayIndex
+ 0.5); // mark that it's between indices
5705 dayIndices
.push(dayIndex
);
5706 dayDates
.push(date
.clone());
5708 date
.add(1, 'days');
5711 if (this.breakOnWeeks
) {
5712 // count columns until the day-of-week repeats
5713 firstDay
= dayDates
[0].day();
5714 for (daysPerRow
= 1; daysPerRow
< dayDates
.length
; daysPerRow
++) {
5715 if (dayDates
[daysPerRow
].day() == firstDay
) {
5719 rowCnt
= Math
.ceil(dayDates
.length
/ daysPerRow
);
5723 daysPerRow
= dayDates
.length
;
5726 this.dayDates
= dayDates
;
5727 this.dayIndices
= dayIndices
;
5728 this.daysPerRow
= daysPerRow
;
5729 this.rowCnt
= rowCnt
;
5731 this.updateDayTableCols();
5735 // Computes and assigned the colCnt property and updates any options that may be computed from it
5736 updateDayTableCols: function() {
5737 this.colCnt
= this.computeColCnt();
5738 this.colHeadFormat
= this.view
.opt('columnFormat') || this.computeColHeadFormat();
5742 // Determines how many columns there should be in the table
5743 computeColCnt: function() {
5744 return this.daysPerRow
;
5748 // Computes the ambiguously-timed moment for the given cell
5749 getCellDate: function(row
, col
) {
5750 return this.dayDates
[
5751 this.getCellDayIndex(row
, col
)
5756 // Computes the ambiguously-timed date range for the given cell
5757 getCellRange: function(row
, col
) {
5758 var start
= this.getCellDate(row
, col
);
5759 var end
= start
.clone().add(1, 'days');
5761 return { start
: start
, end
: end
};
5765 // Returns the number of day cells, chronologically, from the first of the grid (0-based)
5766 getCellDayIndex: function(row
, col
) {
5767 return row
* this.daysPerRow
+ this.getColDayIndex(col
);
5771 // Returns the numner of day cells, chronologically, from the first cell in *any given row*
5772 getColDayIndex: function(col
) {
5774 return this.colCnt
- 1 - col
;
5782 // Given a date, returns its chronolocial cell-index from the first cell of the grid.
5783 // If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets.
5784 // If before the first offset, returns a negative number.
5785 // If after the last offset, returns an offset past the last cell offset.
5786 // Only works for *start* dates of cells. Will not work for exclusive end dates for cells.
5787 getDateDayIndex: function(date
) {
5788 var dayIndices
= this.dayIndices
;
5789 var dayOffset
= date
.diff(this.start
, 'days');
5791 if (dayOffset
< 0) {
5792 return dayIndices
[0] - 1;
5794 else if (dayOffset
>= dayIndices
.length
) {
5795 return dayIndices
[dayIndices
.length
- 1] + 1;
5798 return dayIndices
[dayOffset
];
5804 ------------------------------------------------------------------------------------------------------------------*/
5807 // Computes a default column header formatting string if `colFormat` is not explicitly defined
5808 computeColHeadFormat: function() {
5809 // if more than one week row, or if there are a lot of columns with not much space,
5810 // put just the day numbers will be in each cell
5811 if (this.rowCnt
> 1 || this.colCnt
> 10) {
5812 return 'ddd'; // "Sat"
5814 // multiple days, so full single date string WON'T be in title text
5815 else if (this.colCnt
> 1) {
5816 return this.view
.opt('dayOfMonthFormat'); // "Sat 12/10"
5818 // single day, so full single date string will probably be in title text
5820 return 'dddd'; // "Saturday"
5826 ------------------------------------------------------------------------------------------------------------------*/
5829 // Slices up a date range into a segment for every week-row it intersects with
5830 sliceRangeByRow: function(range
) {
5831 var daysPerRow
= this.daysPerRow
;
5832 var normalRange
= this.view
.computeDayRange(range
); // make whole-day range, considering nextDayThreshold
5833 var rangeFirst
= this.getDateDayIndex(normalRange
.start
); // inclusive first index
5834 var rangeLast
= this.getDateDayIndex(normalRange
.end
.clone().subtract(1, 'days')); // inclusive last index
5837 var rowFirst
, rowLast
; // inclusive day-index range for current row
5838 var segFirst
, segLast
; // inclusive day-index range for segment
5840 for (row
= 0; row
< this.rowCnt
; row
++) {
5841 rowFirst
= row
* daysPerRow
;
5842 rowLast
= rowFirst
+ daysPerRow
- 1;
5844 // intersect segment's offset range with the row's
5845 segFirst
= Math
.max(rangeFirst
, rowFirst
);
5846 segLast
= Math
.min(rangeLast
, rowLast
);
5848 // deal with in-between indices
5849 segFirst
= Math
.ceil(segFirst
); // in-between starts round to next cell
5850 segLast
= Math
.floor(segLast
); // in-between ends round to prev cell
5852 if (segFirst
<= segLast
) { // was there any intersection with the current row?
5856 // normalize to start of row
5857 firstRowDayIndex
: segFirst
- rowFirst
,
5858 lastRowDayIndex
: segLast
- rowFirst
,
5860 // must be matching integers to be the segment's start/end
5861 isStart
: segFirst
=== rangeFirst
,
5862 isEnd
: segLast
=== rangeLast
5871 // Slices up a date range into a segment for every day-cell it intersects with.
5872 // TODO: make more DRY with sliceRangeByRow somehow.
5873 sliceRangeByDay: function(range
) {
5874 var daysPerRow
= this.daysPerRow
;
5875 var normalRange
= this.view
.computeDayRange(range
); // make whole-day range, considering nextDayThreshold
5876 var rangeFirst
= this.getDateDayIndex(normalRange
.start
); // inclusive first index
5877 var rangeLast
= this.getDateDayIndex(normalRange
.end
.clone().subtract(1, 'days')); // inclusive last index
5880 var rowFirst
, rowLast
; // inclusive day-index range for current row
5882 var segFirst
, segLast
; // inclusive day-index range for segment
5884 for (row
= 0; row
< this.rowCnt
; row
++) {
5885 rowFirst
= row
* daysPerRow
;
5886 rowLast
= rowFirst
+ daysPerRow
- 1;
5888 for (i
= rowFirst
; i
<= rowLast
; i
++) {
5890 // intersect segment's offset range with the row's
5891 segFirst
= Math
.max(rangeFirst
, i
);
5892 segLast
= Math
.min(rangeLast
, i
);
5894 // deal with in-between indices
5895 segFirst
= Math
.ceil(segFirst
); // in-between starts round to next cell
5896 segLast
= Math
.floor(segLast
); // in-between ends round to prev cell
5898 if (segFirst
<= segLast
) { // was there any intersection with the current row?
5902 // normalize to start of row
5903 firstRowDayIndex
: segFirst
- rowFirst
,
5904 lastRowDayIndex
: segLast
- rowFirst
,
5906 // must be matching integers to be the segment's start/end
5907 isStart
: segFirst
=== rangeFirst
,
5908 isEnd
: segLast
=== rangeLast
5919 ------------------------------------------------------------------------------------------------------------------*/
5922 renderHeadHtml: function() {
5923 var view
= this.view
;
5926 '<div class="fc-row ' + view
.widgetHeaderClass
+ '">' +
5929 this.renderHeadTrHtml() +
5936 renderHeadIntroHtml: function() {
5937 return this.renderIntroHtml(); // fall back to generic
5941 renderHeadTrHtml: function() {
5944 (this.isRTL
? '' : this.renderHeadIntroHtml()) +
5945 this.renderHeadDateCellsHtml() +
5946 (this.isRTL
? this.renderHeadIntroHtml() : '') +
5951 renderHeadDateCellsHtml: function() {
5955 for (col
= 0; col
< this.colCnt
; col
++) {
5956 date
= this.getCellDate(0, col
);
5957 htmls
.push(this.renderHeadDateCellHtml(date
));
5960 return htmls
.join('');
5964 // TODO: when internalApiVersion, accept an object for HTML attributes
5965 // (colspan should be no different)
5966 renderHeadDateCellHtml: function(date
, colspan
, otherAttrs
) {
5967 var view
= this.view
;
5970 view
.widgetHeaderClass
5973 // if only one row of days, the classNames on the header can represent the specific days beneath
5974 if (this.rowCnt
=== 1) {
5975 classNames
= classNames
.concat(
5976 // includes the day-of-week class
5977 // noThemeHighlight=true (don't highlight the header)
5978 this.getDayClasses(date
, true)
5982 classNames
.push('fc-' + dayIDs
[date
.day()]); // only add the day-of-week class
5986 '<th class="' + classNames
.join(' ') + '"' +
5987 (this.rowCnt
=== 1 ?
5988 ' data-date="' + date
.format('YYYY-MM-DD') + '"' :
5991 ' colspan="' + colspan
+ '"' :
5997 // don't make a link if the heading could represent multiple days, or if there's only one day (forceOff)
5998 view
.buildGotoAnchorHtml(
5999 { date
: date
, forceOff
: this.rowCnt
> 1 || this.colCnt
=== 1 },
6000 htmlEscape(date
.format(this.colHeadFormat
)) // inner HTML
6006 /* Background Rendering
6007 ------------------------------------------------------------------------------------------------------------------*/
6010 renderBgTrHtml: function(row
) {
6013 (this.isRTL
? '' : this.renderBgIntroHtml(row
)) +
6014 this.renderBgCellsHtml(row
) +
6015 (this.isRTL
? this.renderBgIntroHtml(row
) : '') +
6020 renderBgIntroHtml: function(row
) {
6021 return this.renderIntroHtml(); // fall back to generic
6025 renderBgCellsHtml: function(row
) {
6029 for (col
= 0; col
< this.colCnt
; col
++) {
6030 date
= this.getCellDate(row
, col
);
6031 htmls
.push(this.renderBgCellHtml(date
));
6034 return htmls
.join('');
6038 renderBgCellHtml: function(date
, otherAttrs
) {
6039 var view
= this.view
;
6040 var classes
= this.getDayClasses(date
);
6042 classes
.unshift('fc-day', view
.widgetContentClass
);
6044 return '<td class="' + classes
.join(' ') + '"' +
6045 ' data-date="' + date
.format('YYYY-MM-DD') + '"' + // if date has a time, won't format it
6054 ------------------------------------------------------------------------------------------------------------------*/
6057 // Generates the default HTML intro for any row. User classes should override
6058 renderIntroHtml: function() {
6062 // TODO: a generic method for dealing with <tr>, RTL, intro
6063 // when increment internalApiVersion
6064 // wrapTr (scheduler)
6068 ------------------------------------------------------------------------------------------------------------------*/
6071 // Applies the generic "intro" and "outro" HTML to the given cells.
6072 // Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro.
6073 bookendCells: function(trEl
) {
6074 var introHtml
= this.renderIntroHtml();
6078 trEl
.append(introHtml
);
6081 trEl
.prepend(introHtml
);
6090 /* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week.
6091 ----------------------------------------------------------------------------------------------------------------------*/
6093 var DayGrid
= FC
.DayGrid
= Grid
.extend(DayTableMixin
, {
6095 numbersVisible
: false, // should render a row for day/week numbers? set by outside view. TODO: make internal
6096 bottomCoordPadding
: 0, // hack for extending the hit area for the last row of the coordinate grid
6098 rowEls
: null, // set of fake row elements
6099 cellEls
: null, // set of whole-day elements comprising the row's background
6100 helperEls
: null, // set of cell skeleton elements for rendering the mock event "helper"
6102 rowCoordCache
: null,
6103 colCoordCache
: null,
6106 // Renders the rows and columns into the component's `this.el`, which should already be assigned.
6107 // isRigid determins whether the individual rows should ignore the contents and be a constant height.
6108 // Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient.
6109 renderDates: function(isRigid
) {
6110 var view
= this.view
;
6111 var rowCnt
= this.rowCnt
;
6112 var colCnt
= this.colCnt
;
6117 for (row
= 0; row
< rowCnt
; row
++) {
6118 html
+= this.renderDayRowHtml(row
, isRigid
);
6122 this.rowEls
= this.el
.find('.fc-row');
6123 this.cellEls
= this.el
.find('.fc-day');
6125 this.rowCoordCache
= new CoordCache({
6129 this.colCoordCache
= new CoordCache({
6130 els
: this.cellEls
.slice(0, this.colCnt
), // only the first row
6134 // trigger dayRender with each cell's element
6135 for (row
= 0; row
< rowCnt
; row
++) {
6136 for (col
= 0; col
< colCnt
; col
++) {
6137 view
.publiclyTrigger(
6140 this.getCellDate(row
, col
),
6141 this.getCellEl(row
, col
)
6148 unrenderDates: function() {
6149 this.removeSegPopover();
6153 renderBusinessHours: function() {
6154 var segs
= this.buildBusinessHourSegs(true); // wholeDay=true
6155 this.renderFill('businessHours', segs
, 'bgevent');
6159 unrenderBusinessHours: function() {
6160 this.unrenderFill('businessHours');
6164 // Generates the HTML for a single row, which is a div that wraps a table.
6165 // `row` is the row number.
6166 renderDayRowHtml: function(row
, isRigid
) {
6167 var view
= this.view
;
6168 var classes
= [ 'fc-row', 'fc-week', view
.widgetContentClass
];
6171 classes
.push('fc-rigid');
6175 '<div class="' + classes
.join(' ') + '">' +
6176 '<div class="fc-bg">' +
6178 this.renderBgTrHtml(row
) +
6181 '<div class="fc-content-skeleton">' +
6183 (this.numbersVisible
?
6185 this.renderNumberTrHtml(row
) +
6195 /* Grid Number Rendering
6196 ------------------------------------------------------------------------------------------------------------------*/
6199 renderNumberTrHtml: function(row
) {
6202 (this.isRTL
? '' : this.renderNumberIntroHtml(row
)) +
6203 this.renderNumberCellsHtml(row
) +
6204 (this.isRTL
? this.renderNumberIntroHtml(row
) : '') +
6209 renderNumberIntroHtml: function(row
) {
6210 return this.renderIntroHtml();
6214 renderNumberCellsHtml: function(row
) {
6218 for (col
= 0; col
< this.colCnt
; col
++) {
6219 date
= this.getCellDate(row
, col
);
6220 htmls
.push(this.renderNumberCellHtml(date
));
6223 return htmls
.join('');
6227 // Generates the HTML for the <td>s of the "number" row in the DayGrid's content skeleton.
6228 // The number row will only exist if either day numbers or week numbers are turned on.
6229 renderNumberCellHtml: function(date
) {
6232 var weekCalcFirstDoW
;
6234 if (!this.view
.dayNumbersVisible
&& !this.view
.cellWeekNumbersVisible
) {
6235 // no numbers in day cell (week number must be along the side)
6236 return '<td/>'; // will create an empty space above events :(
6239 classes
= this.getDayClasses(date
);
6240 classes
.unshift('fc-day-top');
6242 if (this.view
.cellWeekNumbersVisible
) {
6243 // To determine the day of week number change under ISO, we cannot
6244 // rely on moment.js methods such as firstDayOfWeek() or weekday(),
6245 // because they rely on the locale's dow (possibly overridden by
6246 // our firstDay option), which may not be Monday. We cannot change
6247 // dow, because that would affect the calendar start day as well.
6248 if (date
._locale
._fullCalendar_weekCalc
=== 'ISO') {
6249 weekCalcFirstDoW
= 1; // Monday by ISO 8601 definition
6252 weekCalcFirstDoW
= date
._locale
.firstDayOfWeek();
6256 html
+= '<td class="' + classes
.join(' ') + '" data-date="' + date
.format() + '">';
6258 if (this.view
.cellWeekNumbersVisible
&& (date
.day() == weekCalcFirstDoW
)) {
6259 html
+= this.view
.buildGotoAnchorHtml(
6260 { date
: date
, type
: 'week' },
6261 { 'class': 'fc-week-number' },
6262 date
.format('w') // inner HTML
6266 if (this.view
.dayNumbersVisible
) {
6267 html
+= this.view
.buildGotoAnchorHtml(
6269 { 'class': 'fc-day-number' },
6270 date
.date() // inner HTML
6281 ------------------------------------------------------------------------------------------------------------------*/
6284 // Computes a default event time formatting string if `timeFormat` is not explicitly defined
6285 computeEventTimeFormat: function() {
6286 return this.view
.opt('extraSmallTimeFormat'); // like "6p" or "6:30p"
6290 // Computes a default `displayEventEnd` value if one is not expliclty defined
6291 computeDisplayEventEnd: function() {
6292 return this.colCnt
== 1; // we'll likely have space if there's only one day
6297 ------------------------------------------------------------------------------------------------------------------*/
6300 rangeUpdated: function() {
6301 this.updateDayTable();
6305 // Slices up the given span (unzoned start/end with other misc data) into an array of segments
6306 spanToSegs: function(span
) {
6307 var segs
= this.sliceRangeByRow(span
);
6310 for (i
= 0; i
< segs
.length
; i
++) {
6313 seg
.leftCol
= this.daysPerRow
- 1 - seg
.lastRowDayIndex
;
6314 seg
.rightCol
= this.daysPerRow
- 1 - seg
.firstRowDayIndex
;
6317 seg
.leftCol
= seg
.firstRowDayIndex
;
6318 seg
.rightCol
= seg
.lastRowDayIndex
;
6327 ------------------------------------------------------------------------------------------------------------------*/
6330 prepareHits: function() {
6331 this.colCoordCache
.build();
6332 this.rowCoordCache
.build();
6333 this.rowCoordCache
.bottoms
[this.rowCnt
- 1] += this.bottomCoordPadding
; // hack
6337 releaseHits: function() {
6338 this.colCoordCache
.clear();
6339 this.rowCoordCache
.clear();
6343 queryHit: function(leftOffset
, topOffset
) {
6344 if (this.colCoordCache
.isLeftInBounds(leftOffset
) && this.rowCoordCache
.isTopInBounds(topOffset
)) {
6345 var col
= this.colCoordCache
.getHorizontalIndex(leftOffset
);
6346 var row
= this.rowCoordCache
.getVerticalIndex(topOffset
);
6348 if (row
!= null && col
!= null) {
6349 return this.getCellHit(row
, col
);
6355 getHitSpan: function(hit
) {
6356 return this.getCellRange(hit
.row
, hit
.col
);
6360 getHitEl: function(hit
) {
6361 return this.getCellEl(hit
.row
, hit
.col
);
6366 ------------------------------------------------------------------------------------------------------------------*/
6367 // FYI: the first column is the leftmost column, regardless of date
6370 getCellHit: function(row
, col
) {
6374 component
: this, // needed unfortunately :(
6375 left
: this.colCoordCache
.getLeftOffset(col
),
6376 right
: this.colCoordCache
.getRightOffset(col
),
6377 top
: this.rowCoordCache
.getTopOffset(row
),
6378 bottom
: this.rowCoordCache
.getBottomOffset(row
)
6383 getCellEl: function(row
, col
) {
6384 return this.cellEls
.eq(row
* this.colCnt
+ col
);
6388 /* Event Drag Visualization
6389 ------------------------------------------------------------------------------------------------------------------*/
6390 // TODO: move to DayGrid.event, similar to what we did with Grid's drag methods
6393 // Renders a visual indication of an event or external element being dragged.
6394 // `eventLocation` has zoned start and end (optional)
6395 renderDrag: function(eventLocation
, seg
) {
6397 // always render a highlight underneath
6398 this.renderHighlight(this.eventToSpan(eventLocation
));
6400 // if a segment from the same calendar but another component is being dragged, render a helper event
6401 if (seg
&& seg
.component
!== this) {
6402 return this.renderEventLocationHelper(eventLocation
, seg
); // returns mock event elements
6407 // Unrenders any visual indication of a hovering event
6408 unrenderDrag: function() {
6409 this.unrenderHighlight();
6410 this.unrenderHelper();
6414 /* Event Resize Visualization
6415 ------------------------------------------------------------------------------------------------------------------*/
6418 // Renders a visual indication of an event being resized
6419 renderEventResize: function(eventLocation
, seg
) {
6420 this.renderHighlight(this.eventToSpan(eventLocation
));
6421 return this.renderEventLocationHelper(eventLocation
, seg
); // returns mock event elements
6425 // Unrenders a visual indication of an event being resized
6426 unrenderEventResize: function() {
6427 this.unrenderHighlight();
6428 this.unrenderHelper();
6433 ------------------------------------------------------------------------------------------------------------------*/
6436 // Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null.
6437 renderHelper: function(event
, sourceSeg
) {
6438 var helperNodes
= [];
6439 var segs
= this.eventToSegs(event
);
6442 segs
= this.renderFgSegEls(segs
); // assigns each seg's el and returns a subset of segs that were rendered
6443 rowStructs
= this.renderSegRows(segs
);
6445 // inject each new event skeleton into each associated row
6446 this.rowEls
.each(function(row
, rowNode
) {
6447 var rowEl
= $(rowNode
); // the .fc-row
6448 var skeletonEl
= $('<div class="fc-helper-skeleton"><table/></div>'); // will be absolutely positioned
6451 // If there is an original segment, match the top position. Otherwise, put it at the row's top level
6452 if (sourceSeg
&& sourceSeg
.row
=== row
) {
6453 skeletonTop
= sourceSeg
.el
.position().top
;
6456 skeletonTop
= rowEl
.find('.fc-content-skeleton tbody').position().top
;
6459 skeletonEl
.css('top', skeletonTop
)
6461 .append(rowStructs
[row
].tbodyEl
);
6463 rowEl
.append(skeletonEl
);
6464 helperNodes
.push(skeletonEl
[0]);
6467 return ( // must return the elements rendered
6468 this.helperEls
= $(helperNodes
) // array -> jQuery set
6473 // Unrenders any visual indication of a mock helper event
6474 unrenderHelper: function() {
6475 if (this.helperEls
) {
6476 this.helperEls
.remove();
6477 this.helperEls
= null;
6482 /* Fill System (highlight, background events, business hours)
6483 ------------------------------------------------------------------------------------------------------------------*/
6486 fillSegTag
: 'td', // override the default tag name
6489 // Renders a set of rectangles over the given segments of days.
6490 // Only returns segments that successfully rendered.
6491 renderFill: function(type
, segs
, className
) {
6496 segs
= this.renderFillSegEls(type
, segs
); // assignes `.el` to each seg. returns successfully rendered segs
6498 for (i
= 0; i
< segs
.length
; i
++) {
6500 skeletonEl
= this.renderFillRow(type
, seg
, className
);
6501 this.rowEls
.eq(seg
.row
).append(skeletonEl
);
6502 nodes
.push(skeletonEl
[0]);
6505 this.elsByFill
[type
] = $(nodes
);
6511 // Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered.
6512 renderFillRow: function(type
, seg
, className
) {
6513 var colCnt
= this.colCnt
;
6514 var startCol
= seg
.leftCol
;
6515 var endCol
= seg
.rightCol
+ 1;
6519 className
= className
|| type
.toLowerCase();
6522 '<div class="fc-' + className
+ '-skeleton">' +
6523 '<table><tr/></table>' +
6526 trEl
= skeletonEl
.find('tr');
6529 trEl
.append('<td colspan="' + startCol
+ '"/>');
6533 seg
.el
.attr('colspan', endCol
- startCol
)
6536 if (endCol
< colCnt
) {
6537 trEl
.append('<td colspan="' + (colCnt
- endCol
) + '"/>');
6540 this.bookendCells(trEl
);
6549 /* Event-rendering methods for the DayGrid class
6550 ----------------------------------------------------------------------------------------------------------------------*/
6554 rowStructs
: null, // an array of objects, each holding information about a row's foreground event-rendering
6557 // Unrenders all events currently rendered on the grid
6558 unrenderEvents: function() {
6559 this.removeSegPopover(); // removes the "more.." events popover
6560 Grid
.prototype.unrenderEvents
.apply(this, arguments
); // calls the super-method
6564 // Retrieves all rendered segment objects currently rendered on the grid
6565 getEventSegs: function() {
6566 return Grid
.prototype.getEventSegs
.call(this) // get the segments from the super-method
6567 .concat(this.popoverSegs
|| []); // append the segments from the "more..." popover
6571 // Renders the given background event segments onto the grid
6572 renderBgSegs: function(segs
) {
6574 // don't render timed background events
6575 var allDaySegs
= $.grep(segs
, function(seg
) {
6576 return seg
.event
.allDay
;
6579 return Grid
.prototype.renderBgSegs
.call(this, allDaySegs
); // call the super-method
6583 // Renders the given foreground event segments onto the grid
6584 renderFgSegs: function(segs
) {
6587 // render an `.el` on each seg
6588 // returns a subset of the segs. segs that were actually rendered
6589 segs
= this.renderFgSegEls(segs
);
6591 rowStructs
= this.rowStructs
= this.renderSegRows(segs
);
6593 // append to each row's content skeleton
6594 this.rowEls
.each(function(i
, rowNode
) {
6595 $(rowNode
).find('.fc-content-skeleton > table').append(
6596 rowStructs
[i
].tbodyEl
6600 return segs
; // return only the segs that were actually rendered
6604 // Unrenders all currently rendered foreground event segments
6605 unrenderFgSegs: function() {
6606 var rowStructs
= this.rowStructs
|| [];
6609 while ((rowStruct
= rowStructs
.pop())) {
6610 rowStruct
.tbodyEl
.remove();
6613 this.rowStructs
= null;
6617 // Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton.
6618 // Returns an array of rowStruct objects (see the bottom of `renderSegRow`).
6619 // PRECONDITION: each segment shoud already have a rendered and assigned `.el`
6620 renderSegRows: function(segs
) {
6621 var rowStructs
= [];
6625 segRows
= this.groupSegRows(segs
); // group into nested arrays
6627 // iterate each row of segment groupings
6628 for (row
= 0; row
< segRows
.length
; row
++) {
6630 this.renderSegRow(row
, segRows
[row
])
6638 // Builds the HTML to be used for the default element for an individual segment
6639 fgSegHtml: function(seg
, disableResizing
) {
6640 var view
= this.view
;
6641 var event
= seg
.event
;
6642 var isDraggable
= view
.isEventDraggable(event
);
6643 var isResizableFromStart
= !disableResizing
&& event
.allDay
&&
6644 seg
.isStart
&& view
.isEventResizableFromStart(event
);
6645 var isResizableFromEnd
= !disableResizing
&& event
.allDay
&&
6646 seg
.isEnd
&& view
.isEventResizableFromEnd(event
);
6647 var classes
= this.getSegClasses(seg
, isDraggable
, isResizableFromStart
|| isResizableFromEnd
);
6648 var skinCss
= cssToStr(this.getSegSkinCss(seg
));
6653 classes
.unshift('fc-day-grid-event', 'fc-h-event');
6655 // Only display a timed events time if it is the starting segment
6657 timeText
= this.getEventTimeText(event
);
6659 timeHtml
= '<span class="fc-time">' + htmlEscape(timeText
) + '</span>';
6664 '<span class="fc-title">' +
6665 (htmlEscape(event
.title
|| '') || ' ') + // we always want one line of height
6668 return '<a class="' + classes
.join(' ') + '"' +
6670 ' href="' + htmlEscape(event
.url
) + '"' :
6674 ' style="' + skinCss
+ '"' :
6678 '<div class="fc-content">' +
6680 titleHtml
+ ' ' + timeHtml
: // put a natural space in between
6681 timeHtml
+ ' ' + titleHtml
//
6684 (isResizableFromStart
?
6685 '<div class="fc-resizer fc-start-resizer" />' :
6688 (isResizableFromEnd
?
6689 '<div class="fc-resizer fc-end-resizer" />' :
6696 // Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains
6697 // the segments. Returns object with a bunch of internal data about how the render was calculated.
6698 // NOTE: modifies rowSegs
6699 renderSegRow: function(row
, rowSegs
) {
6700 var colCnt
= this.colCnt
;
6701 var segLevels
= this.buildSegLevels(rowSegs
); // group into sub-arrays of levels
6702 var levelCnt
= Math
.max(1, segLevels
.length
); // ensure at least one level
6703 var tbody
= $('<tbody/>');
6704 var segMatrix
= []; // lookup for which segments are rendered into which level+col cells
6705 var cellMatrix
= []; // lookup for all <td> elements of the level+col matrix
6706 var loneCellMatrix
= []; // lookup for <td> elements that only take up a single column
6713 // populates empty cells from the current column (`col`) to `endCol`
6714 function emptyCellsUntil(endCol
) {
6715 while (col
< endCol
) {
6716 // try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell
6717 td
= (loneCellMatrix
[i
- 1] || [])[col
];
6721 parseInt(td
.attr('rowspan') || 1, 10) + 1
6728 cellMatrix
[i
][col
] = td
;
6729 loneCellMatrix
[i
][col
] = td
;
6734 for (i
= 0; i
< levelCnt
; i
++) { // iterate through all levels
6735 levelSegs
= segLevels
[i
];
6740 cellMatrix
.push([]);
6741 loneCellMatrix
.push([]);
6743 // levelCnt might be 1 even though there are no actual levels. protect against this.
6744 // this single empty row is useful for styling.
6746 for (j
= 0; j
< levelSegs
.length
; j
++) { // iterate through segments in level
6749 emptyCellsUntil(seg
.leftCol
);
6751 // create a container that occupies or more columns. append the event element.
6752 td
= $('<td class="fc-event-container"/>').append(seg
.el
);
6753 if (seg
.leftCol
!= seg
.rightCol
) {
6754 td
.attr('colspan', seg
.rightCol
- seg
.leftCol
+ 1);
6756 else { // a single-column segment
6757 loneCellMatrix
[i
][col
] = td
;
6760 while (col
<= seg
.rightCol
) {
6761 cellMatrix
[i
][col
] = td
;
6762 segMatrix
[i
][col
] = seg
;
6770 emptyCellsUntil(colCnt
); // finish off the row
6771 this.bookendCells(tr
);
6775 return { // a "rowStruct"
6776 row
: row
, // the row number
6778 cellMatrix
: cellMatrix
,
6779 segMatrix
: segMatrix
,
6780 segLevels
: segLevels
,
6786 // Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels.
6787 // NOTE: modifies segs
6788 buildSegLevels: function(segs
) {
6793 // Give preference to elements with certain criteria, so they have
6794 // a chance to be closer to the top.
6795 this.sortEventSegs(segs
);
6797 for (i
= 0; i
< segs
.length
; i
++) {
6800 // loop through levels, starting with the topmost, until the segment doesn't collide with other segments
6801 for (j
= 0; j
< levels
.length
; j
++) {
6802 if (!isDaySegCollision(seg
, levels
[j
])) {
6806 // `j` now holds the desired subrow index
6809 // create new level array if needed and append segment
6810 (levels
[j
] || (levels
[j
] = [])).push(seg
);
6813 // order segments left-to-right. very important if calendar is RTL
6814 for (j
= 0; j
< levels
.length
; j
++) {
6815 levels
[j
].sort(compareDaySegCols
);
6822 // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row
6823 groupSegRows: function(segs
) {
6827 for (i
= 0; i
< this.rowCnt
; i
++) {
6831 for (i
= 0; i
< segs
.length
; i
++) {
6832 segRows
[segs
[i
].row
].push(segs
[i
]);
6841 // Computes whether two segments' columns collide. They are assumed to be in the same row.
6842 function isDaySegCollision(seg
, otherSegs
) {
6845 for (i
= 0; i
< otherSegs
.length
; i
++) {
6846 otherSeg
= otherSegs
[i
];
6849 otherSeg
.leftCol
<= seg
.rightCol
&&
6850 otherSeg
.rightCol
>= seg
.leftCol
6860 // A cmp function for determining the leftmost event
6861 function compareDaySegCols(a
, b
) {
6862 return a
.leftCol
- b
.leftCol
;
6867 /* Methods relate to limiting the number events for a given day on a DayGrid
6868 ----------------------------------------------------------------------------------------------------------------------*/
6869 // NOTE: all the segs being passed around in here are foreground segs
6873 segPopover
: null, // the Popover that holds events that can't fit in a cell. null when not visible
6874 popoverSegs
: null, // an array of segment objects that the segPopover holds. null when not visible
6877 removeSegPopover: function() {
6878 if (this.segPopover
) {
6879 this.segPopover
.hide(); // in handler, will call segPopover's removeElement
6884 // Limits the number of "levels" (vertically stacking layers of events) for each row of the grid.
6885 // `levelLimit` can be false (don't limit), a number, or true (should be computed).
6886 limitRows: function(levelLimit
) {
6887 var rowStructs
= this.rowStructs
|| [];
6891 for (row
= 0; row
< rowStructs
.length
; row
++) {
6892 this.unlimitRow(row
);
6895 rowLevelLimit
= false;
6897 else if (typeof levelLimit
=== 'number') {
6898 rowLevelLimit
= levelLimit
;
6901 rowLevelLimit
= this.computeRowLevelLimit(row
);
6904 if (rowLevelLimit
!== false) {
6905 this.limitRow(row
, rowLevelLimit
);
6911 // Computes the number of levels a row will accomodate without going outside its bounds.
6912 // Assumes the row is "rigid" (maintains a constant height regardless of what is inside).
6913 // `row` is the row number.
6914 computeRowLevelLimit: function(row
) {
6915 var rowEl
= this.rowEls
.eq(row
); // the containing "fake" row div
6916 var rowHeight
= rowEl
.height(); // TODO: cache somehow?
6917 var trEls
= this.rowStructs
[row
].tbodyEl
.children();
6921 function iterInnerHeights(i
, childNode
) {
6922 trHeight
= Math
.max(trHeight
, $(childNode
).outerHeight());
6925 // Reveal one level <tr> at a time and stop when we find one out of bounds
6926 for (i
= 0; i
< trEls
.length
; i
++) {
6927 trEl
= trEls
.eq(i
).removeClass('fc-limited'); // reset to original state (reveal)
6929 // with rowspans>1 and IE8, trEl.outerHeight() would return the height of the largest cell,
6930 // so instead, find the tallest inner content element.
6932 trEl
.find('> td > :first-child').each(iterInnerHeights
);
6934 if (trEl
.position().top
+ trHeight
> rowHeight
) {
6939 return false; // should not limit at all
6943 // Limits the given grid row to the maximum number of levels and injects "more" links if necessary.
6944 // `row` is the row number.
6945 // `levelLimit` is a number for the maximum (inclusive) number of levels allowed.
6946 limitRow: function(row
, levelLimit
) {
6948 var rowStruct
= this.rowStructs
[row
];
6949 var moreNodes
= []; // array of "more" <a> links and <td> DOM nodes
6950 var col
= 0; // col #, left-to-right (not chronologically)
6951 var levelSegs
; // array of segment objects in the last allowable level, ordered left-to-right
6952 var cellMatrix
; // a matrix (by level, then column) of all <td> jQuery elements in the row
6953 var limitedNodes
; // array of temporarily hidden level <tr> and segment <td> DOM nodes
6955 var segsBelow
; // array of segment objects below `seg` in the current `col`
6956 var totalSegsBelow
; // total number of segments below `seg` in any of the columns `seg` occupies
6957 var colSegsBelow
; // array of segment arrays, below seg, one for each column (offset from segs's first column)
6959 var segMoreNodes
; // array of "more" <td> cells that will stand-in for the current seg's cell
6961 var moreTd
, moreWrap
, moreLink
;
6963 // Iterates through empty level cells and places "more" links inside if need be
6964 function emptyCellsUntil(endCol
) { // goes from current `col` to `endCol`
6965 while (col
< endCol
) {
6966 segsBelow
= _this
.getCellSegs(row
, col
, levelLimit
);
6967 if (segsBelow
.length
) {
6968 td
= cellMatrix
[levelLimit
- 1][col
];
6969 moreLink
= _this
.renderMoreLink(row
, col
, segsBelow
);
6970 moreWrap
= $('<div/>').append(moreLink
);
6971 td
.append(moreWrap
);
6972 moreNodes
.push(moreWrap
[0]);
6978 if (levelLimit
&& levelLimit
< rowStruct
.segLevels
.length
) { // is it actually over the limit?
6979 levelSegs
= rowStruct
.segLevels
[levelLimit
- 1];
6980 cellMatrix
= rowStruct
.cellMatrix
;
6982 limitedNodes
= rowStruct
.tbodyEl
.children().slice(levelLimit
) // get level <tr> elements past the limit
6983 .addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array
6985 // iterate though segments in the last allowable level
6986 for (i
= 0; i
< levelSegs
.length
; i
++) {
6988 emptyCellsUntil(seg
.leftCol
); // process empty cells before the segment
6990 // determine *all* segments below `seg` that occupy the same columns
6993 while (col
<= seg
.rightCol
) {
6994 segsBelow
= this.getCellSegs(row
, col
, levelLimit
);
6995 colSegsBelow
.push(segsBelow
);
6996 totalSegsBelow
+= segsBelow
.length
;
7000 if (totalSegsBelow
) { // do we need to replace this segment with one or many "more" links?
7001 td
= cellMatrix
[levelLimit
- 1][seg
.leftCol
]; // the segment's parent cell
7002 rowspan
= td
.attr('rowspan') || 1;
7005 // make a replacement <td> for each column the segment occupies. will be one for each colspan
7006 for (j
= 0; j
< colSegsBelow
.length
; j
++) {
7007 moreTd
= $('<td class="fc-more-cell"/>').attr('rowspan', rowspan
);
7008 segsBelow
= colSegsBelow
[j
];
7009 moreLink
= this.renderMoreLink(
7012 [ seg
].concat(segsBelow
) // count seg as hidden too
7014 moreWrap
= $('<div/>').append(moreLink
);
7015 moreTd
.append(moreWrap
);
7016 segMoreNodes
.push(moreTd
[0]);
7017 moreNodes
.push(moreTd
[0]);
7020 td
.addClass('fc-limited').after($(segMoreNodes
)); // hide original <td> and inject replacements
7021 limitedNodes
.push(td
[0]);
7025 emptyCellsUntil(this.colCnt
); // finish off the level
7026 rowStruct
.moreEls
= $(moreNodes
); // for easy undoing later
7027 rowStruct
.limitedEls
= $(limitedNodes
); // for easy undoing later
7032 // Reveals all levels and removes all "more"-related elements for a grid's row.
7033 // `row` is a row number.
7034 unlimitRow: function(row
) {
7035 var rowStruct
= this.rowStructs
[row
];
7037 if (rowStruct
.moreEls
) {
7038 rowStruct
.moreEls
.remove();
7039 rowStruct
.moreEls
= null;
7042 if (rowStruct
.limitedEls
) {
7043 rowStruct
.limitedEls
.removeClass('fc-limited');
7044 rowStruct
.limitedEls
= null;
7049 // Renders an <a> element that represents hidden event element for a cell.
7050 // Responsible for attaching click handler as well.
7051 renderMoreLink: function(row
, col
, hiddenSegs
) {
7053 var view
= this.view
;
7055 return $('<a class="fc-more"/>')
7057 this.getMoreLinkText(hiddenSegs
.length
)
7059 .on('click', function(ev
) {
7060 var clickOption
= view
.opt('eventLimitClick');
7061 var date
= _this
.getCellDate(row
, col
);
7062 var moreEl
= $(this);
7063 var dayEl
= _this
.getCellEl(row
, col
);
7064 var allSegs
= _this
.getCellSegs(row
, col
);
7066 // rescope the segments to be within the cell's date
7067 var reslicedAllSegs
= _this
.resliceDaySegs(allSegs
, date
);
7068 var reslicedHiddenSegs
= _this
.resliceDaySegs(hiddenSegs
, date
);
7070 if (typeof clickOption
=== 'function') {
7071 // the returned value can be an atomic option
7072 clickOption
= view
.publiclyTrigger('eventLimitClick', null, {
7076 segs
: reslicedAllSegs
,
7077 hiddenSegs
: reslicedHiddenSegs
7081 if (clickOption
=== 'popover') {
7082 _this
.showSegPopover(row
, col
, moreEl
, reslicedAllSegs
);
7084 else if (typeof clickOption
=== 'string') { // a view name
7085 view
.calendar
.zoomTo(date
, clickOption
);
7091 // Reveals the popover that displays all events within a cell
7092 showSegPopover: function(row
, col
, moreLink
, segs
) {
7094 var view
= this.view
;
7095 var moreWrap
= moreLink
.parent(); // the <div> wrapper around the <a>
7096 var topEl
; // the element we want to match the top coordinate of
7099 if (this.rowCnt
== 1) {
7100 topEl
= view
.el
; // will cause the popover to cover any sort of header
7103 topEl
= this.rowEls
.eq(row
); // will align with top of row
7107 className
: 'fc-more-popover',
7108 content
: this.renderSegPopoverContent(row
, col
, segs
),
7109 parentEl
: this.view
.el
, // attach to root of view. guarantees outside of scrollbars.
7110 top
: topEl
.offset().top
,
7111 autoHide
: true, // when the user clicks elsewhere, hide the popover
7112 viewportConstrain
: view
.opt('popoverViewportConstrain'),
7114 // kill everything when the popover is hidden
7115 // notify events to be removed
7116 if (_this
.popoverSegs
) {
7118 for (var i
= 0; i
< _this
.popoverSegs
.length
; ++i
) {
7119 seg
= _this
.popoverSegs
[i
];
7120 view
.publiclyTrigger('eventDestroy', seg
.event
, seg
.event
, seg
.el
);
7123 _this
.segPopover
.removeElement();
7124 _this
.segPopover
= null;
7125 _this
.popoverSegs
= null;
7129 // Determine horizontal coordinate.
7130 // We use the moreWrap instead of the <td> to avoid border confusion.
7132 options
.right
= moreWrap
.offset().left
+ moreWrap
.outerWidth() + 1; // +1 to be over cell border
7135 options
.left
= moreWrap
.offset().left
- 1; // -1 to be over cell border
7138 this.segPopover
= new Popover(options
);
7139 this.segPopover
.show();
7141 // the popover doesn't live within the grid's container element, and thus won't get the event
7142 // delegated-handlers for free. attach event-related handlers to the popover.
7143 this.bindSegHandlersToEl(this.segPopover
.el
);
7147 // Builds the inner DOM contents of the segment popover
7148 renderSegPopoverContent: function(row
, col
, segs
) {
7149 var view
= this.view
;
7150 var isTheme
= view
.opt('theme');
7151 var title
= this.getCellDate(row
, col
).format(view
.opt('dayPopoverFormat'));
7153 '<div class="fc-header ' + view
.widgetHeaderClass
+ '">' +
7154 '<span class="fc-close ' +
7155 (isTheme
? 'ui-icon ui-icon-closethick' : 'fc-icon fc-icon-x') +
7157 '<span class="fc-title">' +
7160 '<div class="fc-clear"/>' +
7162 '<div class="fc-body ' + view
.widgetContentClass
+ '">' +
7163 '<div class="fc-event-container"></div>' +
7166 var segContainer
= content
.find('.fc-event-container');
7169 // render each seg's `el` and only return the visible segs
7170 segs
= this.renderFgSegEls(segs
, true); // disableResizing=true
7171 this.popoverSegs
= segs
;
7173 for (i
= 0; i
< segs
.length
; i
++) {
7175 // because segments in the popover are not part of a grid coordinate system, provide a hint to any
7176 // grids that want to do drag-n-drop about which cell it came from
7178 segs
[i
].hit
= this.getCellHit(row
, col
);
7179 this.hitsNotNeeded();
7181 segContainer
.append(segs
[i
].el
);
7188 // Given the events within an array of segment objects, reslice them to be in a single day
7189 resliceDaySegs: function(segs
, dayDate
) {
7191 // build an array of the original events
7192 var events
= $.map(segs
, function(seg
) {
7196 var dayStart
= dayDate
.clone();
7197 var dayEnd
= dayStart
.clone().add(1, 'days');
7198 var dayRange
= { start
: dayStart
, end
: dayEnd
};
7200 // slice the events with a custom slicing function
7201 segs
= this.eventsToSegs(
7204 var seg
= intersectRanges(range
, dayRange
); // undefind if no intersection
7205 return seg
? [ seg
] : []; // must return an array of segments
7209 // force an order because eventsToSegs doesn't guarantee one
7210 this.sortEventSegs(segs
);
7216 // Generates the text that should be inside a "more" link, given the number of events it represents
7217 getMoreLinkText: function(num
) {
7218 var opt
= this.view
.opt('eventLimitText');
7220 if (typeof opt
=== 'function') {
7224 return '+' + num
+ ' ' + opt
;
7229 // Returns segments within a given cell.
7230 // If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs.
7231 getCellSegs: function(row
, col
, startLevel
) {
7232 var segMatrix
= this.rowStructs
[row
].segMatrix
;
7233 var level
= startLevel
|| 0;
7237 while (level
< segMatrix
.length
) {
7238 seg
= segMatrix
[level
][col
];
7252 /* A component that renders one or more columns of vertical time slots
7253 ----------------------------------------------------------------------------------------------------------------------*/
7254 // We mixin DayTable, even though there is only a single row of days
7256 var TimeGrid
= FC
.TimeGrid
= Grid
.extend(DayTableMixin
, {
7258 slotDuration
: null, // duration of a "slot", a distinct time segment on given day, visualized by lines
7259 snapDuration
: null, // granularity of time for dragging and selecting
7261 minTime
: null, // Duration object that denotes the first visible time of any given day
7262 maxTime
: null, // Duration object that denotes the exclusive visible end time of any given day
7263 labelFormat
: null, // formatting string for times running along vertical axis
7264 labelInterval
: null, // duration of how often a label should be displayed for a slot
7266 colEls
: null, // cells elements in the day-row background
7267 slatContainerEl
: null, // div that wraps all the slat rows
7268 slatEls
: null, // elements running horizontally across all columns
7269 nowIndicatorEls
: null,
7271 colCoordCache
: null,
7272 slatCoordCache
: null,
7275 constructor: function() {
7276 Grid
.apply(this, arguments
); // call the super-constructor
7278 this.processOptions();
7282 // Renders the time grid into `this.el`, which should already be assigned.
7283 // Relies on the view's colCnt. In the future, this component should probably be self-sufficient.
7284 renderDates: function() {
7285 this.el
.html(this.renderHtml());
7286 this.colEls
= this.el
.find('.fc-day');
7287 this.slatContainerEl
= this.el
.find('.fc-slats');
7288 this.slatEls
= this.slatContainerEl
.find('tr');
7290 this.colCoordCache
= new CoordCache({
7294 this.slatCoordCache
= new CoordCache({
7299 this.renderContentSkeleton();
7303 // Renders the basic HTML skeleton for the grid
7304 renderHtml: function() {
7306 '<div class="fc-bg">' +
7308 this.renderBgTrHtml(0) + // row=0
7311 '<div class="fc-slats">' +
7313 this.renderSlatRowHtml() +
7319 // Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL.
7320 renderSlatRowHtml: function() {
7321 var view
= this.view
;
7322 var isRTL
= this.isRTL
;
7324 var slotTime
= moment
.duration(+this.minTime
); // wish there was .clone() for durations
7325 var slotDate
; // will be on the view's first day, but we only care about its time
7329 // Calculate the time for each slot
7330 while (slotTime
< this.maxTime
) {
7331 slotDate
= this.start
.clone().time(slotTime
);
7332 isLabeled
= isInt(divideDurationByDuration(slotTime
, this.labelInterval
));
7335 '<td class="fc-axis fc-time ' + view
.widgetContentClass
+ '" ' + view
.axisStyleAttr() + '>' +
7337 '<span>' + // for matchCellWidths
7338 htmlEscape(slotDate
.format(this.labelFormat
)) +
7345 '<tr data-time="' + slotDate
.format('HH:mm:ss') + '"' +
7346 (isLabeled
? '' : ' class="fc-minor"') +
7348 (!isRTL
? axisHtml
: '') +
7349 '<td class="' + view
.widgetContentClass
+ '"/>' +
7350 (isRTL
? axisHtml
: '') +
7353 slotTime
.add(this.slotDuration
);
7361 ------------------------------------------------------------------------------------------------------------------*/
7364 // Parses various options into properties of this object
7365 processOptions: function() {
7366 var view
= this.view
;
7367 var slotDuration
= view
.opt('slotDuration');
7368 var snapDuration
= view
.opt('snapDuration');
7371 slotDuration
= moment
.duration(slotDuration
);
7372 snapDuration
= snapDuration
? moment
.duration(snapDuration
) : slotDuration
;
7374 this.slotDuration
= slotDuration
;
7375 this.snapDuration
= snapDuration
;
7376 this.snapsPerSlot
= slotDuration
/ snapDuration
; // TODO: ensure an integer multiple?
7378 this.minResizeDuration
= snapDuration
; // hack
7380 this.minTime
= moment
.duration(view
.opt('minTime'));
7381 this.maxTime
= moment
.duration(view
.opt('maxTime'));
7383 // might be an array value (for TimelineView).
7384 // if so, getting the most granular entry (the last one probably).
7385 input
= view
.opt('slotLabelFormat');
7386 if ($.isArray(input
)) {
7387 input
= input
[input
.length
- 1];
7392 view
.opt('smallTimeFormat'); // the computed default
7394 input
= view
.opt('slotLabelInterval');
7395 this.labelInterval
= input
?
7396 moment
.duration(input
) :
7397 this.computeLabelInterval(slotDuration
);
7401 // Computes an automatic value for slotLabelInterval
7402 computeLabelInterval: function(slotDuration
) {
7407 // find the smallest stock label interval that results in more than one slots-per-label
7408 for (i
= AGENDA_STOCK_SUB_DURATIONS
.length
- 1; i
>= 0; i
--) {
7409 labelInterval
= moment
.duration(AGENDA_STOCK_SUB_DURATIONS
[i
]);
7410 slotsPerLabel
= divideDurationByDuration(labelInterval
, slotDuration
);
7411 if (isInt(slotsPerLabel
) && slotsPerLabel
> 1) {
7412 return labelInterval
;
7416 return moment
.duration(slotDuration
); // fall back. clone
7420 // Computes a default event time formatting string if `timeFormat` is not explicitly defined
7421 computeEventTimeFormat: function() {
7422 return this.view
.opt('noMeridiemTimeFormat'); // like "6:30" (no AM/PM)
7426 // Computes a default `displayEventEnd` value if one is not expliclty defined
7427 computeDisplayEventEnd: function() {
7433 ------------------------------------------------------------------------------------------------------------------*/
7436 prepareHits: function() {
7437 this.colCoordCache
.build();
7438 this.slatCoordCache
.build();
7442 releaseHits: function() {
7443 this.colCoordCache
.clear();
7444 // NOTE: don't clear slatCoordCache because we rely on it for computeTimeTop
7448 queryHit: function(leftOffset
, topOffset
) {
7449 var snapsPerSlot
= this.snapsPerSlot
;
7450 var colCoordCache
= this.colCoordCache
;
7451 var slatCoordCache
= this.slatCoordCache
;
7453 if (colCoordCache
.isLeftInBounds(leftOffset
) && slatCoordCache
.isTopInBounds(topOffset
)) {
7454 var colIndex
= colCoordCache
.getHorizontalIndex(leftOffset
);
7455 var slatIndex
= slatCoordCache
.getVerticalIndex(topOffset
);
7457 if (colIndex
!= null && slatIndex
!= null) {
7458 var slatTop
= slatCoordCache
.getTopOffset(slatIndex
);
7459 var slatHeight
= slatCoordCache
.getHeight(slatIndex
);
7460 var partial
= (topOffset
- slatTop
) / slatHeight
; // floating point number between 0 and 1
7461 var localSnapIndex
= Math
.floor(partial
* snapsPerSlot
); // the snap # relative to start of slat
7462 var snapIndex
= slatIndex
* snapsPerSlot
+ localSnapIndex
;
7463 var snapTop
= slatTop
+ (localSnapIndex
/ snapsPerSlot
) * slatHeight
;
7464 var snapBottom
= slatTop
+ ((localSnapIndex
+ 1) / snapsPerSlot
) * slatHeight
;
7469 component
: this, // needed unfortunately :(
7470 left
: colCoordCache
.getLeftOffset(colIndex
),
7471 right
: colCoordCache
.getRightOffset(colIndex
),
7480 getHitSpan: function(hit
) {
7481 var start
= this.getCellDate(0, hit
.col
); // row=0
7482 var time
= this.computeSnapTime(hit
.snap
); // pass in the snap-index
7486 end
= start
.clone().add(this.snapDuration
);
7488 return { start
: start
, end
: end
};
7492 getHitEl: function(hit
) {
7493 return this.colEls
.eq(hit
.col
);
7498 ------------------------------------------------------------------------------------------------------------------*/
7501 rangeUpdated: function() {
7502 this.updateDayTable();
7506 // Given a row number of the grid, representing a "snap", returns a time (Duration) from its start-of-day
7507 computeSnapTime: function(snapIndex
) {
7508 return moment
.duration(this.minTime
+ this.snapDuration
* snapIndex
);
7512 // Slices up the given span (unzoned start/end with other misc data) into an array of segments
7513 spanToSegs: function(span
) {
7514 var segs
= this.sliceRangeByTimes(span
);
7517 for (i
= 0; i
< segs
.length
; i
++) {
7519 segs
[i
].col
= this.daysPerRow
- 1 - segs
[i
].dayIndex
;
7522 segs
[i
].col
= segs
[i
].dayIndex
;
7530 sliceRangeByTimes: function(range
) {
7537 for (dayIndex
= 0; dayIndex
< this.daysPerRow
; dayIndex
++) {
7538 dayDate
= this.dayDates
[dayIndex
].clone(); // TODO: better API for this?
7540 start
: dayDate
.clone().time(this.minTime
),
7541 end
: dayDate
.clone().time(this.maxTime
)
7543 seg
= intersectRanges(range
, dayRange
); // both will be ambig timezone
7545 seg
.dayIndex
= dayIndex
;
7555 ------------------------------------------------------------------------------------------------------------------*/
7558 updateSize: function(isResize
) { // NOT a standard Grid method
7559 this.slatCoordCache
.build();
7562 this.updateSegVerticals(
7563 [].concat(this.fgSegs
|| [], this.bgSegs
|| [], this.businessSegs
|| [])
7569 getTotalSlatHeight: function() {
7570 return this.slatContainerEl
.outerHeight();
7574 // Computes the top coordinate, relative to the bounds of the grid, of the given date.
7575 // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.
7576 computeDateTop: function(date
, startOfDayDate
) {
7577 return this.computeTimeTop(
7579 date
- startOfDayDate
.clone().stripTime()
7585 // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration).
7586 computeTimeTop: function(time
) {
7587 var len
= this.slatEls
.length
;
7588 var slatCoverage
= (time
- this.minTime
) / this.slotDuration
; // floating-point value of # of slots covered
7592 // compute a floating-point number for how many slats should be progressed through.
7593 // from 0 to number of slats (inclusive)
7594 // constrained because minTime/maxTime might be customized.
7595 slatCoverage
= Math
.max(0, slatCoverage
);
7596 slatCoverage
= Math
.min(len
, slatCoverage
);
7598 // an integer index of the furthest whole slat
7599 // from 0 to number slats (*exclusive*, so len-1)
7600 slatIndex
= Math
.floor(slatCoverage
);
7601 slatIndex
= Math
.min(slatIndex
, len
- 1);
7603 // how much further through the slatIndex slat (from 0.0-1.0) must be covered in addition.
7604 // could be 1.0 if slatCoverage is covering *all* the slots
7605 slatRemainder
= slatCoverage
- slatIndex
;
7607 return this.slatCoordCache
.getTopPosition(slatIndex
) +
7608 this.slatCoordCache
.getHeight(slatIndex
) * slatRemainder
;
7613 /* Event Drag Visualization
7614 ------------------------------------------------------------------------------------------------------------------*/
7617 // Renders a visual indication of an event being dragged over the specified date(s).
7618 // A returned value of `true` signals that a mock "helper" event has been rendered.
7619 renderDrag: function(eventLocation
, seg
) {
7621 if (seg
) { // if there is event information for this drag, render a helper event
7623 // returns mock event elements
7624 // signal that a helper has been rendered
7625 return this.renderEventLocationHelper(eventLocation
, seg
);
7628 // otherwise, just render a highlight
7629 this.renderHighlight(this.eventToSpan(eventLocation
));
7634 // Unrenders any visual indication of an event being dragged
7635 unrenderDrag: function() {
7636 this.unrenderHelper();
7637 this.unrenderHighlight();
7641 /* Event Resize Visualization
7642 ------------------------------------------------------------------------------------------------------------------*/
7645 // Renders a visual indication of an event being resized
7646 renderEventResize: function(eventLocation
, seg
) {
7647 return this.renderEventLocationHelper(eventLocation
, seg
); // returns mock event elements
7651 // Unrenders any visual indication of an event being resized
7652 unrenderEventResize: function() {
7653 this.unrenderHelper();
7658 ------------------------------------------------------------------------------------------------------------------*/
7661 // Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag)
7662 renderHelper: function(event
, sourceSeg
) {
7663 return this.renderHelperSegs(this.eventToSegs(event
), sourceSeg
); // returns mock event elements
7667 // Unrenders any mock helper event
7668 unrenderHelper: function() {
7669 this.unrenderHelperSegs();
7674 ------------------------------------------------------------------------------------------------------------------*/
7677 renderBusinessHours: function() {
7678 this.renderBusinessSegs(
7679 this.buildBusinessHourSegs()
7684 unrenderBusinessHours: function() {
7685 this.unrenderBusinessSegs();
7690 ------------------------------------------------------------------------------------------------------------------*/
7693 getNowIndicatorUnit: function() {
7694 return 'minute'; // will refresh on the minute
7698 renderNowIndicator: function(date
) {
7699 // seg system might be overkill, but it handles scenario where line needs to be rendered
7700 // more than once because of columns with the same date (resources columns for example)
7701 var segs
= this.spanToSegs({ start
: date
, end
: date
});
7702 var top
= this.computeDateTop(date
, date
);
7706 // render lines within the columns
7707 for (i
= 0; i
< segs
.length
; i
++) {
7708 nodes
.push($('<div class="fc-now-indicator fc-now-indicator-line"></div>')
7710 .appendTo(this.colContainerEls
.eq(segs
[i
].col
))[0]);
7713 // render an arrow over the axis
7714 if (segs
.length
> 0) { // is the current time in view?
7715 nodes
.push($('<div class="fc-now-indicator fc-now-indicator-arrow"></div>')
7717 .appendTo(this.el
.find('.fc-content-skeleton'))[0]);
7720 this.nowIndicatorEls
= $(nodes
);
7724 unrenderNowIndicator: function() {
7725 if (this.nowIndicatorEls
) {
7726 this.nowIndicatorEls
.remove();
7727 this.nowIndicatorEls
= null;
7733 ------------------------------------------------------------------------------------------------------------------*/
7736 // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight.
7737 renderSelection: function(span
) {
7738 if (this.view
.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered
7740 // normally acceps an eventLocation, span has a start/end, which is good enough
7741 this.renderEventLocationHelper(span
);
7744 this.renderHighlight(span
);
7749 // Unrenders any visual indication of a selection
7750 unrenderSelection: function() {
7751 this.unrenderHelper();
7752 this.unrenderHighlight();
7757 ------------------------------------------------------------------------------------------------------------------*/
7760 renderHighlight: function(span
) {
7761 this.renderHighlightSegs(this.spanToSegs(span
));
7765 unrenderHighlight: function() {
7766 this.unrenderHighlightSegs();
7773 /* Methods for rendering SEGMENTS, pieces of content that live on the view
7774 ( this file is no longer just for events )
7775 ----------------------------------------------------------------------------------------------------------------------*/
7779 colContainerEls
: null, // containers for each column
7781 // inner-containers for each column where different types of segs live
7782 fgContainerEls
: null,
7783 bgContainerEls
: null,
7784 helperContainerEls
: null,
7785 highlightContainerEls
: null,
7786 businessContainerEls
: null,
7788 // arrays of different types of displayed segments
7792 highlightSegs
: null,
7796 // Renders the DOM that the view's content will live in
7797 renderContentSkeleton: function() {
7802 for (i
= 0; i
< this.colCnt
; i
++) {
7805 '<div class="fc-content-col">' +
7806 '<div class="fc-event-container fc-helper-container"></div>' +
7807 '<div class="fc-event-container"></div>' +
7808 '<div class="fc-highlight-container"></div>' +
7809 '<div class="fc-bgevent-container"></div>' +
7810 '<div class="fc-business-container"></div>' +
7816 '<div class="fc-content-skeleton">' +
7818 '<tr>' + cellHtml
+ '</tr>' +
7823 this.colContainerEls
= skeletonEl
.find('.fc-content-col');
7824 this.helperContainerEls
= skeletonEl
.find('.fc-helper-container');
7825 this.fgContainerEls
= skeletonEl
.find('.fc-event-container:not(.fc-helper-container)');
7826 this.bgContainerEls
= skeletonEl
.find('.fc-bgevent-container');
7827 this.highlightContainerEls
= skeletonEl
.find('.fc-highlight-container');
7828 this.businessContainerEls
= skeletonEl
.find('.fc-business-container');
7830 this.bookendCells(skeletonEl
.find('tr')); // TODO: do this on string level
7831 this.el
.append(skeletonEl
);
7835 /* Foreground Events
7836 ------------------------------------------------------------------------------------------------------------------*/
7839 renderFgSegs: function(segs
) {
7840 segs
= this.renderFgSegsIntoContainers(segs
, this.fgContainerEls
);
7842 return segs
; // needed for Grid::renderEvents
7846 unrenderFgSegs: function() {
7847 this.unrenderNamedSegs('fgSegs');
7851 /* Foreground Helper Events
7852 ------------------------------------------------------------------------------------------------------------------*/
7855 renderHelperSegs: function(segs
, sourceSeg
) {
7860 segs
= this.renderFgSegsIntoContainers(segs
, this.helperContainerEls
);
7862 // Try to make the segment that is in the same row as sourceSeg look the same
7863 for (i
= 0; i
< segs
.length
; i
++) {
7865 if (sourceSeg
&& sourceSeg
.col
=== seg
.col
) {
7866 sourceEl
= sourceSeg
.el
;
7868 left
: sourceEl
.css('left'),
7869 right
: sourceEl
.css('right'),
7870 'margin-left': sourceEl
.css('margin-left'),
7871 'margin-right': sourceEl
.css('margin-right')
7874 helperEls
.push(seg
.el
[0]);
7877 this.helperSegs
= segs
;
7879 return $(helperEls
); // must return rendered helpers
7883 unrenderHelperSegs: function() {
7884 this.unrenderNamedSegs('helperSegs');
7888 /* Background Events
7889 ------------------------------------------------------------------------------------------------------------------*/
7892 renderBgSegs: function(segs
) {
7893 segs
= this.renderFillSegEls('bgEvent', segs
); // TODO: old fill system
7894 this.updateSegVerticals(segs
);
7895 this.attachSegsByCol(this.groupSegsByCol(segs
), this.bgContainerEls
);
7897 return segs
; // needed for Grid::renderEvents
7901 unrenderBgSegs: function() {
7902 this.unrenderNamedSegs('bgSegs');
7907 ------------------------------------------------------------------------------------------------------------------*/
7910 renderHighlightSegs: function(segs
) {
7911 segs
= this.renderFillSegEls('highlight', segs
); // TODO: old fill system
7912 this.updateSegVerticals(segs
);
7913 this.attachSegsByCol(this.groupSegsByCol(segs
), this.highlightContainerEls
);
7914 this.highlightSegs
= segs
;
7918 unrenderHighlightSegs: function() {
7919 this.unrenderNamedSegs('highlightSegs');
7924 ------------------------------------------------------------------------------------------------------------------*/
7927 renderBusinessSegs: function(segs
) {
7928 segs
= this.renderFillSegEls('businessHours', segs
); // TODO: old fill system
7929 this.updateSegVerticals(segs
);
7930 this.attachSegsByCol(this.groupSegsByCol(segs
), this.businessContainerEls
);
7931 this.businessSegs
= segs
;
7935 unrenderBusinessSegs: function() {
7936 this.unrenderNamedSegs('businessSegs');
7940 /* Seg Rendering Utils
7941 ------------------------------------------------------------------------------------------------------------------*/
7944 // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col
7945 groupSegsByCol: function(segs
) {
7949 for (i
= 0; i
< this.colCnt
; i
++) {
7953 for (i
= 0; i
< segs
.length
; i
++) {
7954 segsByCol
[segs
[i
].col
].push(segs
[i
]);
7961 // Given segments grouped by column, insert the segments' elements into a parallel array of container
7962 // elements, each living within a column.
7963 attachSegsByCol: function(segsByCol
, containerEls
) {
7968 for (col
= 0; col
< this.colCnt
; col
++) { // iterate each column grouping
7969 segs
= segsByCol
[col
];
7971 for (i
= 0; i
< segs
.length
; i
++) {
7972 containerEls
.eq(col
).append(segs
[i
].el
);
7978 // Given the name of a property of `this` object, assumed to be an array of segments,
7979 // loops through each segment and removes from DOM. Will null-out the property afterwards.
7980 unrenderNamedSegs: function(propName
) {
7981 var segs
= this[propName
];
7985 for (i
= 0; i
< segs
.length
; i
++) {
7986 segs
[i
].el
.remove();
7988 this[propName
] = null;
7994 /* Foreground Event Rendering Utils
7995 ------------------------------------------------------------------------------------------------------------------*/
7998 // Given an array of foreground segments, render a DOM element for each, computes position,
7999 // and attaches to the column inner-container elements.
8000 renderFgSegsIntoContainers: function(segs
, containerEls
) {
8004 segs
= this.renderFgSegEls(segs
); // will call fgSegHtml
8005 segsByCol
= this.groupSegsByCol(segs
);
8007 for (col
= 0; col
< this.colCnt
; col
++) {
8008 this.updateFgSegCoords(segsByCol
[col
]);
8011 this.attachSegsByCol(segsByCol
, containerEls
);
8017 // Renders the HTML for a single event segment's default rendering
8018 fgSegHtml: function(seg
, disableResizing
) {
8019 var view
= this.view
;
8020 var event
= seg
.event
;
8021 var isDraggable
= view
.isEventDraggable(event
);
8022 var isResizableFromStart
= !disableResizing
&& seg
.isStart
&& view
.isEventResizableFromStart(event
);
8023 var isResizableFromEnd
= !disableResizing
&& seg
.isEnd
&& view
.isEventResizableFromEnd(event
);
8024 var classes
= this.getSegClasses(seg
, isDraggable
, isResizableFromStart
|| isResizableFromEnd
);
8025 var skinCss
= cssToStr(this.getSegSkinCss(seg
));
8027 var fullTimeText
; // more verbose time text. for the print stylesheet
8028 var startTimeText
; // just the start time text
8030 classes
.unshift('fc-time-grid-event', 'fc-v-event');
8032 if (view
.isMultiDayEvent(event
)) { // if the event appears to span more than one day...
8033 // Don't display time text on segments that run entirely through a day.
8034 // That would appear as midnight-midnight and would look dumb.
8035 // Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am)
8036 if (seg
.isStart
|| seg
.isEnd
) {
8037 timeText
= this.getEventTimeText(seg
);
8038 fullTimeText
= this.getEventTimeText(seg
, 'LT');
8039 startTimeText
= this.getEventTimeText(seg
, null, false); // displayEnd=false
8042 // Display the normal time text for the *event's* times
8043 timeText
= this.getEventTimeText(event
);
8044 fullTimeText
= this.getEventTimeText(event
, 'LT');
8045 startTimeText
= this.getEventTimeText(event
, null, false); // displayEnd=false
8048 return '<a class="' + classes
.join(' ') + '"' +
8050 ' href="' + htmlEscape(event
.url
) + '"' :
8054 ' style="' + skinCss
+ '"' :
8058 '<div class="fc-content">' +
8060 '<div class="fc-time"' +
8061 ' data-start="' + htmlEscape(startTimeText
) + '"' +
8062 ' data-full="' + htmlEscape(fullTimeText
) + '"' +
8064 '<span>' + htmlEscape(timeText
) + '</span>' +
8069 '<div class="fc-title">' +
8070 htmlEscape(event
.title
) +
8075 '<div class="fc-bg"/>' +
8076 /* TODO: write CSS for this
8077 (isResizableFromStart ?
8078 '<div class="fc-resizer fc-start-resizer" />' :
8082 (isResizableFromEnd
?
8083 '<div class="fc-resizer fc-end-resizer" />' :
8090 /* Seg Position Utils
8091 ------------------------------------------------------------------------------------------------------------------*/
8094 // Refreshes the CSS top/bottom coordinates for each segment element.
8095 // Works when called after initial render, after a window resize/zoom for example.
8096 updateSegVerticals: function(segs
) {
8097 this.computeSegVerticals(segs
);
8098 this.assignSegVerticals(segs
);
8102 // For each segment in an array, computes and assigns its top and bottom properties
8103 computeSegVerticals: function(segs
) {
8106 for (i
= 0; i
< segs
.length
; i
++) {
8108 seg
.top
= this.computeDateTop(seg
.start
, seg
.start
);
8109 seg
.bottom
= this.computeDateTop(seg
.end
, seg
.start
);
8114 // Given segments that already have their top/bottom properties computed, applies those values to
8115 // the segments' elements.
8116 assignSegVerticals: function(segs
) {
8119 for (i
= 0; i
< segs
.length
; i
++) {
8121 seg
.el
.css(this.generateSegVerticalCss(seg
));
8126 // Generates an object with CSS properties for the top/bottom coordinates of a segment element
8127 generateSegVerticalCss: function(seg
) {
8130 bottom
: -seg
.bottom
// flipped because needs to be space beyond bottom edge of event container
8135 /* Foreground Event Positioning Utils
8136 ------------------------------------------------------------------------------------------------------------------*/
8139 // Given segments that are assumed to all live in the *same column*,
8140 // compute their verical/horizontal coordinates and assign to their elements.
8141 updateFgSegCoords: function(segs
) {
8142 this.computeSegVerticals(segs
); // horizontals relies on this
8143 this.computeFgSegHorizontals(segs
); // compute horizontal coordinates, z-index's, and reorder the array
8144 this.assignSegVerticals(segs
);
8145 this.assignFgSegHorizontals(segs
);
8149 // Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each.
8150 // NOTE: Also reorders the given array by date!
8151 computeFgSegHorizontals: function(segs
) {
8156 this.sortEventSegs(segs
); // order by certain criteria
8157 levels
= buildSlotSegLevels(segs
);
8158 computeForwardSlotSegs(levels
);
8160 if ((level0
= levels
[0])) {
8162 for (i
= 0; i
< level0
.length
; i
++) {
8163 computeSlotSegPressures(level0
[i
]);
8166 for (i
= 0; i
< level0
.length
; i
++) {
8167 this.computeFgSegForwardBack(level0
[i
], 0, 0);
8173 // Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
8174 // from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
8175 // seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
8177 // The segment might be part of a "series", which means consecutive segments with the same pressure
8178 // who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
8179 // segments behind this one in the current series, and `seriesBackwardCoord` is the starting
8180 // coordinate of the first segment in the series.
8181 computeFgSegForwardBack: function(seg
, seriesBackwardPressure
, seriesBackwardCoord
) {
8182 var forwardSegs
= seg
.forwardSegs
;
8185 if (seg
.forwardCoord
=== undefined) { // not already computed
8187 if (!forwardSegs
.length
) {
8189 // if there are no forward segments, this segment should butt up against the edge
8190 seg
.forwardCoord
= 1;
8194 // sort highest pressure first
8195 this.sortForwardSegs(forwardSegs
);
8197 // this segment's forwardCoord will be calculated from the backwardCoord of the
8198 // highest-pressure forward segment.
8199 this.computeFgSegForwardBack(forwardSegs
[0], seriesBackwardPressure
+ 1, seriesBackwardCoord
);
8200 seg
.forwardCoord
= forwardSegs
[0].backwardCoord
;
8203 // calculate the backwardCoord from the forwardCoord. consider the series
8204 seg
.backwardCoord
= seg
.forwardCoord
-
8205 (seg
.forwardCoord
- seriesBackwardCoord
) / // available width for series
8206 (seriesBackwardPressure
+ 1); // # of segments in the series
8208 // use this segment's coordinates to computed the coordinates of the less-pressurized
8210 for (i
=0; i
<forwardSegs
.length
; i
++) {
8211 this.computeFgSegForwardBack(forwardSegs
[i
], 0, seg
.forwardCoord
);
8217 sortForwardSegs: function(forwardSegs
) {
8218 forwardSegs
.sort(proxy(this, 'compareForwardSegs'));
8222 // A cmp function for determining which forward segment to rely on more when computing coordinates.
8223 compareForwardSegs: function(seg1
, seg2
) {
8224 // put higher-pressure first
8225 return seg2
.forwardPressure
- seg1
.forwardPressure
||
8226 // put segments that are closer to initial edge first (and favor ones with no coords yet)
8227 (seg1
.backwardCoord
|| 0) - (seg2
.backwardCoord
|| 0) ||
8228 // do normal sorting...
8229 this.compareEventSegs(seg1
, seg2
);
8233 // Given foreground event segments that have already had their position coordinates computed,
8234 // assigns position-related CSS values to their elements.
8235 assignFgSegHorizontals: function(segs
) {
8238 for (i
= 0; i
< segs
.length
; i
++) {
8240 seg
.el
.css(this.generateFgSegHorizontalCss(seg
));
8242 // if the height is short, add a className for alternate styling
8243 if (seg
.bottom
- seg
.top
< 30) {
8244 seg
.el
.addClass('fc-short');
8250 // Generates an object with CSS properties/values that should be applied to an event segment element.
8251 // Contains important positioning-related properties that should be applied to any event element, customized or not.
8252 generateFgSegHorizontalCss: function(seg
) {
8253 var shouldOverlap
= this.view
.opt('slotEventOverlap');
8254 var backwardCoord
= seg
.backwardCoord
; // the left side if LTR. the right side if RTL. floating-point
8255 var forwardCoord
= seg
.forwardCoord
; // the right side if LTR. the left side if RTL. floating-point
8256 var props
= this.generateSegVerticalCss(seg
); // get top/bottom first
8257 var left
; // amount of space from left edge, a fraction of the total width
8258 var right
; // amount of space from right edge, a fraction of the total width
8260 if (shouldOverlap
) {
8261 // double the width, but don't go beyond the maximum forward coordinate (1.0)
8262 forwardCoord
= Math
.min(1, backwardCoord
+ (forwardCoord
- backwardCoord
) * 2);
8266 left
= 1 - forwardCoord
;
8267 right
= backwardCoord
;
8270 left
= backwardCoord
;
8271 right
= 1 - forwardCoord
;
8274 props
.zIndex
= seg
.level
+ 1; // convert from 0-base to 1-based
8275 props
.left
= left
* 100 + '%';
8276 props
.right
= right
* 100 + '%';
8278 if (shouldOverlap
&& seg
.forwardPressure
) {
8279 // add padding to the edge so that forward stacked events don't cover the resizer's icon
8280 props
[this.isRTL
? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width
8289 // Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is
8290 // left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date.
8291 function buildSlotSegLevels(segs
) {
8296 for (i
=0; i
<segs
.length
; i
++) {
8299 // go through all the levels and stop on the first level where there are no collisions
8300 for (j
=0; j
<levels
.length
; j
++) {
8301 if (!computeSlotSegCollisions(seg
, levels
[j
]).length
) {
8308 (levels
[j
] || (levels
[j
] = [])).push(seg
);
8315 // For every segment, figure out the other segments that are in subsequent
8316 // levels that also occupy the same vertical space. Accumulate in seg.forwardSegs
8317 function computeForwardSlotSegs(levels
) {
8322 for (i
=0; i
<levels
.length
; i
++) {
8325 for (j
=0; j
<level
.length
; j
++) {
8328 seg
.forwardSegs
= [];
8329 for (k
=i
+1; k
<levels
.length
; k
++) {
8330 computeSlotSegCollisions(seg
, levels
[k
], seg
.forwardSegs
);
8337 // Figure out which path forward (via seg.forwardSegs) results in the longest path until
8338 // the furthest edge is reached. The number of segments in this path will be seg.forwardPressure
8339 function computeSlotSegPressures(seg
) {
8340 var forwardSegs
= seg
.forwardSegs
;
8341 var forwardPressure
= 0;
8344 if (seg
.forwardPressure
=== undefined) { // not already computed
8346 for (i
=0; i
<forwardSegs
.length
; i
++) {
8347 forwardSeg
= forwardSegs
[i
];
8349 // figure out the child's maximum forward path
8350 computeSlotSegPressures(forwardSeg
);
8352 // either use the existing maximum, or use the child's forward pressure
8353 // plus one (for the forwardSeg itself)
8354 forwardPressure
= Math
.max(
8356 1 + forwardSeg
.forwardPressure
8360 seg
.forwardPressure
= forwardPressure
;
8365 // Find all the segments in `otherSegs` that vertically collide with `seg`.
8366 // Append into an optionally-supplied `results` array and return.
8367 function computeSlotSegCollisions(seg
, otherSegs
, results
) {
8368 results
= results
|| [];
8370 for (var i
=0; i
<otherSegs
.length
; i
++) {
8371 if (isSlotSegCollision(seg
, otherSegs
[i
])) {
8372 results
.push(otherSegs
[i
]);
8380 // Do these segments occupy the same vertical space?
8381 function isSlotSegCollision(seg1
, seg2
) {
8382 return seg1
.bottom
> seg2
.top
&& seg1
.top
< seg2
.bottom
;
8387 /* An abstract class from which other views inherit from
8388 ----------------------------------------------------------------------------------------------------------------------*/
8390 var View
= FC
.View
= Class
.extend(EmitterMixin
, ListenerMixin
, {
8392 type
: null, // subclass' view name (string)
8393 name
: null, // deprecated. use `type` instead
8394 title
: null, // the text that will be displayed in the header's title
8396 calendar
: null, // owner Calendar object
8397 options
: null, // hash containing all options. already merged with view-specific-options
8398 el
: null, // the view's containing element. set by Calendar
8401 isDateRendered
: false,
8402 dateRenderQueue
: null,
8404 isEventsBound
: false,
8406 isEventsRendered
: false,
8407 eventRenderQueue
: null,
8409 // range the view is actually displaying (moments)
8411 end
: null, // exclusive
8413 // range the view is formally responsible for (moments)
8414 // may be different from start/end. for example, a month view might have 1st-31st, excluding padded dates
8415 intervalStart
: null,
8416 intervalEnd
: null, // exclusive
8417 intervalDuration
: null,
8418 intervalUnit
: null, // name of largest unit being displayed, like "month" or "week"
8421 isSelected
: false, // boolean whether a range of time is user-selected or not
8422 selectedEvent
: null,
8424 eventOrderSpecs
: null, // criteria for ordering events when they have same date/time
8426 // classNames styled by jqui themes
8427 widgetHeaderClass
: null,
8428 widgetContentClass
: null,
8429 highlightStateClass
: null,
8431 // for date utils, computed from options
8432 nextDayThreshold
: null,
8433 isHiddenDayHash
: null,
8436 isNowIndicatorRendered
: null,
8437 initialNowDate
: null, // result first getNow call
8438 initialNowQueriedMs
: null, // ms time the getNow was called
8439 nowIndicatorTimeoutID
: null, // for refresh timing of now indicator
8440 nowIndicatorIntervalID
: null, // "
8443 constructor: function(calendar
, type
, options
, intervalDuration
) {
8445 this.calendar
= calendar
;
8446 this.type
= this.name
= type
; // .name is deprecated
8447 this.options
= options
;
8448 this.intervalDuration
= intervalDuration
|| moment
.duration(1, 'day');
8450 this.nextDayThreshold
= moment
.duration(this.opt('nextDayThreshold'));
8451 this.initThemingProps();
8452 this.initHiddenDays();
8453 this.isRTL
= this.opt('isRTL');
8455 this.eventOrderSpecs
= parseFieldSpecs(this.opt('eventOrder'));
8457 this.dateRenderQueue
= new TaskQueue();
8458 this.eventRenderQueue
= new TaskQueue(this.opt('eventRenderWait'));
8464 // A good place for subclasses to initialize member variables
8465 initialize: function() {
8466 // subclasses can implement
8470 // Retrieves an option with the given name
8471 opt: function(name
) {
8472 return this.options
[name
];
8476 // Triggers handlers that are view-related. Modifies args before passing to calendar.
8477 publiclyTrigger: function(name
, thisObj
) { // arguments beyond thisObj are passed along
8478 var calendar
= this.calendar
;
8480 return calendar
.publiclyTrigger
.apply(
8482 [name
, thisObj
|| this].concat(
8483 Array
.prototype.slice
.call(arguments
, 2), // arguments beyond thisObj
8484 [ this ] // always make the last argument a reference to the view. TODO: deprecate
8490 // Returns a proxy of the given promise that will be rejected if the given event fires
8491 // before the promise resolves.
8492 rejectOn: function(eventName
, promise
) {
8495 return new Promise(function(resolve
, reject
) {
8496 _this
.one(eventName
, reject
);
8498 function cleanup() {
8499 _this
.off(eventName
, reject
);
8502 promise
.then(function(res
) { // success
8505 }, function() { // failure
8514 ------------------------------------------------------------------------------------------------------------------*/
8517 // Updates all internal dates for displaying the given unzoned range.
8518 setRange: function(range
) {
8519 $.extend(this, range
); // assigns every property to this object's member variables
8524 // Given a single current unzoned date, produce information about what range to display.
8525 // Subclasses can override. Must return all properties.
8526 computeRange: function(date
) {
8527 var intervalUnit
= computeIntervalUnit(this.intervalDuration
);
8528 var intervalStart
= date
.clone().startOf(intervalUnit
);
8529 var intervalEnd
= intervalStart
.clone().add(this.intervalDuration
);
8532 // normalize the range's time-ambiguity
8533 if (/year|month|week|day/.test(intervalUnit
)) { // whole-days?
8534 intervalStart
.stripTime();
8535 intervalEnd
.stripTime();
8537 else { // needs to have a time?
8538 if (!intervalStart
.hasTime()) {
8539 intervalStart
= this.calendar
.time(0); // give 00:00 time
8541 if (!intervalEnd
.hasTime()) {
8542 intervalEnd
= this.calendar
.time(0); // give 00:00 time
8546 start
= intervalStart
.clone();
8547 start
= this.skipHiddenDays(start
);
8548 end
= intervalEnd
.clone();
8549 end
= this.skipHiddenDays(end
, -1, true); // exclusively move backwards
8552 intervalUnit
: intervalUnit
,
8553 intervalStart
: intervalStart
,
8554 intervalEnd
: intervalEnd
,
8561 // Computes the new date when the user hits the prev button, given the current date
8562 computePrevDate: function(date
) {
8563 return this.massageCurrentDate(
8564 date
.clone().startOf(this.intervalUnit
).subtract(this.intervalDuration
), -1
8569 // Computes the new date when the user hits the next button, given the current date
8570 computeNextDate: function(date
) {
8571 return this.massageCurrentDate(
8572 date
.clone().startOf(this.intervalUnit
).add(this.intervalDuration
)
8577 // Given an arbitrarily calculated current date of the calendar, returns a date that is ensured to be completely
8578 // visible. `direction` is optional and indicates which direction the current date was being
8579 // incremented or decremented (1 or -1).
8580 massageCurrentDate: function(date
, direction
) {
8581 if (this.intervalDuration
.as('days') <= 1) { // if the view displays a single day or smaller
8582 if (this.isHiddenDay(date
)) {
8583 date
= this.skipHiddenDays(date
, direction
);
8584 date
.startOf('day');
8592 /* Title and Date Formatting
8593 ------------------------------------------------------------------------------------------------------------------*/
8596 // Sets the view's title property to the most updated computed value
8597 updateTitle: function() {
8598 this.title
= this.computeTitle();
8599 this.calendar
.setToolbarsTitle(this.title
);
8603 // Computes what the title at the top of the calendar should be for this view
8604 computeTitle: function() {
8607 // for views that span a large unit of time, show the proper interval, ignoring stray days before and after
8608 if (this.intervalUnit
=== 'year' || this.intervalUnit
=== 'month') {
8609 start
= this.intervalStart
;
8610 end
= this.intervalEnd
;
8612 else { // for day units or smaller, use the actual day range
8617 return this.formatRange(
8619 // in case intervalStart/End has a time, make sure timezone is correct
8620 start
: this.calendar
.applyTimezone(start
),
8621 end
: this.calendar
.applyTimezone(end
)
8623 this.opt('titleFormat') || this.computeTitleFormat(),
8624 this.opt('titleRangeSeparator')
8629 // Generates the format string that should be used to generate the title for the current date range.
8630 // Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
8631 computeTitleFormat: function() {
8632 if (this.intervalUnit
== 'year') {
8635 else if (this.intervalUnit
== 'month') {
8636 return this.opt('monthYearFormat'); // like "September 2014"
8638 else if (this.intervalDuration
.as('days') > 1) {
8639 return 'll'; // multi-day range. shorter, like "Sep 9 - 10 2014"
8642 return 'LL'; // one day. longer, like "September 9 2014"
8647 // Utility for formatting a range. Accepts a range object, formatting string, and optional separator.
8648 // Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account.
8649 // The timezones of the dates within `range` will be respected.
8650 formatRange: function(range
, formatStr
, separator
) {
8651 var end
= range
.end
;
8653 if (!end
.hasTime()) { // all-day?
8654 end
= end
.clone().subtract(1); // convert to inclusive. last ms of previous day
8657 return formatRange(range
.start
, end
, formatStr
, separator
, this.opt('isRTL'));
8661 getAllDayHtml: function() {
8662 return this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'));
8667 ------------------------------------------------------------------------------------------------------------------*/
8670 // Generates HTML for an anchor to another view into the calendar.
8671 // Will either generate an <a> tag or a non-clickable <span> tag, depending on enabled settings.
8672 // `gotoOptions` can either be a moment input, or an object with the form:
8673 // { date, type, forceOff }
8674 // `type` is a view-type like "day" or "week". default value is "day".
8675 // `attrs` and `innerHtml` are use to generate the rest of the HTML tag.
8676 buildGotoAnchorHtml: function(gotoOptions
, attrs
, innerHtml
) {
8677 var date
, type
, forceOff
;
8680 if ($.isPlainObject(gotoOptions
)) {
8681 date
= gotoOptions
.date
;
8682 type
= gotoOptions
.type
;
8683 forceOff
= gotoOptions
.forceOff
;
8686 date
= gotoOptions
; // a single moment input
8688 date
= FC
.moment(date
); // if a string, parse it
8690 finalOptions
= { // for serialization into the link
8691 date
: date
.format('YYYY-MM-DD'),
8695 if (typeof attrs
=== 'string') {
8700 attrs
= attrs
? ' ' + attrsToStr(attrs
) : ''; // will have a leading space
8701 innerHtml
= innerHtml
|| '';
8703 if (!forceOff
&& this.opt('navLinks')) {
8704 return '<a' + attrs
+
8705 ' data-goto="' + htmlEscape(JSON
.stringify(finalOptions
)) + '">' +
8710 return '<span' + attrs
+ '>' +
8717 // Rendering Non-date-related Content
8718 // -----------------------------------------------------------------------------------------------------------------
8721 // Sets the container element that the view should render inside of, does global DOM-related initializations,
8722 // and renders all the non-date-related content inside.
8723 setElement: function(el
) {
8725 this.bindGlobalHandlers();
8726 this.renderSkeleton();
8730 // Removes the view's container element from the DOM, clearing any content beforehand.
8731 // Undoes any other DOM-related attachments.
8732 removeElement: function() {
8734 this.unrenderSkeleton();
8736 this.unbindGlobalHandlers();
8739 // NOTE: don't null-out this.el in case the View was destroyed within an API callback.
8740 // We don't null-out the View's other jQuery element references upon destroy,
8741 // so we shouldn't kill this.el either.
8745 // Renders the basic structure of the view before any content is rendered
8746 renderSkeleton: function() {
8747 // subclasses should implement
8751 // Unrenders the basic structure of the view
8752 unrenderSkeleton: function() {
8753 // subclasses should implement
8757 // Date Setting/Unsetting
8758 // -----------------------------------------------------------------------------------------------------------------
8761 setDate: function(date
) {
8762 var isReset
= this.isDateSet
;
8764 this.isDateSet
= true;
8765 this.handleDate(date
, isReset
);
8766 this.trigger(isReset
? 'dateReset' : 'dateSet', date
);
8770 unsetDate: function() {
8771 if (this.isDateSet
) {
8772 this.isDateSet
= false;
8773 this.handleDateUnset();
8774 this.trigger('dateUnset');
8780 // -----------------------------------------------------------------------------------------------------------------
8783 handleDate: function(date
, isReset
) {
8786 this.unbindEvents(); // will do nothing if not already bound
8787 this.requestDateRender(date
).then(function() {
8788 // wish we could start earlier, but setRange/computeRange needs to execute first
8789 _this
.bindEvents(); // will request events
8794 handleDateUnset: function() {
8795 this.unbindEvents();
8796 this.requestDateUnrender();
8800 // Date Render Queuing
8801 // -----------------------------------------------------------------------------------------------------------------
8804 // if date not specified, uses current
8805 requestDateRender: function(date
) {
8808 return this.dateRenderQueue
.add(function() {
8809 return _this
.executeDateRender(date
);
8814 requestDateUnrender: function() {
8817 return this.dateRenderQueue
.add(function() {
8818 return _this
.executeDateUnrender();
8823 // Date High-level Rendering
8824 // -----------------------------------------------------------------------------------------------------------------
8827 // if date not specified, uses current
8828 executeDateRender: function(date
) {
8831 // if rendering a new date, reset scroll to initial state (scrollTime)
8833 this.captureInitialScroll();
8836 this.captureScroll(); // a rerender of the current date
8839 this.freezeHeight();
8841 return this.executeDateUnrender().then(function() {
8844 _this
.setRange(_this
.computeRange(date
));
8848 _this
.render(); // TODO: deprecate
8851 _this
.renderDates();
8853 _this
.renderBusinessHours(); // might need coordinates, so should go after updateSize()
8854 _this
.startNowIndicator();
8857 _this
.releaseScroll();
8859 _this
.isDateRendered
= true;
8860 _this
.onDateRender();
8861 _this
.trigger('dateRender');
8866 executeDateUnrender: function() {
8869 if (_this
.isDateRendered
) {
8870 return this.requestEventsUnrender().then(function() {
8873 _this
.stopNowIndicator();
8874 _this
.triggerUnrender();
8875 _this
.unrenderBusinessHours();
8876 _this
.unrenderDates();
8878 if (_this
.destroy
) {
8879 _this
.destroy(); // TODO: deprecate
8882 _this
.isDateRendered
= false;
8883 _this
.trigger('dateUnrender');
8887 return Promise
.resolve();
8892 // Date Rendering Triggers
8893 // -----------------------------------------------------------------------------------------------------------------
8896 onDateRender: function() {
8897 this.triggerRender();
8901 // Date Low-level Rendering
8902 // -----------------------------------------------------------------------------------------------------------------
8905 // date-cell content only
8906 renderDates: function() {
8907 // subclasses should implement
8911 // date-cell content only
8912 unrenderDates: function() {
8913 // subclasses should override
8917 // Misc view rendering utils
8918 // -------------------------
8921 // Signals that the view's content has been rendered
8922 triggerRender: function() {
8923 this.publiclyTrigger('viewRender', this, this, this.el
);
8927 // Signals that the view's content is about to be unrendered
8928 triggerUnrender: function() {
8929 this.publiclyTrigger('viewDestroy', this, this, this.el
);
8933 // Binds DOM handlers to elements that reside outside the view container, such as the document
8934 bindGlobalHandlers: function() {
8935 this.listenTo(GlobalEmitter
.get(), {
8936 touchstart
: this.processUnselect
,
8937 mousedown
: this.handleDocumentMousedown
8942 // Unbinds DOM handlers from elements that reside outside the view container
8943 unbindGlobalHandlers: function() {
8944 this.stopListeningTo(GlobalEmitter
.get());
8948 // Initializes internal variables related to theming
8949 initThemingProps: function() {
8950 var tm
= this.opt('theme') ? 'ui' : 'fc';
8952 this.widgetHeaderClass
= tm
+ '-widget-header';
8953 this.widgetContentClass
= tm
+ '-widget-content';
8954 this.highlightStateClass
= tm
+ '-state-highlight';
8959 ------------------------------------------------------------------------------------------------------------------*/
8962 // Renders business-hours onto the view. Assumes updateSize has already been called.
8963 renderBusinessHours: function() {
8964 // subclasses should implement
8968 // Unrenders previously-rendered business-hours
8969 unrenderBusinessHours: function() {
8970 // subclasses should implement
8975 ------------------------------------------------------------------------------------------------------------------*/
8978 // Immediately render the current time indicator and begins re-rendering it at an interval,
8979 // which is defined by this.getNowIndicatorUnit().
8980 // TODO: somehow do this for the current whole day's background too
8981 startNowIndicator: function() {
8985 var delay
; // ms wait value
8987 if (this.opt('nowIndicator')) {
8988 unit
= this.getNowIndicatorUnit();
8990 update
= proxy(this, 'updateNowIndicator'); // bind to `this`
8992 this.initialNowDate
= this.calendar
.getNow();
8993 this.initialNowQueriedMs
= +new Date();
8994 this.renderNowIndicator(this.initialNowDate
);
8995 this.isNowIndicatorRendered
= true;
8997 // wait until the beginning of the next interval
8998 delay
= this.initialNowDate
.clone().startOf(unit
).add(1, unit
) - this.initialNowDate
;
8999 this.nowIndicatorTimeoutID
= setTimeout(function() {
9000 _this
.nowIndicatorTimeoutID
= null;
9002 delay
= +moment
.duration(1, unit
);
9003 delay
= Math
.max(100, delay
); // prevent too frequent
9004 _this
.nowIndicatorIntervalID
= setInterval(update
, delay
); // update every interval
9011 // rerenders the now indicator, computing the new current time from the amount of time that has passed
9012 // since the initial getNow call.
9013 updateNowIndicator: function() {
9014 if (this.isNowIndicatorRendered
) {
9015 this.unrenderNowIndicator();
9016 this.renderNowIndicator(
9017 this.initialNowDate
.clone().add(new Date() - this.initialNowQueriedMs
) // add ms
9023 // Immediately unrenders the view's current time indicator and stops any re-rendering timers.
9024 // Won't cause side effects if indicator isn't rendered.
9025 stopNowIndicator: function() {
9026 if (this.isNowIndicatorRendered
) {
9028 if (this.nowIndicatorTimeoutID
) {
9029 clearTimeout(this.nowIndicatorTimeoutID
);
9030 this.nowIndicatorTimeoutID
= null;
9032 if (this.nowIndicatorIntervalID
) {
9033 clearTimeout(this.nowIndicatorIntervalID
);
9034 this.nowIndicatorIntervalID
= null;
9037 this.unrenderNowIndicator();
9038 this.isNowIndicatorRendered
= false;
9043 // Returns a string unit, like 'second' or 'minute' that defined how often the current time indicator
9044 // should be refreshed. If something falsy is returned, no time indicator is rendered at all.
9045 getNowIndicatorUnit: function() {
9046 // subclasses should implement
9050 // Renders a current time indicator at the given datetime
9051 renderNowIndicator: function(date
) {
9052 // subclasses should implement
9056 // Undoes the rendering actions from renderNowIndicator
9057 unrenderNowIndicator: function() {
9058 // subclasses should implement
9063 ------------------------------------------------------------------------------------------------------------------*/
9066 // Refreshes anything dependant upon sizing of the container element of the grid
9067 updateSize: function(isResize
) {
9070 this.captureScroll();
9073 this.updateHeight(isResize
);
9074 this.updateWidth(isResize
);
9075 this.updateNowIndicator();
9078 this.releaseScroll();
9083 // Refreshes the horizontal dimensions of the calendar
9084 updateWidth: function(isResize
) {
9085 // subclasses should implement
9089 // Refreshes the vertical dimensions of the calendar
9090 updateHeight: function(isResize
) {
9091 var calendar
= this.calendar
; // we poll the calendar for height information
9094 calendar
.getSuggestedViewHeight(),
9095 calendar
.isHeightAuto()
9100 // Updates the vertical dimensions of the calendar to the specified height.
9101 // if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height.
9102 setHeight: function(height
, isAuto
) {
9103 // subclasses should implement
9108 ------------------------------------------------------------------------------------------------------------------*/
9111 capturedScroll
: null,
9112 capturedScrollDepth
: 0,
9115 captureScroll: function() {
9116 if (!(this.capturedScrollDepth
++)) {
9117 this.capturedScroll
= this.isDateRendered
? this.queryScroll() : {}; // require a render first
9118 return true; // root?
9124 captureInitialScroll: function(forcedScroll
) {
9125 if (this.captureScroll()) { // root?
9126 this.capturedScroll
.isInitial
= true;
9129 $.extend(this.capturedScroll
, forcedScroll
);
9132 this.capturedScroll
.isComputed
= true;
9138 releaseScroll: function() {
9139 var scroll
= this.capturedScroll
;
9140 var isRoot
= this.discardScroll();
9142 if (scroll
.isComputed
) {
9144 // only compute initial scroll if it will actually be used (is the root capture)
9145 $.extend(scroll
, this.computeInitialScroll());
9148 scroll
= null; // scroll couldn't be computed. don't apply it to the DOM
9153 // we act immediately on a releaseScroll operation, as opposed to captureScroll.
9154 // if capture/release wraps a render operation that screws up the scroll,
9155 // we still want to restore it a good state after, regardless of depth.
9157 if (scroll
.isInitial
) {
9158 this.hardSetScroll(scroll
); // outsmart how browsers set scroll on initial DOM
9161 this.setScroll(scroll
);
9167 discardScroll: function() {
9168 if (!(--this.capturedScrollDepth
)) {
9169 this.capturedScroll
= null;
9170 return true; // root?
9176 computeInitialScroll: function() {
9181 queryScroll: function() {
9186 hardSetScroll: function(scroll
) {
9188 var exec = function() { _this
.setScroll(scroll
); };
9190 setTimeout(exec
, 0); // to surely clear the browser's initial scroll for the DOM
9194 setScroll: function(scroll
) {
9199 ------------------------------------------------------------------------------------------------------------------*/
9202 freezeHeight: function() {
9203 this.calendar
.freezeContentHeight();
9207 thawHeight: function() {
9208 this.calendar
.thawContentHeight();
9212 // Event Binding/Unbinding
9213 // -----------------------------------------------------------------------------------------------------------------
9216 bindEvents: function() {
9219 if (!this.isEventsBound
) {
9220 this.isEventsBound
= true;
9221 this.rejectOn('eventsUnbind', this.requestEvents()).then(function(events
) { // TODO: test rejection
9222 _this
.listenTo(_this
.calendar
, 'eventsReset', _this
.setEvents
);
9223 _this
.setEvents(events
);
9229 unbindEvents: function() {
9230 if (this.isEventsBound
) {
9231 this.isEventsBound
= false;
9232 this.stopListeningTo(this.calendar
, 'eventsReset');
9234 this.trigger('eventsUnbind');
9239 // Event Setting/Unsetting
9240 // -----------------------------------------------------------------------------------------------------------------
9243 setEvents: function(events
) {
9244 var isReset
= this.isEventSet
;
9246 this.isEventsSet
= true;
9247 this.handleEvents(events
, isReset
);
9248 this.trigger(isReset
? 'eventsReset' : 'eventsSet', events
);
9252 unsetEvents: function() {
9253 if (this.isEventsSet
) {
9254 this.isEventsSet
= false;
9255 this.handleEventsUnset();
9256 this.trigger('eventsUnset');
9261 whenEventsSet: function() {
9264 if (this.isEventsSet
) {
9265 return Promise
.resolve(this.getCurrentEvents());
9268 return new Promise(function(resolve
) {
9269 _this
.one('eventsSet', resolve
);
9276 // -----------------------------------------------------------------------------------------------------------------
9279 handleEvents: function(events
, isReset
) {
9280 this.requestEventsRender(events
);
9284 handleEventsUnset: function() {
9285 this.requestEventsUnrender();
9289 // Event Render Queuing
9290 // -----------------------------------------------------------------------------------------------------------------
9293 // assumes any previous event renders have been cleared already
9294 requestEventsRender: function(events
) {
9297 return this.eventRenderQueue
.add(function() { // might not return a promise if debounced!? bad
9298 return _this
.executeEventsRender(events
);
9303 requestEventsUnrender: function() {
9306 if (this.isEventsRendered
) {
9307 return this.eventRenderQueue
.addQuickly(function() {
9308 return _this
.executeEventsUnrender();
9312 return Promise
.resolve();
9317 requestCurrentEventsRender: function() {
9318 if (this.isEventsSet
) {
9319 this.requestEventsRender(this.getCurrentEvents());
9322 return Promise
.reject();
9327 // Event High-level Rendering
9328 // -----------------------------------------------------------------------------------------------------------------
9331 executeEventsRender: function(events
) {
9334 this.captureScroll();
9335 this.freezeHeight();
9337 return this.executeEventsUnrender().then(function() {
9338 _this
.renderEvents(events
);
9341 _this
.releaseScroll();
9343 _this
.isEventsRendered
= true;
9344 _this
.onEventsRender();
9345 _this
.trigger('eventsRender');
9350 executeEventsUnrender: function() {
9351 if (this.isEventsRendered
) {
9352 this.onBeforeEventsUnrender();
9354 this.captureScroll();
9355 this.freezeHeight();
9357 if (this.destroyEvents
) {
9358 this.destroyEvents(); // TODO: deprecate
9361 this.unrenderEvents();
9364 this.releaseScroll();
9366 this.isEventsRendered
= false;
9367 this.trigger('eventsUnrender');
9370 return Promise
.resolve(); // always synchronous
9374 // Event Rendering Triggers
9375 // -----------------------------------------------------------------------------------------------------------------
9378 // Signals that all events have been rendered
9379 onEventsRender: function() {
9380 this.renderedEventSegEach(function(seg
) {
9381 this.publiclyTrigger('eventAfterRender', seg
.event
, seg
.event
, seg
.el
);
9383 this.publiclyTrigger('eventAfterAllRender');
9387 // Signals that all event elements are about to be removed
9388 onBeforeEventsUnrender: function() {
9389 this.renderedEventSegEach(function(seg
) {
9390 this.publiclyTrigger('eventDestroy', seg
.event
, seg
.event
, seg
.el
);
9395 // Event Low-level Rendering
9396 // -----------------------------------------------------------------------------------------------------------------
9399 // Renders the events onto the view.
9400 renderEvents: function(events
) {
9401 // subclasses should implement
9405 // Removes event elements from the view.
9406 unrenderEvents: function() {
9407 // subclasses should implement
9411 // Event Data Access
9412 // -----------------------------------------------------------------------------------------------------------------
9415 requestEvents: function() {
9416 return this.calendar
.requestEvents(this.start
, this.end
);
9420 getCurrentEvents: function() {
9421 return this.calendar
.getPrunedEventCache();
9425 // Event Rendering Utils
9426 // -----------------------------------------------------------------------------------------------------------------
9429 // Given an event and the default element used for rendering, returns the element that should actually be used.
9430 // Basically runs events and elements through the eventRender hook.
9431 resolveEventEl: function(event
, el
) {
9432 var custom
= this.publiclyTrigger('eventRender', event
, event
, el
);
9434 if (custom
=== false) { // means don't render at all
9437 else if (custom
&& custom
!== true) {
9445 // Hides all rendered event segments linked to the given event
9446 showEvent: function(event
) {
9447 this.renderedEventSegEach(function(seg
) {
9448 seg
.el
.css('visibility', '');
9453 // Shows all rendered event segments linked to the given event
9454 hideEvent: function(event
) {
9455 this.renderedEventSegEach(function(seg
) {
9456 seg
.el
.css('visibility', 'hidden');
9461 // Iterates through event segments that have been rendered (have an el). Goes through all by default.
9462 // If the optional `event` argument is specified, only iterates through segments linked to that event.
9463 // The `this` value of the callback function will be the view.
9464 renderedEventSegEach: function(func
, event
) {
9465 var segs
= this.getEventSegs();
9468 for (i
= 0; i
< segs
.length
; i
++) {
9469 if (!event
|| segs
[i
].event
._id
=== event
._id
) {
9471 func
.call(this, segs
[i
]);
9478 // Retrieves all the rendered segment objects for the view
9479 getEventSegs: function() {
9480 // subclasses must implement
9485 /* Event Drag-n-Drop
9486 ------------------------------------------------------------------------------------------------------------------*/
9489 // Computes if the given event is allowed to be dragged by the user
9490 isEventDraggable: function(event
) {
9491 return this.isEventStartEditable(event
);
9495 isEventStartEditable: function(event
) {
9496 return firstDefined(
9497 event
.startEditable
,
9498 (event
.source
|| {}).startEditable
,
9499 this.opt('eventStartEditable'),
9500 this.isEventGenerallyEditable(event
)
9505 isEventGenerallyEditable: function(event
) {
9506 return firstDefined(
9508 (event
.source
|| {}).editable
,
9509 this.opt('editable')
9514 // Must be called when an event in the view is dropped onto new location.
9515 // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event.
9516 reportSegDrop: function(seg
, dropLocation
, largeUnit
, el
, ev
) {
9517 var calendar
= this.calendar
;
9518 var mutateResult
= calendar
.mutateSeg(seg
, dropLocation
, largeUnit
);
9519 var undoFunc = function() {
9520 mutateResult
.undo();
9521 calendar
.reportEventChange();
9524 this.triggerEventDrop(seg
.event
, mutateResult
.dateDelta
, undoFunc
, el
, ev
);
9525 calendar
.reportEventChange(); // will rerender events
9529 // Triggers event-drop handlers that have subscribed via the API
9530 triggerEventDrop: function(event
, dateDelta
, undoFunc
, el
, ev
) {
9531 this.publiclyTrigger('eventDrop', el
[0], event
, dateDelta
, undoFunc
, ev
, {}); // {} = jqui dummy
9535 /* External Element Drag-n-Drop
9536 ------------------------------------------------------------------------------------------------------------------*/
9539 // Must be called when an external element, via jQuery UI, has been dropped onto the calendar.
9540 // `meta` is the parsed data that has been embedded into the dragging event.
9541 // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event.
9542 reportExternalDrop: function(meta
, dropLocation
, el
, ev
, ui
) {
9543 var eventProps
= meta
.eventProps
;
9547 // Try to build an event object and render it. TODO: decouple the two
9549 eventInput
= $.extend({}, eventProps
, dropLocation
);
9550 event
= this.calendar
.renderEvent(eventInput
, meta
.stick
)[0]; // renderEvent returns an array
9553 this.triggerExternalDrop(event
, dropLocation
, el
, ev
, ui
);
9557 // Triggers external-drop handlers that have subscribed via the API
9558 triggerExternalDrop: function(event
, dropLocation
, el
, ev
, ui
) {
9560 // trigger 'drop' regardless of whether element represents an event
9561 this.publiclyTrigger('drop', el
[0], dropLocation
.start
, ev
, ui
);
9564 this.publiclyTrigger('eventReceive', null, event
); // signal an external event landed
9569 /* Drag-n-Drop Rendering (for both events and external elements)
9570 ------------------------------------------------------------------------------------------------------------------*/
9573 // Renders a visual indication of a event or external-element drag over the given drop zone.
9574 // If an external-element, seg will be `null`.
9575 // Must return elements used for any mock events.
9576 renderDrag: function(dropLocation
, seg
) {
9577 // subclasses must implement
9581 // Unrenders a visual indication of an event or external-element being dragged.
9582 unrenderDrag: function() {
9583 // subclasses must implement
9588 ------------------------------------------------------------------------------------------------------------------*/
9591 // Computes if the given event is allowed to be resized from its starting edge
9592 isEventResizableFromStart: function(event
) {
9593 return this.opt('eventResizableFromStart') && this.isEventResizable(event
);
9597 // Computes if the given event is allowed to be resized from its ending edge
9598 isEventResizableFromEnd: function(event
) {
9599 return this.isEventResizable(event
);
9603 // Computes if the given event is allowed to be resized by the user at all
9604 isEventResizable: function(event
) {
9605 var source
= event
.source
|| {};
9607 return firstDefined(
9608 event
.durationEditable
,
9609 source
.durationEditable
,
9610 this.opt('eventDurationEditable'),
9613 this.opt('editable')
9618 // Must be called when an event in the view has been resized to a new length
9619 reportSegResize: function(seg
, resizeLocation
, largeUnit
, el
, ev
) {
9620 var calendar
= this.calendar
;
9621 var mutateResult
= calendar
.mutateSeg(seg
, resizeLocation
, largeUnit
);
9622 var undoFunc = function() {
9623 mutateResult
.undo();
9624 calendar
.reportEventChange();
9627 this.triggerEventResize(seg
.event
, mutateResult
.durationDelta
, undoFunc
, el
, ev
);
9628 calendar
.reportEventChange(); // will rerender events
9632 // Triggers event-resize handlers that have subscribed via the API
9633 triggerEventResize: function(event
, durationDelta
, undoFunc
, el
, ev
) {
9634 this.publiclyTrigger('eventResize', el
[0], event
, durationDelta
, undoFunc
, ev
, {}); // {} = jqui dummy
9638 /* Selection (time range)
9639 ------------------------------------------------------------------------------------------------------------------*/
9642 // Selects a date span on the view. `start` and `end` are both Moments.
9643 // `ev` is the native mouse event that begin the interaction.
9644 select: function(span
, ev
) {
9646 this.renderSelection(span
);
9647 this.reportSelection(span
, ev
);
9651 // Renders a visual indication of the selection
9652 renderSelection: function(span
) {
9653 // subclasses should implement
9657 // Called when a new selection is made. Updates internal state and triggers handlers.
9658 reportSelection: function(span
, ev
) {
9659 this.isSelected
= true;
9660 this.triggerSelect(span
, ev
);
9664 // Triggers handlers to 'select'
9665 triggerSelect: function(span
, ev
) {
9666 this.publiclyTrigger(
9669 this.calendar
.applyTimezone(span
.start
), // convert to calendar's tz for external API
9670 this.calendar
.applyTimezone(span
.end
), // "
9676 // Undoes a selection. updates in the internal state and triggers handlers.
9677 // `ev` is the native mouse event that began the interaction.
9678 unselect: function(ev
) {
9679 if (this.isSelected
) {
9680 this.isSelected
= false;
9681 if (this.destroySelection
) {
9682 this.destroySelection(); // TODO: deprecate
9684 this.unrenderSelection();
9685 this.publiclyTrigger('unselect', null, ev
);
9690 // Unrenders a visual indication of selection
9691 unrenderSelection: function() {
9692 // subclasses should implement
9697 ------------------------------------------------------------------------------------------------------------------*/
9700 selectEvent: function(event
) {
9701 if (!this.selectedEvent
|| this.selectedEvent
!== event
) {
9702 this.unselectEvent();
9703 this.renderedEventSegEach(function(seg
) {
9704 seg
.el
.addClass('fc-selected');
9706 this.selectedEvent
= event
;
9711 unselectEvent: function() {
9712 if (this.selectedEvent
) {
9713 this.renderedEventSegEach(function(seg
) {
9714 seg
.el
.removeClass('fc-selected');
9715 }, this.selectedEvent
);
9716 this.selectedEvent
= null;
9721 isEventSelected: function(event
) {
9722 // event references might change on refetchEvents(), while selectedEvent doesn't,
9724 return this.selectedEvent
&& this.selectedEvent
._id
=== event
._id
;
9728 /* Mouse / Touch Unselecting (time range & event unselection)
9729 ------------------------------------------------------------------------------------------------------------------*/
9730 // TODO: move consistently to down/start or up/end?
9731 // TODO: don't kill previous selection if touch scrolling
9734 handleDocumentMousedown: function(ev
) {
9735 if (isPrimaryMouseButton(ev
)) {
9736 this.processUnselect(ev
);
9741 processUnselect: function(ev
) {
9742 this.processRangeUnselect(ev
);
9743 this.processEventUnselect(ev
);
9747 processRangeUnselect: function(ev
) {
9750 // is there a time-range selection?
9751 if (this.isSelected
&& this.opt('unselectAuto')) {
9752 // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element
9753 ignore
= this.opt('unselectCancel');
9754 if (!ignore
|| !$(ev
.target
).closest(ignore
).length
) {
9761 processEventUnselect: function(ev
) {
9762 if (this.selectedEvent
) {
9763 if (!$(ev
.target
).closest('.fc-selected').length
) {
9764 this.unselectEvent();
9771 ------------------------------------------------------------------------------------------------------------------*/
9774 // Triggers handlers to 'dayClick'
9775 // Span has start/end of the clicked area. Only the start is useful.
9776 triggerDayClick: function(span
, dayEl
, ev
) {
9777 this.publiclyTrigger(
9780 this.calendar
.applyTimezone(span
.start
), // convert to calendar's timezone for external API
9787 ------------------------------------------------------------------------------------------------------------------*/
9790 // Initializes internal variables related to calculating hidden days-of-week
9791 initHiddenDays: function() {
9792 var hiddenDays
= this.opt('hiddenDays') || []; // array of day-of-week indices that are hidden
9793 var isHiddenDayHash
= []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
9797 if (this.opt('weekends') === false) {
9798 hiddenDays
.push(0, 6); // 0=sunday, 6=saturday
9801 for (i
= 0; i
< 7; i
++) {
9803 !(isHiddenDayHash
[i
] = $.inArray(i
, hiddenDays
) !== -1)
9810 throw 'invalid hiddenDays'; // all days were hidden? bad.
9813 this.isHiddenDayHash
= isHiddenDayHash
;
9817 // Is the current day hidden?
9818 // `day` is a day-of-week index (0-6), or a Moment
9819 isHiddenDay: function(day
) {
9820 if (moment
.isMoment(day
)) {
9823 return this.isHiddenDayHash
[day
];
9827 // Incrementing the current day until it is no longer a hidden day, returning a copy.
9828 // If the initial value of `date` is not a hidden day, don't do anything.
9829 // Pass `isExclusive` as `true` if you are dealing with an end date.
9830 // `inc` defaults to `1` (increment one day forward each time)
9831 skipHiddenDays: function(date
, inc
, isExclusive
) {
9832 var out
= date
.clone();
9835 this.isHiddenDayHash
[(out
.day() + (isExclusive
? inc
: 0) + 7) % 7]
9837 out
.add(inc
, 'days');
9843 // Returns the date range of the full days the given range visually appears to occupy.
9844 // Returns a new range object.
9845 computeDayRange: function(range
) {
9846 var startDay
= range
.start
.clone().stripTime(); // the beginning of the day the range starts
9847 var end
= range
.end
;
9852 endDay
= end
.clone().stripTime(); // the beginning of the day the range exclusively ends
9853 endTimeMS
= +end
.time(); // # of milliseconds into `endDay`
9855 // If the end time is actually inclusively part of the next day and is equal to or
9856 // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
9857 // Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
9858 if (endTimeMS
&& endTimeMS
>= this.nextDayThreshold
) {
9859 endDay
.add(1, 'days');
9863 // If no end was specified, or if it is within `startDay` but not past nextDayThreshold,
9864 // assign the default duration of one day.
9865 if (!end
|| endDay
<= startDay
) {
9866 endDay
= startDay
.clone().add(1, 'days');
9869 return { start
: startDay
, end
: endDay
};
9873 // Does the given event visually appear to occupy more than one day?
9874 isMultiDayEvent: function(event
) {
9875 var range
= this.computeDayRange(event
); // event is range-ish
9877 return range
.end
.diff(range
.start
, 'days') > 1;
9885 Embodies a div that has potential scrollbars
9887 var Scroller
= FC
.Scroller
= Class
.extend({
9889 el
: null, // the guaranteed outer element
9890 scrollEl
: null, // the element with the scrollbars
9895 constructor: function(options
) {
9896 options
= options
|| {};
9897 this.overflowX
= options
.overflowX
|| options
.overflow
|| 'auto';
9898 this.overflowY
= options
.overflowY
|| options
.overflow
|| 'auto';
9902 render: function() {
9903 this.el
= this.renderEl();
9904 this.applyOverflow();
9908 renderEl: function() {
9909 return (this.scrollEl
= $('<div class="fc-scroller"></div>'));
9913 // sets to natural height, unlocks overflow
9915 this.setHeight('auto');
9916 this.applyOverflow();
9920 destroy: function() {
9926 // -----------------------------------------------------------------------------------------------------------------
9929 applyOverflow: function() {
9931 'overflow-x': this.overflowX
,
9932 'overflow-y': this.overflowY
9937 // Causes any 'auto' overflow values to resolves to 'scroll' or 'hidden'.
9938 // Useful for preserving scrollbar widths regardless of future resizes.
9939 // Can pass in scrollbarWidths for optimization.
9940 lockOverflow: function(scrollbarWidths
) {
9941 var overflowX
= this.overflowX
;
9942 var overflowY
= this.overflowY
;
9944 scrollbarWidths
= scrollbarWidths
|| this.getScrollbarWidths();
9946 if (overflowX
=== 'auto') {
9948 scrollbarWidths
.top
|| scrollbarWidths
.bottom
|| // horizontal scrollbars?
9949 // OR scrolling pane with massless scrollbars?
9950 this.scrollEl
[0].scrollWidth
- 1 > this.scrollEl
[0].clientWidth
9951 // subtract 1 because of IE off-by-one issue
9952 ) ? 'scroll' : 'hidden';
9955 if (overflowY
=== 'auto') {
9957 scrollbarWidths
.left
|| scrollbarWidths
.right
|| // vertical scrollbars?
9958 // OR scrolling pane with massless scrollbars?
9959 this.scrollEl
[0].scrollHeight
- 1 > this.scrollEl
[0].clientHeight
9960 // subtract 1 because of IE off-by-one issue
9961 ) ? 'scroll' : 'hidden';
9964 this.scrollEl
.css({ 'overflow-x': overflowX
, 'overflow-y': overflowY
});
9968 // Getters / Setters
9969 // -----------------------------------------------------------------------------------------------------------------
9972 setHeight: function(height
) {
9973 this.scrollEl
.height(height
);
9977 getScrollTop: function() {
9978 return this.scrollEl
.scrollTop();
9982 setScrollTop: function(top
) {
9983 this.scrollEl
.scrollTop(top
);
9987 getClientWidth: function() {
9988 return this.scrollEl
[0].clientWidth
;
9992 getClientHeight: function() {
9993 return this.scrollEl
[0].clientHeight
;
9997 getScrollbarWidths: function() {
9998 return getScrollbarWidths(this.scrollEl
);
10004 function Iterator(items
) {
10005 this.items
= items
|| [];
10009 /* Calls a method on every item passing the arguments through */
10010 Iterator
.prototype.proxyCall = function(methodName
) {
10011 var args
= Array
.prototype.slice
.call(arguments
, 1);
10014 this.items
.forEach(function(item
) {
10015 results
.push(item
[methodName
].apply(item
, args
));
10023 /* Toolbar with buttons and title
10024 ----------------------------------------------------------------------------------------------------------------------*/
10026 function Toolbar(calendar
, toolbarOptions
) {
10030 t
.setToolbarOptions
= setToolbarOptions
;
10032 t
.removeElement
= removeElement
;
10033 t
.updateTitle
= updateTitle
;
10034 t
.activateButton
= activateButton
;
10035 t
.deactivateButton
= deactivateButton
;
10036 t
.disableButton
= disableButton
;
10037 t
.enableButton
= enableButton
;
10038 t
.getViewsWithButtons
= getViewsWithButtons
;
10039 t
.el
= null; // mirrors local `el`
10043 var viewsWithButtons
= [];
10046 // method to update toolbar-specific options, not calendar-wide options
10047 function setToolbarOptions(newToolbarOptions
) {
10048 toolbarOptions
= newToolbarOptions
;
10051 // can be called repeatedly and will rerender
10052 function render() {
10053 var sections
= toolbarOptions
.layout
;
10055 tm
= calendar
.options
.theme
? 'ui' : 'fc';
10059 el
= this.el
= $("<div class='fc-toolbar "+ toolbarOptions
.extraClasses
+ "'/>");
10064 el
.append(renderSection('left'))
10065 .append(renderSection('right'))
10066 .append(renderSection('center'))
10067 .append('<div class="fc-clear"/>');
10075 function removeElement() {
10083 function renderSection(position
) {
10084 var sectionEl
= $('<div class="fc-' + position
+ '"/>');
10085 var buttonStr
= toolbarOptions
.layout
[position
];
10088 $.each(buttonStr
.split(' '), function(i
) {
10089 var groupChildren
= $();
10090 var isOnlyButtons
= true;
10093 $.each(this.split(','), function(j
, buttonName
) {
10094 var customButtonProps
;
10097 var overrideText
; // text explicitly set by calendar's constructor options. overcomes icons
10103 var button
; // the element
10105 if (buttonName
== 'title') {
10106 groupChildren
= groupChildren
.add($('<h2> </h2>')); // we always want it to take up height
10107 isOnlyButtons
= false;
10110 if ((customButtonProps
= (calendar
.options
.customButtons
|| {})[buttonName
])) {
10111 buttonClick = function(ev
) {
10112 if (customButtonProps
.click
) {
10113 customButtonProps
.click
.call(button
[0], ev
);
10116 overrideText
= ''; // icons will override text
10117 defaultText
= customButtonProps
.text
;
10119 else if ((viewSpec
= calendar
.getViewSpec(buttonName
))) {
10120 buttonClick = function() {
10121 calendar
.changeView(buttonName
);
10123 viewsWithButtons
.push(buttonName
);
10124 overrideText
= viewSpec
.buttonTextOverride
;
10125 defaultText
= viewSpec
.buttonTextDefault
;
10127 else if (calendar
[buttonName
]) { // a calendar method
10128 buttonClick = function() {
10129 calendar
[buttonName
]();
10131 overrideText
= (calendar
.overrides
.buttonText
|| {})[buttonName
];
10132 defaultText
= calendar
.options
.buttonText
[buttonName
]; // everything else is considered default
10138 customButtonProps
?
10139 customButtonProps
.themeIcon
:
10140 calendar
.options
.themeButtonIcons
[buttonName
];
10143 customButtonProps
?
10144 customButtonProps
.icon
:
10145 calendar
.options
.buttonIcons
[buttonName
];
10147 if (overrideText
) {
10148 innerHtml
= htmlEscape(overrideText
);
10150 else if (themeIcon
&& calendar
.options
.theme
) {
10151 innerHtml
= "<span class='ui-icon ui-icon-" + themeIcon
+ "'></span>";
10153 else if (normalIcon
&& !calendar
.options
.theme
) {
10154 innerHtml
= "<span class='fc-icon fc-icon-" + normalIcon
+ "'></span>";
10157 innerHtml
= htmlEscape(defaultText
);
10161 'fc-' + buttonName
+ '-button',
10163 tm
+ '-state-default'
10166 button
= $( // type="button" so that it doesn't submit a form
10167 '<button type="button" class="' + classes
.join(' ') + '">' +
10171 .click(function(ev
) {
10172 // don't process clicks for disabled buttons
10173 if (!button
.hasClass(tm
+ '-state-disabled')) {
10177 // after the click action, if the button becomes the "active" tab, or disabled,
10178 // it should never have a hover class, so remove it now.
10180 button
.hasClass(tm
+ '-state-active') ||
10181 button
.hasClass(tm
+ '-state-disabled')
10183 button
.removeClass(tm
+ '-state-hover');
10187 .mousedown(function() {
10188 // the *down* effect (mouse pressed in).
10189 // only on buttons that are not the "active" tab, or disabled
10191 .not('.' + tm
+ '-state-active')
10192 .not('.' + tm
+ '-state-disabled')
10193 .addClass(tm
+ '-state-down');
10195 .mouseup(function() {
10196 // undo the *down* effect
10197 button
.removeClass(tm
+ '-state-down');
10201 // the *hover* effect.
10202 // only on buttons that are not the "active" tab, or disabled
10204 .not('.' + tm
+ '-state-active')
10205 .not('.' + tm
+ '-state-disabled')
10206 .addClass(tm
+ '-state-hover');
10209 // undo the *hover* effect
10211 .removeClass(tm
+ '-state-hover')
10212 .removeClass(tm
+ '-state-down'); // if mouseleave happens before mouseup
10216 groupChildren
= groupChildren
.add(button
);
10221 if (isOnlyButtons
) {
10223 .first().addClass(tm
+ '-corner-left').end()
10224 .last().addClass(tm
+ '-corner-right').end();
10227 if (groupChildren
.length
> 1) {
10228 groupEl
= $('<div/>');
10229 if (isOnlyButtons
) {
10230 groupEl
.addClass('fc-button-group');
10232 groupEl
.append(groupChildren
);
10233 sectionEl
.append(groupEl
);
10236 sectionEl
.append(groupChildren
); // 1 or 0 children
10245 function updateTitle(text
) {
10247 el
.find('h2').text(text
);
10252 function activateButton(buttonName
) {
10254 el
.find('.fc-' + buttonName
+ '-button')
10255 .addClass(tm
+ '-state-active');
10260 function deactivateButton(buttonName
) {
10262 el
.find('.fc-' + buttonName
+ '-button')
10263 .removeClass(tm
+ '-state-active');
10268 function disableButton(buttonName
) {
10270 el
.find('.fc-' + buttonName
+ '-button')
10271 .prop('disabled', true)
10272 .addClass(tm
+ '-state-disabled');
10277 function enableButton(buttonName
) {
10279 el
.find('.fc-' + buttonName
+ '-button')
10280 .prop('disabled', false)
10281 .removeClass(tm
+ '-state-disabled');
10286 function getViewsWithButtons() {
10287 return viewsWithButtons
;
10294 var Calendar
= FC
.Calendar
= Class
.extend({
10296 dirDefaults
: null, // option defaults related to LTR or RTL
10297 localeDefaults
: null, // option defaults related to current locale
10298 overrides
: null, // option overrides given to the fullCalendar constructor
10299 dynamicOverrides
: null, // options set with dynamic setter method. higher precedence than view overrides.
10300 options
: null, // all defaults combined with overrides
10301 viewSpecCache
: null, // cache of view definitions
10302 view
: null, // current View object
10305 loadingLevel
: 0, // number of simultaneous loading tasks
10308 // a lot of this class' OOP logic is scoped within this constructor function,
10309 // but in the future, write individual methods on the prototype.
10310 constructor: Calendar_constructor
,
10313 // Subclasses can override this for initialization logic after the constructor has been called
10314 initialize: function() {
10318 // Computes the flattened options hash for the calendar and assigns to `this.options`.
10319 // Assumes this.overrides and this.dynamicOverrides have already been initialized.
10320 populateOptionsHash: function() {
10321 var locale
, localeDefaults
;
10322 var isRTL
, dirDefaults
;
10324 locale
= firstDefined( // explicit locale option given?
10325 this.dynamicOverrides
.locale
,
10326 this.overrides
.locale
10328 localeDefaults
= localeOptionHash
[locale
];
10329 if (!localeDefaults
) { // explicit locale option not given or invalid?
10330 locale
= Calendar
.defaults
.locale
;
10331 localeDefaults
= localeOptionHash
[locale
] || {};
10334 isRTL
= firstDefined( // based on options computed so far, is direction RTL?
10335 this.dynamicOverrides
.isRTL
,
10336 this.overrides
.isRTL
,
10337 localeDefaults
.isRTL
,
10338 Calendar
.defaults
.isRTL
10340 dirDefaults
= isRTL
? Calendar
.rtlDefaults
: {};
10342 this.dirDefaults
= dirDefaults
;
10343 this.localeDefaults
= localeDefaults
;
10344 this.options
= mergeOptions([ // merge defaults and overrides. lowest to highest precedence
10345 Calendar
.defaults
, // global defaults
10349 this.dynamicOverrides
10351 populateInstanceComputableOptions(this.options
); // fill in gaps with computed options
10355 // Gets information about how to create a view. Will use a cache.
10356 getViewSpec: function(viewType
) {
10357 var cache
= this.viewSpecCache
;
10359 return cache
[viewType
] || (cache
[viewType
] = this.buildViewSpec(viewType
));
10363 // Given a duration singular unit, like "week" or "day", finds a matching view spec.
10364 // Preference is given to views that have corresponding buttons.
10365 getUnitViewSpec: function(unit
) {
10370 if ($.inArray(unit
, intervalUnits
) != -1) {
10372 // put views that have buttons first. there will be duplicates, but oh well
10373 viewTypes
= this.header
.getViewsWithButtons(); // TODO: include footer as well?
10374 $.each(FC
.views
, function(viewType
) { // all views
10375 viewTypes
.push(viewType
);
10378 for (i
= 0; i
< viewTypes
.length
; i
++) {
10379 spec
= this.getViewSpec(viewTypes
[i
]);
10381 if (spec
.singleUnit
== unit
) {
10390 // Builds an object with information on how to create a given view
10391 buildViewSpec: function(requestedViewType
) {
10392 var viewOverrides
= this.overrides
.views
|| {};
10393 var specChain
= []; // for the view. lowest to highest priority
10394 var defaultsChain
= []; // for the view. lowest to highest priority
10395 var overridesChain
= []; // for the view. lowest to highest priority
10396 var viewType
= requestedViewType
;
10397 var spec
; // for the view
10398 var overrides
; // for the view
10402 // iterate from the specific view definition to a more general one until we hit an actual View class
10404 spec
= fcViews
[viewType
];
10405 overrides
= viewOverrides
[viewType
];
10406 viewType
= null; // clear. might repopulate for another iteration
10408 if (typeof spec
=== 'function') { // TODO: deprecate
10409 spec
= { 'class': spec
};
10413 specChain
.unshift(spec
);
10414 defaultsChain
.unshift(spec
.defaults
|| {});
10415 duration
= duration
|| spec
.duration
;
10416 viewType
= viewType
|| spec
.type
;
10420 overridesChain
.unshift(overrides
); // view-specific option hashes have options at zero-level
10421 duration
= duration
|| overrides
.duration
;
10422 viewType
= viewType
|| overrides
.type
;
10426 spec
= mergeProps(specChain
);
10427 spec
.type
= requestedViewType
;
10428 if (!spec
['class']) {
10433 duration
= moment
.duration(duration
);
10434 if (duration
.valueOf()) { // valid?
10435 spec
.duration
= duration
;
10436 unit
= computeIntervalUnit(duration
);
10438 // view is a single-unit duration, like "week" or "day"
10439 // incorporate options for this. lowest priority
10440 if (duration
.as(unit
) === 1) {
10441 spec
.singleUnit
= unit
;
10442 overridesChain
.unshift(viewOverrides
[unit
] || {});
10447 spec
.defaults
= mergeOptions(defaultsChain
);
10448 spec
.overrides
= mergeOptions(overridesChain
);
10450 this.buildViewSpecOptions(spec
);
10451 this.buildViewSpecButtonText(spec
, requestedViewType
);
10457 // Builds and assigns a view spec's options object from its already-assigned defaults and overrides
10458 buildViewSpecOptions: function(spec
) {
10459 spec
.options
= mergeOptions([ // lowest to highest priority
10460 Calendar
.defaults
, // global defaults
10461 spec
.defaults
, // view's defaults (from ViewSubclass.defaults)
10463 this.localeDefaults
, // locale and dir take precedence over view's defaults!
10464 this.overrides
, // calendar's overrides (options given to constructor)
10465 spec
.overrides
, // view's overrides (view-specific options)
10466 this.dynamicOverrides
// dynamically set via setter. highest precedence
10468 populateInstanceComputableOptions(spec
.options
);
10472 // Computes and assigns a view spec's buttonText-related options
10473 buildViewSpecButtonText: function(spec
, requestedViewType
) {
10475 // given an options object with a possible `buttonText` hash, lookup the buttonText for the
10476 // requested view, falling back to a generic unit entry like "week" or "day"
10477 function queryButtonText(options
) {
10478 var buttonText
= options
.buttonText
|| {};
10479 return buttonText
[requestedViewType
] ||
10480 // view can decide to look up a certain key
10481 (spec
.buttonTextKey
? buttonText
[spec
.buttonTextKey
] : null) ||
10482 // a key like "month"
10483 (spec
.singleUnit
? buttonText
[spec
.singleUnit
] : null);
10486 // highest to lowest priority
10487 spec
.buttonTextOverride
=
10488 queryButtonText(this.dynamicOverrides
) ||
10489 queryButtonText(this.overrides
) || // constructor-specified buttonText lookup hash takes precedence
10490 spec
.overrides
.buttonText
; // `buttonText` for view-specific options is a string
10492 // highest to lowest priority. mirrors buildViewSpecOptions
10493 spec
.buttonTextDefault
=
10494 queryButtonText(this.localeDefaults
) ||
10495 queryButtonText(this.dirDefaults
) ||
10496 spec
.defaults
.buttonText
|| // a single string. from ViewSubclass.defaults
10497 queryButtonText(Calendar
.defaults
) ||
10498 (spec
.duration
? this.humanizeDuration(spec
.duration
) : null) || // like "3 days"
10499 requestedViewType
; // fall back to given view name
10503 // Given a view name for a custom view or a standard view, creates a ready-to-go View object
10504 instantiateView: function(viewType
) {
10505 var spec
= this.getViewSpec(viewType
);
10507 return new spec
['class'](this, viewType
, spec
.options
, spec
.duration
);
10511 // Returns a boolean about whether the view is okay to instantiate at some point
10512 isValidViewType: function(viewType
) {
10513 return Boolean(this.getViewSpec(viewType
));
10517 // Should be called when any type of async data fetching begins
10518 pushLoading: function() {
10519 if (!(this.loadingLevel
++)) {
10520 this.publiclyTrigger('loading', null, true, this.view
);
10525 // Should be called when any type of async data fetching completes
10526 popLoading: function() {
10527 if (!(--this.loadingLevel
)) {
10528 this.publiclyTrigger('loading', null, false, this.view
);
10533 // Given arguments to the select method in the API, returns a span (unzoned start/end and other info)
10534 buildSelectSpan: function(zonedStartInput
, zonedEndInput
) {
10535 var start
= this.moment(zonedStartInput
).stripZone();
10538 if (zonedEndInput
) {
10539 end
= this.moment(zonedEndInput
).stripZone();
10541 else if (start
.hasTime()) {
10542 end
= start
.clone().add(this.defaultTimedEventDuration
);
10545 end
= start
.clone().add(this.defaultAllDayEventDuration
);
10548 return { start
: start
, end
: end
};
10554 Calendar
.mixin(EmitterMixin
);
10557 function Calendar_constructor(element
, overrides
) {
10560 // declare the current calendar instance relies on GlobalEmitter. needed for garbage collection.
10561 GlobalEmitter
.needed();
10565 // -----------------------------------------------------------------------------------
10568 t
.destroy
= destroy
;
10569 t
.rerenderEvents
= rerenderEvents
;
10570 t
.changeView
= renderView
; // `renderView` will switch to another view
10572 t
.unselect
= unselect
;
10575 t
.prevYear
= prevYear
;
10576 t
.nextYear
= nextYear
;
10578 t
.gotoDate
= gotoDate
;
10579 t
.incrementDate
= incrementDate
;
10581 t
.getDate
= getDate
;
10582 t
.getCalendar
= getCalendar
;
10583 t
.getView
= getView
;
10584 t
.option
= option
; // getter/setter method
10585 t
.publiclyTrigger
= publiclyTrigger
;
10589 // -----------------------------------------------------------------------------------
10591 t
.dynamicOverrides
= {};
10592 t
.viewSpecCache
= {};
10593 t
.optionHandlers
= {}; // for Calendar.options.js
10594 t
.overrides
= $.extend({}, overrides
); // make a copy
10596 t
.populateOptionsHash(); // sets this.options
10600 // Locale-data Internals
10601 // -----------------------------------------------------------------------------------
10602 // Apply overrides to the current locale's data
10606 // Called immediately, and when any of the options change.
10607 // Happens before any internal objects rebuild or rerender, because this is very core.
10609 'locale', 'monthNames', 'monthNamesShort', 'dayNames', 'dayNamesShort', 'firstDay', 'weekNumberCalculation'
10610 ], function(locale
, monthNames
, monthNamesShort
, dayNames
, dayNamesShort
, firstDay
, weekNumberCalculation
) {
10613 if (weekNumberCalculation
=== 'iso') {
10614 weekNumberCalculation
= 'ISO'; // normalize
10617 localeData
= createObject( // make a cheap copy
10618 getMomentLocaleData(locale
) // will fall back to en
10622 localeData
._months
= monthNames
;
10624 if (monthNamesShort
) {
10625 localeData
._monthsShort
= monthNamesShort
;
10628 localeData
._weekdays
= dayNames
;
10630 if (dayNamesShort
) {
10631 localeData
._weekdaysShort
= dayNamesShort
;
10634 if (firstDay
== null && weekNumberCalculation
=== 'ISO') {
10637 if (firstDay
!= null) {
10638 var _week
= createObject(localeData
._week
); // _week: { dow: # }
10639 _week
.dow
= firstDay
;
10640 localeData
._week
= _week
;
10643 if ( // whitelist certain kinds of input
10644 weekNumberCalculation
=== 'ISO' ||
10645 weekNumberCalculation
=== 'local' ||
10646 typeof weekNumberCalculation
=== 'function'
10648 localeData
._fullCalendar_weekCalc
= weekNumberCalculation
; // moment-ext will know what to do with it
10651 // If the internal current date object already exists, move to new locale.
10652 // We do NOT need to do this technique for event dates, because this happens when converting to "segments".
10654 localizeMoment(date
); // sets to localeData
10659 // Calendar-specific Date Utilities
10660 // -----------------------------------------------------------------------------------
10663 t
.defaultAllDayEventDuration
= moment
.duration(t
.options
.defaultAllDayEventDuration
);
10664 t
.defaultTimedEventDuration
= moment
.duration(t
.options
.defaultTimedEventDuration
);
10667 // Builds a moment using the settings of the current calendar: timezone and locale.
10668 // Accepts anything the vanilla moment() constructor accepts.
10669 t
.moment = function() {
10672 if (t
.options
.timezone
=== 'local') {
10673 mom
= FC
.moment
.apply(null, arguments
);
10675 // Force the moment to be local, because FC.moment doesn't guarantee it.
10676 if (mom
.hasTime()) { // don't give ambiguously-timed moments a local zone
10680 else if (t
.options
.timezone
=== 'UTC') {
10681 mom
= FC
.moment
.utc
.apply(null, arguments
); // process as UTC
10684 mom
= FC
.moment
.parseZone
.apply(null, arguments
); // let the input decide the zone
10687 localizeMoment(mom
);
10693 // Updates the given moment's locale settings to the current calendar locale settings.
10694 function localizeMoment(mom
) {
10695 mom
._locale
= localeData
;
10697 t
.localizeMoment
= localizeMoment
;
10700 // Returns a boolean about whether or not the calendar knows how to calculate
10701 // the timezone offset of arbitrary dates in the current timezone.
10702 t
.getIsAmbigTimezone = function() {
10703 return t
.options
.timezone
!== 'local' && t
.options
.timezone
!== 'UTC';
10707 // Returns a copy of the given date in the current timezone. Has no effect on dates without times.
10708 t
.applyTimezone = function(date
) {
10709 if (!date
.hasTime()) {
10710 return date
.clone();
10713 var zonedDate
= t
.moment(date
.toArray());
10714 var timeAdjust
= date
.time() - zonedDate
.time();
10715 var adjustedZonedDate
;
10717 // Safari sometimes has problems with this coersion when near DST. Adjust if necessary. (bug #2396)
10718 if (timeAdjust
) { // is the time result different than expected?
10719 adjustedZonedDate
= zonedDate
.clone().add(timeAdjust
); // add milliseconds
10720 if (date
.time() - adjustedZonedDate
.time() === 0) { // does it match perfectly now?
10721 zonedDate
= adjustedZonedDate
;
10729 // Returns a moment for the current date, as defined by the client's computer or from the `now` option.
10730 // Will return an moment with an ambiguous timezone.
10731 t
.getNow = function() {
10732 var now
= t
.options
.now
;
10733 if (typeof now
=== 'function') {
10736 return t
.moment(now
).stripZone();
10740 // Get an event's normalized end date. If not present, calculate it from the defaults.
10741 t
.getEventEnd = function(event
) {
10743 return event
.end
.clone();
10746 return t
.getDefaultEventEnd(event
.allDay
, event
.start
);
10751 // Given an event's allDay status and start date, return what its fallback end date should be.
10752 // TODO: rename to computeDefaultEventEnd
10753 t
.getDefaultEventEnd = function(allDay
, zonedStart
) {
10754 var end
= zonedStart
.clone();
10757 end
.stripTime().add(t
.defaultAllDayEventDuration
);
10760 end
.add(t
.defaultTimedEventDuration
);
10763 if (t
.getIsAmbigTimezone()) {
10764 end
.stripZone(); // we don't know what the tzo should be
10771 // Produces a human-readable string for the given duration.
10772 // Side-effect: changes the locale of the given duration.
10773 t
.humanizeDuration = function(duration
) {
10774 return duration
.locale(t
.options
.locale
).humanize();
10780 // -----------------------------------------------------------------------------------
10783 EventManager
.call(t
);
10788 // -----------------------------------------------------------------------------------
10791 var _element
= element
[0];
10792 var toolbarsManager
;
10796 var tm
; // for making theme classes
10797 var currentView
; // NOTE: keep this in sync with this.view
10798 var viewsByType
= {}; // holds all instantiated view instances, current or not
10799 var suggestedViewHeight
;
10800 var windowResizeProxy
; // wraps the windowResize function
10801 var ignoreWindowResize
= 0;
10802 var date
; // unzoned
10807 // -----------------------------------------------------------------------------------
10810 // compute the initial ambig-timezone date
10811 if (t
.options
.defaultDate
!= null) {
10812 date
= t
.moment(t
.options
.defaultDate
).stripZone();
10815 date
= t
.getNow(); // getNow already returns unzoned
10819 function render() {
10823 else if (elementVisible()) {
10824 // mainly for the public API
10831 function initialRender() {
10832 element
.addClass('fc');
10834 // event delegation for nav links
10835 element
.on('click.fc', 'a[data-goto]', function(ev
) {
10836 var anchorEl
= $(this);
10837 var gotoOptions
= anchorEl
.data('goto'); // will automatically parse JSON
10838 var date
= t
.moment(gotoOptions
.date
);
10839 var viewType
= gotoOptions
.type
;
10841 // property like "navLinkDayClick". might be a string or a function
10842 var customAction
= currentView
.opt('navLink' + capitaliseFirstLetter(viewType
) + 'Click');
10844 if (typeof customAction
=== 'function') {
10845 customAction(date
, ev
);
10848 if (typeof customAction
=== 'string') {
10849 viewType
= customAction
;
10851 zoomTo(date
, viewType
);
10855 // called immediately, and upon option change
10856 t
.bindOption('theme', function(theme
) {
10857 tm
= theme
? 'ui' : 'fc'; // affects a larger scope
10858 element
.toggleClass('ui-widget', theme
);
10859 element
.toggleClass('fc-unthemed', !theme
);
10862 // called immediately, and upon option change.
10863 // HACK: locale often affects isRTL, so we explicitly listen to that too.
10864 t
.bindOptions([ 'isRTL', 'locale' ], function(isRTL
) {
10865 element
.toggleClass('fc-ltr', !isRTL
);
10866 element
.toggleClass('fc-rtl', isRTL
);
10869 content
= $("<div class='fc-view-container'/>").prependTo(element
);
10871 var toolbars
= buildToolbars();
10872 toolbarsManager
= new Iterator(toolbars
);
10874 header
= t
.header
= toolbars
[0];
10875 footer
= t
.footer
= toolbars
[1];
10879 renderView(t
.options
.defaultView
);
10881 if (t
.options
.handleWindowResize
) {
10882 windowResizeProxy
= debounce(windowResize
, t
.options
.windowResizeDelay
); // prevents rapid calls
10883 $(window
).resize(windowResizeProxy
);
10888 function destroy() {
10891 currentView
.removeElement();
10893 // NOTE: don't null-out currentView/t.view in case API methods are called after destroy.
10894 // It is still the "current" view, just not rendered.
10897 toolbarsManager
.proxyCall('removeElement');
10899 element
.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget');
10901 element
.off('.fc'); // unbind nav link handlers
10903 if (windowResizeProxy
) {
10904 $(window
).unbind('resize', windowResizeProxy
);
10907 GlobalEmitter
.unneeded();
10911 function elementVisible() {
10912 return element
.is(':visible');
10918 // -----------------------------------------------------------------------------------
10921 // Renders a view because of a date change, view-type change, or for the first time.
10922 // If not given a viewType, keep the current view but render different dates.
10923 // Accepts an optional scroll state to restore to.
10924 function renderView(viewType
, forcedScroll
) {
10925 ignoreWindowResize
++;
10927 var needsClearView
= currentView
&& viewType
&& currentView
.type
!== viewType
;
10929 // if viewType is changing, remove the old view's rendering
10930 if (needsClearView
) {
10931 freezeContentHeight(); // prevent a scroll jump when view element is removed
10935 // if viewType changed, or the view was never created, create a fresh view
10936 if (!currentView
&& viewType
) {
10937 currentView
= t
.view
=
10938 viewsByType
[viewType
] ||
10939 (viewsByType
[viewType
] = t
.instantiateView(viewType
));
10941 currentView
.setElement(
10942 $("<div class='fc-view fc-" + viewType
+ "-view' />").appendTo(content
)
10944 toolbarsManager
.proxyCall('activateButton', viewType
);
10949 // in case the view should render a period of time that is completely hidden
10950 date
= currentView
.massageCurrentDate(date
);
10952 // render or rerender the view
10954 !currentView
.isDateSet
||
10955 !( // NOT within interval range signals an implicit date window change
10956 date
>= currentView
.intervalStart
&&
10957 date
< currentView
.intervalEnd
10960 if (elementVisible()) {
10962 if (forcedScroll
) {
10963 currentView
.captureInitialScroll(forcedScroll
);
10966 currentView
.setDate(date
, forcedScroll
);
10968 if (forcedScroll
) {
10969 currentView
.releaseScroll();
10972 // need to do this after View::render, so dates are calculated
10973 // NOTE: view updates title text proactively
10974 updateToolbarsTodayButton();
10979 if (needsClearView
) {
10980 thawContentHeight();
10983 ignoreWindowResize
--;
10987 // Unrenders the current view and reflects this change in the Header.
10988 // Unregsiters the `currentView`, but does not remove from viewByType hash.
10989 function clearView() {
10990 toolbarsManager
.proxyCall('deactivateButton', currentView
.type
);
10991 currentView
.removeElement();
10992 currentView
= t
.view
= null;
10996 // Destroys the view, including the view object. Then, re-instantiates it and renders it.
10997 // Maintains the same scroll state.
10998 // TODO: maintain any other user-manipulated state.
10999 function reinitView() {
11000 ignoreWindowResize
++;
11001 freezeContentHeight();
11003 var viewType
= currentView
.type
;
11004 var scrollState
= currentView
.queryScroll();
11007 renderView(viewType
, scrollState
);
11009 thawContentHeight();
11010 ignoreWindowResize
--;
11016 // -----------------------------------------------------------------------------------
11019 t
.getSuggestedViewHeight = function() {
11020 if (suggestedViewHeight
=== undefined) {
11023 return suggestedViewHeight
;
11027 t
.isHeightAuto = function() {
11028 return t
.options
.contentHeight
=== 'auto' || t
.options
.height
=== 'auto';
11032 function updateSize(shouldRecalc
) {
11033 if (elementVisible()) {
11035 if (shouldRecalc
) {
11039 ignoreWindowResize
++;
11040 currentView
.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto()
11041 ignoreWindowResize
--;
11043 return true; // signal success
11048 function calcSize() {
11049 if (elementVisible()) {
11055 function _calcSize() { // assumes elementVisible
11056 var contentHeightInput
= t
.options
.contentHeight
;
11057 var heightInput
= t
.options
.height
;
11059 if (typeof contentHeightInput
=== 'number') { // exists and not 'auto'
11060 suggestedViewHeight
= contentHeightInput
;
11062 else if (typeof contentHeightInput
=== 'function') { // exists and is a function
11063 suggestedViewHeight
= contentHeightInput();
11065 else if (typeof heightInput
=== 'number') { // exists and not 'auto'
11066 suggestedViewHeight
= heightInput
- queryToolbarsHeight();
11068 else if (typeof heightInput
=== 'function') { // exists and is a function
11069 suggestedViewHeight
= heightInput() - queryToolbarsHeight();
11071 else if (heightInput
=== 'parent') { // set to height of parent element
11072 suggestedViewHeight
= element
.parent().height() - queryToolbarsHeight();
11075 suggestedViewHeight
= Math
.round(content
.width() / Math
.max(t
.options
.aspectRatio
, .5));
11080 function queryToolbarsHeight() {
11081 return toolbarsManager
.items
.reduce(function(accumulator
, toolbar
) {
11082 var toolbarHeight
= toolbar
.el
? toolbar
.el
.outerHeight(true) : 0; // includes margin
11083 return accumulator
+ toolbarHeight
;
11088 function windowResize(ev
) {
11090 !ignoreWindowResize
&&
11091 ev
.target
=== window
&& // so we don't process jqui "resize" events that have bubbled up
11092 currentView
.start
// view has already been rendered
11094 if (updateSize(true)) {
11095 currentView
.publiclyTrigger('windowResize', _element
);
11103 -----------------------------------------------------------------------------*/
11106 function rerenderEvents() { // API method. destroys old events if previously rendered.
11107 if (elementVisible()) {
11108 t
.reportEventChange(); // will re-trasmit events to the view, causing a rerender
11115 -----------------------------------------------------------------------------*/
11118 function buildToolbars() {
11120 new Toolbar(t
, computeHeaderOptions()),
11121 new Toolbar(t
, computeFooterOptions())
11126 function computeHeaderOptions() {
11128 extraClasses
: 'fc-header-toolbar',
11129 layout
: t
.options
.header
11134 function computeFooterOptions() {
11136 extraClasses
: 'fc-footer-toolbar',
11137 layout
: t
.options
.footer
11142 // can be called repeatedly and Header will rerender
11143 function renderHeader() {
11144 header
.setToolbarOptions(computeHeaderOptions());
11147 element
.prepend(header
.el
);
11152 // can be called repeatedly and Footer will rerender
11153 function renderFooter() {
11154 footer
.setToolbarOptions(computeFooterOptions());
11157 element
.append(footer
.el
);
11162 t
.setToolbarsTitle = function(title
) {
11163 toolbarsManager
.proxyCall('updateTitle', title
);
11167 function updateToolbarsTodayButton() {
11168 var now
= t
.getNow();
11169 if (now
>= currentView
.intervalStart
&& now
< currentView
.intervalEnd
) {
11170 toolbarsManager
.proxyCall('disableButton', 'today');
11173 toolbarsManager
.proxyCall('enableButton', 'today');
11180 -----------------------------------------------------------------------------*/
11183 // this public method receives start/end dates in any format, with any timezone
11184 function select(zonedStartInput
, zonedEndInput
) {
11185 currentView
.select(
11186 t
.buildSelectSpan
.apply(t
, arguments
)
11191 function unselect() { // safe to be called before renderView
11193 currentView
.unselect();
11200 -----------------------------------------------------------------------------*/
11204 date
= currentView
.computePrevDate(date
);
11210 date
= currentView
.computeNextDate(date
);
11215 function prevYear() {
11216 date
.add(-1, 'years');
11221 function nextYear() {
11222 date
.add(1, 'years');
11233 function gotoDate(zonedDateInput
) {
11234 date
= t
.moment(zonedDateInput
).stripZone();
11239 function incrementDate(delta
) {
11240 date
.add(moment
.duration(delta
));
11245 // Forces navigation to a view for the given date.
11246 // `viewType` can be a specific view name or a generic one like "week" or "day".
11247 function zoomTo(newDate
, viewType
) {
11250 viewType
= viewType
|| 'day'; // day is default zoom
11251 spec
= t
.getViewSpec(viewType
) || t
.getUnitViewSpec(viewType
);
11253 date
= newDate
.clone();
11254 renderView(spec
? spec
.type
: null);
11258 // for external API
11259 function getDate() {
11260 return t
.applyTimezone(date
); // infuse the calendar's timezone
11265 /* Height "Freezing"
11266 -----------------------------------------------------------------------------*/
11269 t
.freezeContentHeight
= freezeContentHeight
;
11270 t
.thawContentHeight
= thawContentHeight
;
11272 var freezeContentHeightDepth
= 0;
11275 function freezeContentHeight() {
11276 if (!(freezeContentHeightDepth
++)) {
11279 height
: content
.height(),
11286 function thawContentHeight() {
11287 if (!(--freezeContentHeightDepth
)) {
11299 -----------------------------------------------------------------------------*/
11302 function getCalendar() {
11307 function getView() {
11308 return currentView
;
11312 function option(name
, value
) {
11315 if (typeof name
=== 'string') {
11316 if (value
=== undefined) { // getter
11317 return t
.options
[name
];
11319 else { // setter for individual option
11320 newOptionHash
= {};
11321 newOptionHash
[name
] = value
;
11322 setOptions(newOptionHash
);
11325 else if (typeof name
=== 'object') { // compound setter with object input
11331 function setOptions(newOptionHash
) {
11335 for (optionName
in newOptionHash
) {
11336 t
.dynamicOverrides
[optionName
] = newOptionHash
[optionName
];
11339 t
.viewSpecCache
= {}; // the dynamic override invalidates the options in this cache, so just clear it
11340 t
.populateOptionsHash(); // this.options needs to be recomputed after the dynamic override
11342 // trigger handlers after this.options has been updated
11343 for (optionName
in newOptionHash
) {
11344 t
.triggerOptionHandlers(optionName
); // recall bindOption/bindOptions
11348 // special-case handling of single option change.
11349 // if only one option change, `optionName` will be its name.
11350 if (optionCnt
=== 1) {
11351 if (optionName
=== 'height' || optionName
=== 'contentHeight' || optionName
=== 'aspectRatio') {
11352 updateSize(true); // true = allow recalculation of height
11355 else if (optionName
=== 'defaultDate') {
11356 return; // can't change date this way. use gotoDate instead
11358 else if (optionName
=== 'businessHours') {
11360 currentView
.unrenderBusinessHours();
11361 currentView
.renderBusinessHours();
11365 else if (optionName
=== 'timezone') {
11366 t
.rezoneArrayEventSources();
11372 // catch-all. rerender the header and footer and rebuild/rerender the current view
11375 viewsByType
= {}; // even non-current views will be affected by this option change. do before rerender
11380 function publiclyTrigger(name
, thisObj
) {
11381 var args
= Array
.prototype.slice
.call(arguments
, 2);
11383 thisObj
= thisObj
|| _element
;
11384 this.triggerWith(name
, thisObj
, args
); // Emitter's method
11386 if (t
.options
[name
]) {
11387 return t
.options
[name
].apply(thisObj
, args
);
11396 Options binding/triggering system.
11400 // A map of option names to arrays of handler objects. Initialized to {} in Calendar.
11401 // Format for a handler object:
11403 // func // callback function to be called upon change
11404 // names // option names whose values should be given to func
11406 optionHandlers
: null,
11408 // Calls handlerFunc immediately, and when the given option has changed.
11409 // handlerFunc will be given the option value.
11410 bindOption: function(optionName
, handlerFunc
) {
11411 this.bindOptions([ optionName
], handlerFunc
);
11414 // Calls handlerFunc immediately, and when any of the given options change.
11415 // handlerFunc will be given each option value as ordered function arguments.
11416 bindOptions: function(optionNames
, handlerFunc
) {
11417 var handlerObj
= { func
: handlerFunc
, names
: optionNames
};
11420 for (i
= 0; i
< optionNames
.length
; i
++) {
11421 this.registerOptionHandlerObj(optionNames
[i
], handlerObj
);
11424 this.triggerOptionHandlerObj(handlerObj
);
11427 // Puts the given handler object into the internal hash
11428 registerOptionHandlerObj: function(optionName
, handlerObj
) {
11429 (this.optionHandlers
[optionName
] || (this.optionHandlers
[optionName
] = []))
11433 // Reports that the given option has changed, and calls all appropriate handlers.
11434 triggerOptionHandlers: function(optionName
) {
11435 var handlerObjs
= this.optionHandlers
[optionName
] || [];
11438 for (i
= 0; i
< handlerObjs
.length
; i
++) {
11439 this.triggerOptionHandlerObj(handlerObjs
[i
]);
11443 // Calls the callback for a specific handler object, passing in the appropriate arguments.
11444 triggerOptionHandlerObj: function(handlerObj
) {
11445 var optionNames
= handlerObj
.names
;
11446 var optionValues
= [];
11449 for (i
= 0; i
< optionNames
.length
; i
++) {
11450 optionValues
.push(this.options
[optionNames
[i
]]);
11453 handlerObj
.func
.apply(this, optionValues
); // maintain the Calendar's `this` context
11460 Calendar
.defaults
= {
11462 titleRangeSeparator
: ' \u2013 ', // en dash
11463 monthYearFormat
: 'MMMM YYYY', // required for en. other locales rely on datepicker computable option
11465 defaultTimedEventDuration
: '02:00:00',
11466 defaultAllDayEventDuration
: { days
: 1 },
11467 forceEventDuration
: false,
11468 nextDayThreshold
: '09:00:00', // 9am
11471 defaultView
: 'month',
11476 right
: 'today prev,next'
11479 weekNumbers
: false,
11481 weekNumberTitle
: 'W',
11482 weekNumberCalculation
: 'local',
11486 //nowIndicator: false,
11488 scrollTime
: '06:00:00',
11491 lazyFetching
: true,
11492 startParam
: 'start',
11494 timezoneParam
: 'timezone',
11498 //allDayDefault: undefined,
11505 prevYear
: "prev year",
11506 nextYear
: "next year",
11507 year
: 'year', // TODO: locale files need to specify this
11515 prev
: 'left-single-arrow',
11516 next
: 'right-single-arrow',
11517 prevYear
: 'left-double-arrow',
11518 nextYear
: 'right-double-arrow'
11521 allDayText
: 'all-day',
11523 // jquery-ui theming
11525 themeButtonIcons
: {
11526 prev
: 'circle-triangle-w',
11527 next
: 'circle-triangle-e',
11528 prevYear
: 'seek-prev',
11529 nextYear
: 'seek-next'
11532 //eventResizableFromStart: false,
11534 dragRevertDuration
: 500,
11537 //selectable: false,
11538 unselectAuto
: true,
11539 //selectMinDistance: 0,
11543 eventOrder
: 'title',
11544 //eventRenderWait: null,
11547 eventLimitText
: 'more',
11548 eventLimitClick
: 'popover',
11549 dayPopoverFormat
: 'LL',
11551 handleWindowResize
: true,
11552 windowResizeDelay
: 100, // milliseconds before an updateSize happens
11554 longPressDelay
: 1000
11559 Calendar
.englishDefaults
= { // used by locale.js
11560 dayPopoverFormat
: 'dddd, MMMM D'
11564 Calendar
.rtlDefaults
= { // right-to-left defaults
11565 header
: { // TODO: smarter solution (first/center/last ?)
11566 left
: 'next,prev today',
11571 prev
: 'right-single-arrow',
11572 next
: 'left-single-arrow',
11573 prevYear
: 'right-double-arrow',
11574 nextYear
: 'left-double-arrow'
11576 themeButtonIcons
: {
11577 prev
: 'circle-triangle-e',
11578 next
: 'circle-triangle-w',
11579 nextYear
: 'seek-prev',
11580 prevYear
: 'seek-next'
11586 var localeOptionHash
= FC
.locales
= {}; // initialize and expose
11589 // TODO: document the structure and ordering of a FullCalendar locale file
11592 // Initialize jQuery UI datepicker translations while using some of the translations
11593 // Will set this as the default locales for datepicker.
11594 FC
.datepickerLocale = function(localeCode
, dpLocaleCode
, dpOptions
) {
11596 // get the FullCalendar internal option hash for this locale. create if necessary
11597 var fcOptions
= localeOptionHash
[localeCode
] || (localeOptionHash
[localeCode
] = {});
11599 // transfer some simple options from datepicker to fc
11600 fcOptions
.isRTL
= dpOptions
.isRTL
;
11601 fcOptions
.weekNumberTitle
= dpOptions
.weekHeader
;
11603 // compute some more complex options from datepicker
11604 $.each(dpComputableOptions
, function(name
, func
) {
11605 fcOptions
[name
] = func(dpOptions
);
11608 // is jQuery UI Datepicker is on the page?
11609 if ($.datepicker
) {
11611 // Register the locale data.
11612 // FullCalendar and MomentJS use locale codes like "pt-br" but Datepicker
11613 // does it like "pt-BR" or if it doesn't have the locale, maybe just "pt".
11614 // Make an alias so the locale can be referenced either way.
11615 $.datepicker
.regional
[dpLocaleCode
] =
11616 $.datepicker
.regional
[localeCode
] = // alias
11619 // Alias 'en' to the default locale data. Do this every time.
11620 $.datepicker
.regional
.en
= $.datepicker
.regional
[''];
11622 // Set as Datepicker's global defaults.
11623 $.datepicker
.setDefaults(dpOptions
);
11628 // Sets FullCalendar-specific translations. Will set the locales as the global default.
11629 FC
.locale = function(localeCode
, newFcOptions
) {
11633 // get the FullCalendar internal option hash for this locale. create if necessary
11634 fcOptions
= localeOptionHash
[localeCode
] || (localeOptionHash
[localeCode
] = {});
11636 // provided new options for this locales? merge them in
11637 if (newFcOptions
) {
11638 fcOptions
= localeOptionHash
[localeCode
] = mergeOptions([ fcOptions
, newFcOptions
]);
11641 // compute locale options that weren't defined.
11642 // always do this. newFcOptions can be undefined when initializing from i18n file,
11643 // so no way to tell if this is an initialization or a default-setting.
11644 momOptions
= getMomentLocaleData(localeCode
); // will fall back to en
11645 $.each(momComputableOptions
, function(name
, func
) {
11646 if (fcOptions
[name
] == null) {
11647 fcOptions
[name
] = func(momOptions
, fcOptions
);
11651 // set it as the default locale for FullCalendar
11652 Calendar
.defaults
.locale
= localeCode
;
11656 // NOTE: can't guarantee any of these computations will run because not every locale has datepicker
11657 // configs, so make sure there are English fallbacks for these in the defaults file.
11658 var dpComputableOptions
= {
11660 buttonText: function(dpOptions
) {
11662 // the translations sometimes wrongly contain HTML entities
11663 prev
: stripHtmlEntities(dpOptions
.prevText
),
11664 next
: stripHtmlEntities(dpOptions
.nextText
),
11665 today
: stripHtmlEntities(dpOptions
.currentText
)
11669 // Produces format strings like "MMMM YYYY" -> "September 2014"
11670 monthYearFormat: function(dpOptions
) {
11671 return dpOptions
.showMonthAfterYear
?
11672 'YYYY[' + dpOptions
.yearSuffix
+ '] MMMM' :
11673 'MMMM YYYY[' + dpOptions
.yearSuffix
+ ']';
11678 var momComputableOptions
= {
11680 // Produces format strings like "ddd M/D" -> "Fri 9/15"
11681 dayOfMonthFormat: function(momOptions
, fcOptions
) {
11682 var format
= momOptions
.longDateFormat('l'); // for the format like "M/D/YYYY"
11684 // strip the year off the edge, as well as other misc non-whitespace chars
11685 format
= format
.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, '');
11687 if (fcOptions
.isRTL
) {
11688 format
+= ' ddd'; // for RTL, add day-of-week to end
11691 format
= 'ddd ' + format
; // for LTR, add day-of-week to beginning
11696 // Produces format strings like "h:mma" -> "6:00pm"
11697 mediumTimeFormat: function(momOptions
) { // can't be called `timeFormat` because collides with option
11698 return momOptions
.longDateFormat('LT')
11699 .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
11702 // Produces format strings like "h(:mm)a" -> "6pm" / "6:30pm"
11703 smallTimeFormat: function(momOptions
) {
11704 return momOptions
.longDateFormat('LT')
11705 .replace(':mm', '(:mm)')
11706 .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales
11707 .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
11710 // Produces format strings like "h(:mm)t" -> "6p" / "6:30p"
11711 extraSmallTimeFormat: function(momOptions
) {
11712 return momOptions
.longDateFormat('LT')
11713 .replace(':mm', '(:mm)')
11714 .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales
11715 .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand
11718 // Produces format strings like "ha" / "H" -> "6pm" / "18"
11719 hourFormat: function(momOptions
) {
11720 return momOptions
.longDateFormat('LT')
11721 .replace(':mm', '')
11722 .replace(/(\Wmm)$/, '') // like above, but for foreign locales
11723 .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
11726 // Produces format strings like "h:mm" -> "6:30" (with no AM/PM)
11727 noMeridiemTimeFormat: function(momOptions
) {
11728 return momOptions
.longDateFormat('LT')
11729 .replace(/\s*a$/i, ''); // remove trailing AM/PM
11735 // options that should be computed off live calendar options (considers override options)
11736 // TODO: best place for this? related to locale?
11737 // TODO: flipping text based on isRTL is a bad idea because the CSS `direction` might want to handle it
11738 var instanceComputableOptions
= {
11740 // Produces format strings for results like "Mo 16"
11741 smallDayDateFormat: function(options
) {
11742 return options
.isRTL
?
11747 // Produces format strings for results like "Wk 5"
11748 weekFormat: function(options
) {
11749 return options
.isRTL
?
11750 'w[ ' + options
.weekNumberTitle
+ ']' :
11751 '[' + options
.weekNumberTitle
+ ' ]w';
11754 // Produces format strings for results like "Wk5"
11755 smallWeekFormat: function(options
) {
11756 return options
.isRTL
?
11757 'w[' + options
.weekNumberTitle
+ ']' :
11758 '[' + options
.weekNumberTitle
+ ']w';
11763 function populateInstanceComputableOptions(options
) {
11764 $.each(instanceComputableOptions
, function(name
, func
) {
11765 if (options
[name
] == null) {
11766 options
[name
] = func(options
);
11772 // Returns moment's internal locale data. If doesn't exist, returns English.
11773 function getMomentLocaleData(localeCode
) {
11774 return moment
.localeData(localeCode
) || moment
.localeData('en');
11778 // Initialize English by forcing computation of moment-derived options.
11779 // Also, sets it as the default.
11780 FC
.locale('en', Calendar
.englishDefaults
);
11784 FC
.sourceNormalizers
= [];
11785 FC
.sourceFetchers
= [];
11787 var ajaxDefaults
= {
11795 function EventManager() { // assumed to be a calendar
11800 t
.requestEvents
= requestEvents
;
11801 t
.reportEventChange
= reportEventChange
;
11802 t
.isFetchNeeded
= isFetchNeeded
;
11803 t
.fetchEvents
= fetchEvents
;
11804 t
.fetchEventSources
= fetchEventSources
;
11805 t
.refetchEvents
= refetchEvents
;
11806 t
.refetchEventSources
= refetchEventSources
;
11807 t
.getEventSources
= getEventSources
;
11808 t
.getEventSourceById
= getEventSourceById
;
11809 t
.addEventSource
= addEventSource
;
11810 t
.removeEventSource
= removeEventSource
;
11811 t
.removeEventSources
= removeEventSources
;
11812 t
.updateEvent
= updateEvent
;
11813 t
.updateEvents
= updateEvents
;
11814 t
.renderEvent
= renderEvent
;
11815 t
.renderEvents
= renderEvents
;
11816 t
.removeEvents
= removeEvents
;
11817 t
.clientEvents
= clientEvents
;
11818 t
.mutateEvent
= mutateEvent
;
11819 t
.normalizeEventDates
= normalizeEventDates
;
11820 t
.normalizeEventTimes
= normalizeEventTimes
;
11824 var stickySource
= { events
: [] };
11825 var sources
= [ stickySource
];
11826 var rangeStart
, rangeEnd
;
11827 var pendingSourceCnt
= 0; // outstanding fetch requests, max one per source
11828 var cache
= []; // holds events that have already been expanded
11829 var prunedCache
; // like cache, but only events that intersect with rangeStart/rangeEnd
11833 (t
.options
.events
? [ t
.options
.events
] : []).concat(t
.options
.eventSources
|| []),
11834 function(i
, sourceInput
) {
11835 var source
= buildEventSource(sourceInput
);
11837 sources
.push(source
);
11844 function requestEvents(start
, end
) {
11845 if (!t
.options
.lazyFetching
|| isFetchNeeded(start
, end
)) {
11846 return fetchEvents(start
, end
);
11849 return Promise
.resolve(prunedCache
);
11854 function reportEventChange() {
11855 prunedCache
= filterEventsWithinRange(cache
);
11856 t
.trigger('eventsReset', prunedCache
);
11860 function filterEventsWithinRange(events
) {
11861 var filteredEvents
= [];
11864 for (i
= 0; i
< events
.length
; i
++) {
11868 event
.start
.clone().stripZone() < rangeEnd
&&
11869 t
.getEventEnd(event
).stripZone() > rangeStart
11871 filteredEvents
.push(event
);
11875 return filteredEvents
;
11879 t
.getEventCache = function() {
11884 t
.getPrunedEventCache = function() {
11885 return prunedCache
;
11891 -----------------------------------------------------------------------------*/
11894 // start and end are assumed to be unzoned
11895 function isFetchNeeded(start
, end
) {
11896 return !rangeStart
|| // nothing has been fetched yet?
11897 start
< rangeStart
|| end
> rangeEnd
; // is part of the new range outside of the old range?
11901 function fetchEvents(start
, end
) {
11902 rangeStart
= start
;
11904 return refetchEvents();
11908 // poorly named. fetches all sources with current `rangeStart` and `rangeEnd`.
11909 function refetchEvents() {
11910 return fetchEventSources(sources
, 'reset');
11914 // poorly named. fetches a subset of event sources.
11915 function refetchEventSources(matchInputs
) {
11916 return fetchEventSources(getEventSourcesByMatchArray(matchInputs
));
11920 // expects an array of event source objects (the originals, not copies)
11921 // `specialFetchType` is an optimization parameter that affects purging of the event cache.
11922 function fetchEventSources(specificSources
, specialFetchType
) {
11925 if (specialFetchType
=== 'reset') {
11928 else if (specialFetchType
!== 'add') {
11929 cache
= excludeEventsBySources(cache
, specificSources
);
11932 for (i
= 0; i
< specificSources
.length
; i
++) {
11933 source
= specificSources
[i
];
11935 // already-pending sources have already been accounted for in pendingSourceCnt
11936 if (source
._status
!== 'pending') {
11937 pendingSourceCnt
++;
11940 source
._fetchId
= (source
._fetchId
|| 0) + 1;
11941 source
._status
= 'pending';
11944 for (i
= 0; i
< specificSources
.length
; i
++) {
11945 source
= specificSources
[i
];
11946 tryFetchEventSource(source
, source
._fetchId
);
11949 if (pendingSourceCnt
) {
11950 return new Promise(function(resolve
) {
11951 t
.one('eventsReceived', resolve
); // will send prunedCache
11954 else { // executed all synchronously, or no sources at all
11955 return Promise
.resolve(prunedCache
);
11960 // fetches an event source and processes its result ONLY if it is still the current fetch.
11961 // caller is responsible for incrementing pendingSourceCnt first.
11962 function tryFetchEventSource(source
, fetchId
) {
11963 _fetchEventSource(source
, function(eventInputs
) {
11964 var isArraySource
= $.isArray(source
.events
);
11969 // is this the source's most recent fetch?
11970 // if not, rely on an upcoming fetch of this source to decrement pendingSourceCnt
11971 fetchId
=== source
._fetchId
&&
11972 // event source no longer valid?
11973 source
._status
!== 'rejected'
11975 source
._status
= 'resolved';
11978 for (i
= 0; i
< eventInputs
.length
; i
++) {
11979 eventInput
= eventInputs
[i
];
11981 if (isArraySource
) { // array sources have already been convert to Event Objects
11982 abstractEvent
= eventInput
;
11985 abstractEvent
= buildEventFromInput(eventInput
, source
);
11988 if (abstractEvent
) { // not false (an invalid event)
11989 cache
.push
.apply( // append
11991 expandEvent(abstractEvent
) // add individual expanded events to the cache
11997 decrementPendingSourceCnt();
12003 function rejectEventSource(source
) {
12004 var wasPending
= source
._status
=== 'pending';
12006 source
._status
= 'rejected';
12009 decrementPendingSourceCnt();
12014 function decrementPendingSourceCnt() {
12015 pendingSourceCnt
--;
12016 if (!pendingSourceCnt
) {
12017 reportEventChange(cache
); // updates prunedCache
12018 t
.trigger('eventsReceived', prunedCache
);
12023 function _fetchEventSource(source
, callback
) {
12025 var fetchers
= FC
.sourceFetchers
;
12028 for (i
=0; i
<fetchers
.length
; i
++) {
12029 res
= fetchers
[i
].call(
12030 t
, // this, the Calendar object
12032 rangeStart
.clone(),
12034 t
.options
.timezone
,
12038 if (res
=== true) {
12039 // the fetcher is in charge. made its own async request
12042 else if (typeof res
== 'object') {
12043 // the fetcher returned a new source. process it
12044 _fetchEventSource(res
, callback
);
12049 var events
= source
.events
;
12051 if ($.isFunction(events
)) {
12054 t
, // this, the Calendar object
12055 rangeStart
.clone(),
12057 t
.options
.timezone
,
12064 else if ($.isArray(events
)) {
12071 var url
= source
.url
;
12073 var success
= source
.success
;
12074 var error
= source
.error
;
12075 var complete
= source
.complete
;
12077 // retrieve any outbound GET/POST $.ajax data from the options
12079 if ($.isFunction(source
.data
)) {
12080 // supplied as a function that returns a key/value object
12081 customData
= source
.data();
12084 // supplied as a straight key/value object
12085 customData
= source
.data
;
12088 // use a copy of the custom data so we can modify the parameters
12089 // and not affect the passed-in object.
12090 var data
= $.extend({}, customData
|| {});
12092 var startParam
= firstDefined(source
.startParam
, t
.options
.startParam
);
12093 var endParam
= firstDefined(source
.endParam
, t
.options
.endParam
);
12094 var timezoneParam
= firstDefined(source
.timezoneParam
, t
.options
.timezoneParam
);
12097 data
[startParam
] = rangeStart
.format();
12100 data
[endParam
] = rangeEnd
.format();
12102 if (t
.options
.timezone
&& t
.options
.timezone
!= 'local') {
12103 data
[timezoneParam
] = t
.options
.timezone
;
12107 $.ajax($.extend({}, ajaxDefaults
, source
, {
12109 success: function(events
) {
12110 events
= events
|| [];
12111 var res
= applyAll(success
, this, arguments
);
12112 if ($.isArray(res
)) {
12117 error: function() {
12118 applyAll(error
, this, arguments
);
12121 complete: function() {
12122 applyAll(complete
, this, arguments
);
12135 -----------------------------------------------------------------------------*/
12138 function addEventSource(sourceInput
) {
12139 var source
= buildEventSource(sourceInput
);
12141 sources
.push(source
);
12142 fetchEventSources([ source
], 'add'); // will eventually call reportEventChange
12147 function buildEventSource(sourceInput
) { // will return undefined if invalid source
12148 var normalizers
= FC
.sourceNormalizers
;
12152 if ($.isFunction(sourceInput
) || $.isArray(sourceInput
)) {
12153 source
= { events
: sourceInput
};
12155 else if (typeof sourceInput
=== 'string') {
12156 source
= { url
: sourceInput
};
12158 else if (typeof sourceInput
=== 'object') {
12159 source
= $.extend({}, sourceInput
); // shallow copy
12164 // TODO: repeat code, same code for event classNames
12165 if (source
.className
) {
12166 if (typeof source
.className
=== 'string') {
12167 source
.className
= source
.className
.split(/\s+/);
12169 // otherwise, assumed to be an array
12172 source
.className
= [];
12175 // for array sources, we convert to standard Event Objects up front
12176 if ($.isArray(source
.events
)) {
12177 source
.origArray
= source
.events
; // for removeEventSource
12178 source
.events
= $.map(source
.events
, function(eventInput
) {
12179 return buildEventFromInput(eventInput
, source
);
12183 for (i
=0; i
<normalizers
.length
; i
++) {
12184 normalizers
[i
].call(t
, source
);
12192 function removeEventSource(matchInput
) {
12193 removeSpecificEventSources(
12194 getEventSourcesByMatch(matchInput
)
12199 // if called with no arguments, removes all.
12200 function removeEventSources(matchInputs
) {
12201 if (matchInputs
== null) {
12202 removeSpecificEventSources(sources
, true); // isAll=true
12205 removeSpecificEventSources(
12206 getEventSourcesByMatchArray(matchInputs
)
12212 function removeSpecificEventSources(targetSources
, isAll
) {
12215 // cancel pending requests
12216 for (i
= 0; i
< targetSources
.length
; i
++) {
12217 rejectEventSource(targetSources
[i
]);
12220 if (isAll
) { // an optimization
12225 // remove from persisted source list
12226 sources
= $.grep(sources
, function(source
) {
12227 for (i
= 0; i
< targetSources
.length
; i
++) {
12228 if (source
=== targetSources
[i
]) {
12229 return false; // exclude
12232 return true; // include
12235 cache
= excludeEventsBySources(cache
, targetSources
);
12238 reportEventChange();
12242 function getEventSources() {
12243 return sources
.slice(1); // returns a shallow copy of sources with stickySource removed
12247 function getEventSourceById(id
) {
12248 return $.grep(sources
, function(source
) {
12249 return source
.id
&& source
.id
=== id
;
12254 // like getEventSourcesByMatch, but accepts multple match criteria (like multiple IDs)
12255 function getEventSourcesByMatchArray(matchInputs
) {
12257 // coerce into an array
12258 if (!matchInputs
) {
12261 else if (!$.isArray(matchInputs
)) {
12262 matchInputs
= [ matchInputs
];
12265 var matchingSources
= [];
12268 // resolve raw inputs to real event source objects
12269 for (i
= 0; i
< matchInputs
.length
; i
++) {
12270 matchingSources
.push
.apply( // append
12272 getEventSourcesByMatch(matchInputs
[i
])
12276 return matchingSources
;
12280 // matchInput can either by a real event source object, an ID, or the function/URL for the source.
12281 // returns an array of matching source objects.
12282 function getEventSourcesByMatch(matchInput
) {
12285 // given an proper event source object
12286 for (i
= 0; i
< sources
.length
; i
++) {
12287 source
= sources
[i
];
12288 if (source
=== matchInput
) {
12294 source
= getEventSourceById(matchInput
);
12299 return $.grep(sources
, function(source
) {
12300 return isSourcesEquivalent(matchInput
, source
);
12305 function isSourcesEquivalent(source1
, source2
) {
12306 return source1
&& source2
&& getSourcePrimitive(source1
) == getSourcePrimitive(source2
);
12310 function getSourcePrimitive(source
) {
12312 (typeof source
=== 'object') ? // a normalized event source?
12313 (source
.origArray
|| source
.googleCalendarId
|| source
.url
|| source
.events
) : // get the primitive
12316 source
; // the given argument *is* the primitive
12321 // returns a filtered array without events that are part of any of the given sources
12322 function excludeEventsBySources(specificEvents
, specificSources
) {
12323 return $.grep(specificEvents
, function(event
) {
12324 for (var i
= 0; i
< specificSources
.length
; i
++) {
12325 if (event
.source
=== specificSources
[i
]) {
12326 return false; // exclude
12329 return true; // keep
12336 -----------------------------------------------------------------------------*/
12339 // Only ever called from the externally-facing API
12340 function updateEvent(event
) {
12341 updateEvents([ event
]);
12345 // Only ever called from the externally-facing API
12346 function updateEvents(events
) {
12349 for (i
= 0; i
< events
.length
; i
++) {
12352 // massage start/end values, even if date string values
12353 event
.start
= t
.moment(event
.start
);
12355 event
.end
= t
.moment(event
.end
);
12361 mutateEvent(event
, getMiscEventProps(event
)); // will handle start/end/allDay normalization
12364 reportEventChange(); // reports event modifications (so we can redraw)
12368 // Returns a hash of misc event properties that should be copied over to related events.
12369 function getMiscEventProps(event
) {
12372 $.each(event
, function(name
, val
) {
12373 if (isMiscEventPropName(name
)) {
12374 if (val
!== undefined && isAtomic(val
)) { // a defined non-object
12383 // non-date-related, non-id-related, non-secret
12384 function isMiscEventPropName(name
) {
12385 return !/^_|^(id|allDay|start|end)$/.test(name
);
12389 // returns the expanded events that were created
12390 function renderEvent(eventInput
, stick
) {
12391 return renderEvents([ eventInput
], stick
);
12395 // returns the expanded events that were created
12396 function renderEvents(eventInputs
, stick
) {
12397 var renderedEvents
= [];
12398 var renderableEvents
;
12402 for (i
= 0; i
< eventInputs
.length
; i
++) {
12403 abstractEvent
= buildEventFromInput(eventInputs
[i
]);
12405 if (abstractEvent
) { // not false (a valid input)
12406 renderableEvents
= expandEvent(abstractEvent
);
12408 for (j
= 0; j
< renderableEvents
.length
; j
++) {
12409 event
= renderableEvents
[j
];
12411 if (!event
.source
) {
12413 stickySource
.events
.push(event
);
12414 event
.source
= stickySource
;
12420 renderedEvents
= renderedEvents
.concat(renderableEvents
);
12424 if (renderedEvents
.length
) { // any new events rendered?
12425 reportEventChange();
12428 return renderedEvents
;
12432 function removeEvents(filter
) {
12436 if (filter
== null) { // null or undefined. remove all events
12437 filter = function() { return true; }; // will always match
12439 else if (!$.isFunction(filter
)) { // an event ID
12440 eventID
= filter
+ '';
12441 filter = function(event
) {
12442 return event
._id
== eventID
;
12446 // Purge event(s) from our local cache
12447 cache
= $.grep(cache
, filter
, true); // inverse=true
12449 // Remove events from array sources.
12450 // This works because they have been converted to official Event Objects up front.
12451 // (and as a result, event._id has been calculated).
12452 for (i
=0; i
<sources
.length
; i
++) {
12453 if ($.isArray(sources
[i
].events
)) {
12454 sources
[i
].events
= $.grep(sources
[i
].events
, filter
, true);
12458 reportEventChange();
12462 function clientEvents(filter
) {
12463 if ($.isFunction(filter
)) {
12464 return $.grep(cache
, filter
);
12466 else if (filter
!= null) { // not null, not undefined. an event ID
12468 return $.grep(cache
, function(e
) {
12469 return e
._id
== filter
;
12472 return cache
; // else, return all
12476 // Makes sure all array event sources have their internal event objects
12477 // converted over to the Calendar's current timezone.
12478 t
.rezoneArrayEventSources = function() {
12483 for (i
= 0; i
< sources
.length
; i
++) {
12484 events
= sources
[i
].events
;
12485 if ($.isArray(events
)) {
12487 for (j
= 0; j
< events
.length
; j
++) {
12488 rezoneEventDates(events
[j
]);
12494 function rezoneEventDates(event
) {
12495 event
.start
= t
.moment(event
.start
);
12497 event
.end
= t
.moment(event
.end
);
12499 backupEventDates(event
);
12503 /* Event Normalization
12504 -----------------------------------------------------------------------------*/
12507 // Given a raw object with key/value properties, returns an "abstract" Event object.
12508 // An "abstract" event is an event that, if recurring, will not have been expanded yet.
12509 // Will return `false` when input is invalid.
12510 // `source` is optional
12511 function buildEventFromInput(input
, source
) {
12516 if (t
.options
.eventDataTransform
) {
12517 input
= t
.options
.eventDataTransform(input
);
12519 if (source
&& source
.eventDataTransform
) {
12520 input
= source
.eventDataTransform(input
);
12523 // Copy all properties over to the resulting object.
12524 // The special-case properties will be copied over afterwards.
12525 $.extend(out
, input
);
12528 out
.source
= source
;
12531 out
._id
= input
._id
|| (input
.id
=== undefined ? '_fc' + eventGUID
++ : input
.id
+ '');
12533 if (input
.className
) {
12534 if (typeof input
.className
== 'string') {
12535 out
.className
= input
.className
.split(/\s+/);
12537 else { // assumed to be an array
12538 out
.className
= input
.className
;
12542 out
.className
= [];
12545 start
= input
.start
|| input
.date
; // "date" is an alias for "start"
12548 // parse as a time (Duration) if applicable
12549 if (isTimeString(start
)) {
12550 start
= moment
.duration(start
);
12552 if (isTimeString(end
)) {
12553 end
= moment
.duration(end
);
12556 if (input
.dow
|| moment
.isDuration(start
) || moment
.isDuration(end
)) {
12558 // the event is "abstract" (recurring) so don't calculate exact start/end dates just yet
12559 out
.start
= start
? moment
.duration(start
) : null; // will be a Duration or null
12560 out
.end
= end
? moment
.duration(end
) : null; // will be a Duration or null
12561 out
._recurring
= true; // our internal marker
12566 start
= t
.moment(start
);
12567 if (!start
.isValid()) {
12573 end
= t
.moment(end
);
12574 if (!end
.isValid()) {
12575 end
= null; // let defaults take over
12579 allDay
= input
.allDay
;
12580 if (allDay
=== undefined) { // still undefined? fallback to default
12581 allDay
= firstDefined(
12582 source
? source
.allDayDefault
: undefined,
12583 t
.options
.allDayDefault
12585 // still undefined? normalizeEventDates will calculate it
12588 assignDatesToEvent(start
, end
, allDay
, out
);
12591 t
.normalizeEvent(out
); // hook for external use. a prototype method
12595 t
.buildEventFromInput
= buildEventFromInput
;
12598 // Normalizes and assigns the given dates to the given partially-formed event object.
12599 // NOTE: mutates the given start/end moments. does not make a copy.
12600 function assignDatesToEvent(start
, end
, allDay
, event
) {
12601 event
.start
= start
;
12603 event
.allDay
= allDay
;
12604 normalizeEventDates(event
);
12605 backupEventDates(event
);
12609 // Ensures proper values for allDay/start/end. Accepts an Event object, or a plain object with event-ish properties.
12610 // NOTE: Will modify the given object.
12611 function normalizeEventDates(eventProps
) {
12613 normalizeEventTimes(eventProps
);
12615 if (eventProps
.end
&& !eventProps
.end
.isAfter(eventProps
.start
)) {
12616 eventProps
.end
= null;
12619 if (!eventProps
.end
) {
12620 if (t
.options
.forceEventDuration
) {
12621 eventProps
.end
= t
.getDefaultEventEnd(eventProps
.allDay
, eventProps
.start
);
12624 eventProps
.end
= null;
12630 // Ensures the allDay property exists and the timeliness of the start/end dates are consistent
12631 function normalizeEventTimes(eventProps
) {
12632 if (eventProps
.allDay
== null) {
12633 eventProps
.allDay
= !(eventProps
.start
.hasTime() || (eventProps
.end
&& eventProps
.end
.hasTime()));
12636 if (eventProps
.allDay
) {
12637 eventProps
.start
.stripTime();
12638 if (eventProps
.end
) {
12639 // TODO: consider nextDayThreshold here? If so, will require a lot of testing and adjustment
12640 eventProps
.end
.stripTime();
12644 if (!eventProps
.start
.hasTime()) {
12645 eventProps
.start
= t
.applyTimezone(eventProps
.start
.time(0)); // will assign a 00:00 time
12647 if (eventProps
.end
&& !eventProps
.end
.hasTime()) {
12648 eventProps
.end
= t
.applyTimezone(eventProps
.end
.time(0)); // will assign a 00:00 time
12654 // If the given event is a recurring event, break it down into an array of individual instances.
12655 // If not a recurring event, return an array with the single original event.
12656 // If given a falsy input (probably because of a failed buildEventFromInput call), returns an empty array.
12657 // HACK: can override the recurring window by providing custom rangeStart/rangeEnd (for businessHours).
12658 function expandEvent(abstractEvent
, _rangeStart
, _rangeEnd
) {
12664 var startTime
, endTime
;
12668 _rangeStart
= _rangeStart
|| rangeStart
;
12669 _rangeEnd
= _rangeEnd
|| rangeEnd
;
12671 if (abstractEvent
) {
12672 if (abstractEvent
._recurring
) {
12674 // make a boolean hash as to whether the event occurs on each day-of-week
12675 if ((dow
= abstractEvent
.dow
)) {
12677 for (i
= 0; i
< dow
.length
; i
++) {
12678 dowHash
[dow
[i
]] = true;
12682 // iterate through every day in the current range
12683 date
= _rangeStart
.clone().stripTime(); // holds the date of the current day
12684 while (date
.isBefore(_rangeEnd
)) {
12686 if (!dowHash
|| dowHash
[date
.day()]) { // if everyday, or this particular day-of-week
12688 startTime
= abstractEvent
.start
; // the stored start and end properties are times (Durations)
12689 endTime
= abstractEvent
.end
; // "
12690 start
= date
.clone();
12694 start
= start
.time(startTime
);
12697 end
= date
.clone().time(endTime
);
12700 event
= $.extend({}, abstractEvent
); // make a copy of the original
12701 assignDatesToEvent(
12703 !startTime
&& !endTime
, // allDay?
12706 events
.push(event
);
12709 date
.add(1, 'days');
12713 events
.push(abstractEvent
); // return the original event. will be a one-item array
12719 t
.expandEvent
= expandEvent
;
12723 /* Event Modification Math
12724 -----------------------------------------------------------------------------------------*/
12727 // Modifies an event and all related events by applying the given properties.
12728 // Special date-diffing logic is used for manipulation of dates.
12729 // If `props` does not contain start/end dates, the updated values are assumed to be the event's current start/end.
12730 // All date comparisons are done against the event's pristine _start and _end dates.
12731 // Returns an object with delta information and a function to undo all operations.
12732 // For making computations in a granularity greater than day/time, specify largeUnit.
12733 // NOTE: The given `newProps` might be mutated for normalization purposes.
12734 function mutateEvent(event
, newProps
, largeUnit
) {
12735 var miscProps
= {};
12743 // diffs the dates in the appropriate way, returning a duration
12744 function diffDates(date1
, date0
) { // date1 - date0
12746 return diffByUnit(date1
, date0
, largeUnit
);
12748 else if (newProps
.allDay
) {
12749 return diffDay(date1
, date0
);
12752 return diffDayTime(date1
, date0
);
12756 newProps
= newProps
|| {};
12758 // normalize new date-related properties
12759 if (!newProps
.start
) {
12760 newProps
.start
= event
.start
.clone();
12762 if (newProps
.end
=== undefined) {
12763 newProps
.end
= event
.end
? event
.end
.clone() : null;
12765 if (newProps
.allDay
== null) { // is null or undefined?
12766 newProps
.allDay
= event
.allDay
;
12768 normalizeEventDates(newProps
);
12770 // create normalized versions of the original props to compare against
12771 // need a real end value, for diffing
12773 start
: event
._start
.clone(),
12774 end
: event
._end
? event
._end
.clone() : t
.getDefaultEventEnd(event
._allDay
, event
._start
),
12775 allDay
: newProps
.allDay
// normalize the dates in the same regard as the new properties
12777 normalizeEventDates(oldProps
);
12779 // need to clear the end date if explicitly changed to null
12780 clearEnd
= event
._end
!== null && newProps
.end
=== null;
12782 // compute the delta for moving the start date
12783 startDelta
= diffDates(newProps
.start
, oldProps
.start
);
12785 // compute the delta for moving the end date
12786 if (newProps
.end
) {
12787 endDelta
= diffDates(newProps
.end
, oldProps
.end
);
12788 durationDelta
= endDelta
.subtract(startDelta
);
12791 durationDelta
= null;
12794 // gather all non-date-related properties
12795 $.each(newProps
, function(name
, val
) {
12796 if (isMiscEventPropName(name
)) {
12797 if (val
!== undefined) {
12798 miscProps
[name
] = val
;
12803 // apply the operations to the event and all related events
12804 undoFunc
= mutateEvents(
12805 clientEvents(event
._id
), // get events with this ID
12814 dateDelta
: startDelta
,
12815 durationDelta
: durationDelta
,
12821 // Modifies an array of events in the following ways (operations are in order):
12822 // - clear the event's `end`
12823 // - convert the event to allDay
12824 // - add `dateDelta` to the start and end
12825 // - add `durationDelta` to the event's duration
12826 // - assign `miscProps` to the event
12828 // Returns a function that can be called to undo all the operations.
12830 // TODO: don't use so many closures. possible memory issues when lots of events with same ID.
12832 function mutateEvents(events
, clearEnd
, allDay
, dateDelta
, durationDelta
, miscProps
) {
12833 var isAmbigTimezone
= t
.getIsAmbigTimezone();
12834 var undoFunctions
= [];
12836 // normalize zero-length deltas to be null
12837 if (dateDelta
&& !dateDelta
.valueOf()) { dateDelta
= null; }
12838 if (durationDelta
&& !durationDelta
.valueOf()) { durationDelta
= null; }
12840 $.each(events
, function(i
, event
) {
12844 // build an object holding all the old values, both date-related and misc.
12845 // for the undo function.
12847 start
: event
.start
.clone(),
12848 end
: event
.end
? event
.end
.clone() : null,
12849 allDay
: event
.allDay
12851 $.each(miscProps
, function(name
) {
12852 oldProps
[name
] = event
[name
];
12855 // new date-related properties. work off the original date snapshot.
12856 // ok to use references because they will be thrown away when backupEventDates is called.
12858 start
: event
._start
,
12860 allDay
: allDay
// normalize the dates in the same regard as the new properties
12862 normalizeEventDates(newProps
); // massages start/end/allDay
12864 // strip or ensure the end date
12866 newProps
.end
= null;
12868 else if (durationDelta
&& !newProps
.end
) { // the duration translation requires an end date
12869 newProps
.end
= t
.getDefaultEventEnd(newProps
.allDay
, newProps
.start
);
12873 newProps
.start
.add(dateDelta
);
12874 if (newProps
.end
) {
12875 newProps
.end
.add(dateDelta
);
12879 if (durationDelta
) {
12880 newProps
.end
.add(durationDelta
); // end already ensured above
12883 // if the dates have changed, and we know it is impossible to recompute the
12884 // timezone offsets, strip the zone.
12887 !newProps
.allDay
&&
12888 (dateDelta
|| durationDelta
)
12890 newProps
.start
.stripZone();
12891 if (newProps
.end
) {
12892 newProps
.end
.stripZone();
12896 $.extend(event
, miscProps
, newProps
); // copy over misc props, then date-related props
12897 backupEventDates(event
); // regenerate internal _start/_end/_allDay
12899 undoFunctions
.push(function() {
12900 $.extend(event
, oldProps
);
12901 backupEventDates(event
); // regenerate internal _start/_end/_allDay
12905 return function() {
12906 for (var i
= 0; i
< undoFunctions
.length
; i
++) {
12907 undoFunctions
[i
]();
12915 // returns an undo function
12916 Calendar
.prototype.mutateSeg = function(seg
, newProps
) {
12917 return this.mutateEvent(seg
.event
, newProps
);
12921 // hook for external libs to manipulate event properties upon creation.
12922 // should manipulate the event in-place.
12923 Calendar
.prototype.normalizeEvent = function(event
) {
12927 // Does the given span (start, end, and other location information)
12928 // fully contain the other?
12929 Calendar
.prototype.spanContainsSpan = function(outerSpan
, innerSpan
) {
12930 var eventStart
= outerSpan
.start
.clone().stripZone();
12931 var eventEnd
= this.getEventEnd(outerSpan
).stripZone();
12933 return innerSpan
.start
>= eventStart
&& innerSpan
.end
<= eventEnd
;
12937 // Returns a list of events that the given event should be compared against when being considered for a move to
12938 // the specified span. Attached to the Calendar's prototype because EventManager is a mixin for a Calendar.
12939 Calendar
.prototype.getPeerEvents = function(span
, event
) {
12940 var cache
= this.getEventCache();
12941 var peerEvents
= [];
12944 for (i
= 0; i
< cache
.length
; i
++) {
12945 otherEvent
= cache
[i
];
12948 event
._id
!== otherEvent
._id
// don't compare the event to itself or other related [repeating] events
12950 peerEvents
.push(otherEvent
);
12958 // updates the "backup" properties, which are preserved in order to compute diffs later on.
12959 function backupEventDates(event
) {
12960 event
._allDay
= event
.allDay
;
12961 event
._start
= event
.start
.clone();
12962 event
._end
= event
.end
? event
.end
.clone() : null;
12966 /* Overlapping / Constraining
12967 -----------------------------------------------------------------------------------------*/
12970 // Determines if the given event can be relocated to the given span (unzoned start/end with other misc data)
12971 Calendar
.prototype.isEventSpanAllowed = function(span
, event
) {
12972 var source
= event
.source
|| {};
12974 var constraint
= firstDefined(
12977 this.options
.eventConstraint
12980 var overlap
= firstDefined(
12983 this.options
.eventOverlap
12986 return this.isSpanAllowed(span
, constraint
, overlap
, event
) &&
12987 (!this.options
.eventAllow
|| this.options
.eventAllow(span
, event
) !== false);
12991 // Determines if an external event can be relocated to the given span (unzoned start/end with other misc data)
12992 Calendar
.prototype.isExternalSpanAllowed = function(eventSpan
, eventLocation
, eventProps
) {
12996 // note: very similar logic is in View's reportExternalDrop
12998 eventInput
= $.extend({}, eventProps
, eventLocation
);
12999 event
= this.expandEvent(
13000 this.buildEventFromInput(eventInput
)
13005 return this.isEventSpanAllowed(eventSpan
, event
);
13007 else { // treat it as a selection
13009 return this.isSelectionSpanAllowed(eventSpan
);
13014 // Determines the given span (unzoned start/end with other misc data) can be selected.
13015 Calendar
.prototype.isSelectionSpanAllowed = function(span
) {
13016 return this.isSpanAllowed(span
, this.options
.selectConstraint
, this.options
.selectOverlap
) &&
13017 (!this.options
.selectAllow
|| this.options
.selectAllow(span
) !== false);
13021 // Returns true if the given span (caused by an event drop/resize or a selection) is allowed to exist
13022 // according to the constraint/overlap settings.
13023 // `event` is not required if checking a selection.
13024 Calendar
.prototype.isSpanAllowed = function(span
, constraint
, overlap
, event
) {
13025 var constraintEvents
;
13026 var anyContainment
;
13031 // the range must be fully contained by at least one of produced constraint events
13032 if (constraint
!= null) {
13034 // not treated as an event! intermediate data structure
13035 // TODO: use ranges in the future
13036 constraintEvents
= this.constraintToEvents(constraint
);
13037 if (constraintEvents
) { // not invalid
13039 anyContainment
= false;
13040 for (i
= 0; i
< constraintEvents
.length
; i
++) {
13041 if (this.spanContainsSpan(constraintEvents
[i
], span
)) {
13042 anyContainment
= true;
13047 if (!anyContainment
) {
13053 peerEvents
= this.getPeerEvents(span
, event
);
13055 for (i
= 0; i
< peerEvents
.length
; i
++) {
13056 peerEvent
= peerEvents
[i
];
13058 // there needs to be an actual intersection before disallowing anything
13059 if (this.eventIntersectsRange(peerEvent
, span
)) {
13061 // evaluate overlap for the given range and short-circuit if necessary
13062 if (overlap
=== false) {
13065 // if the event's overlap is a test function, pass the peer event in question as the first param
13066 else if (typeof overlap
=== 'function' && !overlap(peerEvent
, event
)) {
13070 // if we are computing if the given range is allowable for an event, consider the other event's
13071 // EventObject-specific or Source-specific `overlap` property
13073 peerOverlap
= firstDefined(
13075 (peerEvent
.source
|| {}).overlap
13076 // we already considered the global `eventOverlap`
13078 if (peerOverlap
=== false) {
13081 // if the peer event's overlap is a test function, pass the subject event as the first param
13082 if (typeof peerOverlap
=== 'function' && !peerOverlap(event
, peerEvent
)) {
13093 // Given an event input from the API, produces an array of event objects. Possible event inputs:
13095 // An event ID (number or string)
13096 // An object with specific start/end dates or a recurring event (like what businessHours accepts)
13097 Calendar
.prototype.constraintToEvents = function(constraintInput
) {
13099 if (constraintInput
=== 'businessHours') {
13100 return this.getCurrentBusinessHourEvents();
13103 if (typeof constraintInput
=== 'object') {
13104 if (constraintInput
.start
!= null) { // needs to be event-like input
13105 return this.expandEvent(this.buildEventFromInput(constraintInput
));
13108 return null; // invalid
13112 return this.clientEvents(constraintInput
); // probably an ID
13116 // Does the event's date range intersect with the given range?
13117 // start/end already assumed to have stripped zones :(
13118 Calendar
.prototype.eventIntersectsRange = function(event
, range
) {
13119 var eventStart
= event
.start
.clone().stripZone();
13120 var eventEnd
= this.getEventEnd(event
).stripZone();
13122 return range
.start
< eventEnd
&& range
.end
> eventStart
;
13127 -----------------------------------------------------------------------------------------*/
13129 var BUSINESS_HOUR_EVENT_DEFAULTS
= {
13130 id
: '_fcBusinessHours', // will relate events from different calls to expandEvent
13133 dow
: [ 1, 2, 3, 4, 5 ], // monday - friday
13134 rendering
: 'inverse-background'
13135 // classNames are defined in businessHoursSegClasses
13138 // Return events objects for business hours within the current view.
13139 // Abuse of our event system :(
13140 Calendar
.prototype.getCurrentBusinessHourEvents = function(wholeDay
) {
13141 return this.computeBusinessHourEvents(wholeDay
, this.options
.businessHours
);
13144 // Given a raw input value from options, return events objects for business hours within the current view.
13145 Calendar
.prototype.computeBusinessHourEvents = function(wholeDay
, input
) {
13146 if (input
=== true) {
13147 return this.expandBusinessHourEvents(wholeDay
, [ {} ]);
13149 else if ($.isPlainObject(input
)) {
13150 return this.expandBusinessHourEvents(wholeDay
, [ input
]);
13152 else if ($.isArray(input
)) {
13153 return this.expandBusinessHourEvents(wholeDay
, input
, true);
13160 // inputs expected to be an array of objects.
13161 // if ignoreNoDow is true, will ignore entries that don't specify a day-of-week (dow) key.
13162 Calendar
.prototype.expandBusinessHourEvents = function(wholeDay
, inputs
, ignoreNoDow
) {
13163 var view
= this.getView();
13167 for (i
= 0; i
< inputs
.length
; i
++) {
13170 if (ignoreNoDow
&& !input
.dow
) {
13174 // give defaults. will make a copy
13175 input
= $.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS
, input
);
13177 // if a whole-day series is requested, clear the start/end times
13179 input
.start
= null;
13183 events
.push
.apply(events
, // append
13185 this.buildEventFromInput(input
),
13197 /* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells.
13198 ----------------------------------------------------------------------------------------------------------------------*/
13199 // It is a manager for a DayGrid subcomponent, which does most of the heavy lifting.
13200 // It is responsible for managing width/height.
13202 var BasicView
= FC
.BasicView
= View
.extend({
13206 dayGridClass
: DayGrid
, // class the dayGrid will be instantiated from (overridable by subclasses)
13207 dayGrid
: null, // the main subcomponent that does most of the heavy lifting
13209 dayNumbersVisible
: false, // display day numbers on each day cell?
13210 colWeekNumbersVisible
: false, // display week numbers along the side?
13211 cellWeekNumbersVisible
: false, // display week numbers in day cell?
13213 weekNumberWidth
: null, // width of all the week-number cells running down the side
13215 headContainerEl
: null, // div that hold's the dayGrid's rendered date header
13216 headRowEl
: null, // the fake row element of the day-of-week header
13219 initialize: function() {
13220 this.dayGrid
= this.instantiateDayGrid();
13222 this.scroller
= new Scroller({
13223 overflowX
: 'hidden',
13229 // Generates the DayGrid object this view needs. Draws from this.dayGridClass
13230 instantiateDayGrid: function() {
13231 // generate a subclass on the fly with BasicView-specific behavior
13232 // TODO: cache this subclass
13233 var subclass
= this.dayGridClass
.extend(basicDayGridMethods
);
13235 return new subclass(this);
13239 // Sets the display range and computes all necessary dates
13240 setRange: function(range
) {
13241 View
.prototype.setRange
.call(this, range
); // call the super-method
13243 this.dayGrid
.breakOnWeeks
= /year|month|week/.test(this.intervalUnit
); // do before setRange
13244 this.dayGrid
.setRange(range
);
13248 // Compute the value to feed into setRange. Overrides superclass.
13249 computeRange: function(date
) {
13250 var range
= View
.prototype.computeRange
.call(this, date
); // get value from the super-method
13252 // year and month views should be aligned with weeks. this is already done for week
13253 if (/year|month/.test(range
.intervalUnit
)) {
13254 range
.start
.startOf('week');
13255 range
.start
= this.skipHiddenDays(range
.start
);
13257 // make end-of-week if not already
13258 if (range
.end
.weekday()) {
13259 range
.end
.add(1, 'week').startOf('week');
13260 range
.end
= this.skipHiddenDays(range
.end
, -1, true); // exclusively move backwards
13268 // Renders the view into `this.el`, which should already be assigned
13269 renderDates: function() {
13271 this.dayNumbersVisible
= this.dayGrid
.rowCnt
> 1; // TODO: make grid responsible
13272 if (this.opt('weekNumbers')) {
13273 if (this.opt('weekNumbersWithinDays')) {
13274 this.cellWeekNumbersVisible
= true;
13275 this.colWeekNumbersVisible
= false;
13278 this.cellWeekNumbersVisible
= false;
13279 this.colWeekNumbersVisible
= true;
13282 this.dayGrid
.numbersVisible
= this.dayNumbersVisible
||
13283 this.cellWeekNumbersVisible
|| this.colWeekNumbersVisible
;
13285 this.el
.addClass('fc-basic-view').html(this.renderSkeletonHtml());
13288 this.scroller
.render();
13289 var dayGridContainerEl
= this.scroller
.el
.addClass('fc-day-grid-container');
13290 var dayGridEl
= $('<div class="fc-day-grid" />').appendTo(dayGridContainerEl
);
13291 this.el
.find('.fc-body > tr > td').append(dayGridContainerEl
);
13293 this.dayGrid
.setElement(dayGridEl
);
13294 this.dayGrid
.renderDates(this.hasRigidRows());
13298 // render the day-of-week headers
13299 renderHead: function() {
13300 this.headContainerEl
=
13301 this.el
.find('.fc-head-container')
13302 .html(this.dayGrid
.renderHeadHtml());
13303 this.headRowEl
= this.headContainerEl
.find('.fc-row');
13307 // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering,
13308 // always completely kill the dayGrid's rendering.
13309 unrenderDates: function() {
13310 this.dayGrid
.unrenderDates();
13311 this.dayGrid
.removeElement();
13312 this.scroller
.destroy();
13316 renderBusinessHours: function() {
13317 this.dayGrid
.renderBusinessHours();
13321 unrenderBusinessHours: function() {
13322 this.dayGrid
.unrenderBusinessHours();
13326 // Builds the HTML skeleton for the view.
13327 // The day-grid component will render inside of a container defined by this HTML.
13328 renderSkeletonHtml: function() {
13331 '<thead class="fc-head">' +
13333 '<td class="fc-head-container ' + this.widgetHeaderClass
+ '"></td>' +
13336 '<tbody class="fc-body">' +
13338 '<td class="' + this.widgetContentClass
+ '"></td>' +
13345 // Generates an HTML attribute string for setting the width of the week number column, if it is known
13346 weekNumberStyleAttr: function() {
13347 if (this.weekNumberWidth
!== null) {
13348 return 'style="width:' + this.weekNumberWidth
+ 'px"';
13354 // Determines whether each row should have a constant height
13355 hasRigidRows: function() {
13356 var eventLimit
= this.opt('eventLimit');
13357 return eventLimit
&& typeof eventLimit
!== 'number';
13362 ------------------------------------------------------------------------------------------------------------------*/
13365 // Refreshes the horizontal dimensions of the view
13366 updateWidth: function() {
13367 if (this.colWeekNumbersVisible
) {
13368 // Make sure all week number cells running down the side have the same width.
13369 // Record the width for cells created later.
13370 this.weekNumberWidth
= matchCellWidths(
13371 this.el
.find('.fc-week-number')
13377 // Adjusts the vertical dimensions of the view to the specified values
13378 setHeight: function(totalHeight
, isAuto
) {
13379 var eventLimit
= this.opt('eventLimit');
13380 var scrollerHeight
;
13381 var scrollbarWidths
;
13383 // reset all heights to be natural
13384 this.scroller
.clear();
13385 uncompensateScroll(this.headRowEl
);
13387 this.dayGrid
.removeSegPopover(); // kill the "more" popover if displayed
13389 // is the event limit a constant level number?
13390 if (eventLimit
&& typeof eventLimit
=== 'number') {
13391 this.dayGrid
.limitRows(eventLimit
); // limit the levels first so the height can redistribute after
13394 // distribute the height to the rows
13395 // (totalHeight is a "recommended" value if isAuto)
13396 scrollerHeight
= this.computeScrollerHeight(totalHeight
);
13397 this.setGridHeight(scrollerHeight
, isAuto
);
13399 // is the event limit dynamically calculated?
13400 if (eventLimit
&& typeof eventLimit
!== 'number') {
13401 this.dayGrid
.limitRows(eventLimit
); // limit the levels after the grid's row heights have been set
13404 if (!isAuto
) { // should we force dimensions of the scroll container?
13406 this.scroller
.setHeight(scrollerHeight
);
13407 scrollbarWidths
= this.scroller
.getScrollbarWidths();
13409 if (scrollbarWidths
.left
|| scrollbarWidths
.right
) { // using scrollbars?
13411 compensateScroll(this.headRowEl
, scrollbarWidths
);
13413 // doing the scrollbar compensation might have created text overflow which created more height. redo
13414 scrollerHeight
= this.computeScrollerHeight(totalHeight
);
13415 this.scroller
.setHeight(scrollerHeight
);
13418 // guarantees the same scrollbar widths
13419 this.scroller
.lockOverflow(scrollbarWidths
);
13424 // given a desired total height of the view, returns what the height of the scroller should be
13425 computeScrollerHeight: function(totalHeight
) {
13426 return totalHeight
-
13427 subtractInnerElHeight(this.el
, this.scroller
.el
); // everything that's NOT the scroller
13431 // Sets the height of just the DayGrid component in this view
13432 setGridHeight: function(height
, isAuto
) {
13434 undistributeHeight(this.dayGrid
.rowEls
); // let the rows be their natural height with no expanding
13437 distributeHeight(this.dayGrid
.rowEls
, height
, true); // true = compensate for height-hogging rows
13443 ------------------------------------------------------------------------------------------------------------------*/
13446 computeInitialScroll: function() {
13451 queryScroll: function() {
13452 return { top
: this.scroller
.getScrollTop() };
13456 setScroll: function(scroll
) {
13457 this.scroller
.setScrollTop(scroll
.top
);
13462 ------------------------------------------------------------------------------------------------------------------*/
13463 // forward all hit-related method calls to dayGrid
13466 hitsNeeded: function() {
13467 this.dayGrid
.hitsNeeded();
13471 hitsNotNeeded: function() {
13472 this.dayGrid
.hitsNotNeeded();
13476 prepareHits: function() {
13477 this.dayGrid
.prepareHits();
13481 releaseHits: function() {
13482 this.dayGrid
.releaseHits();
13486 queryHit: function(left
, top
) {
13487 return this.dayGrid
.queryHit(left
, top
);
13491 getHitSpan: function(hit
) {
13492 return this.dayGrid
.getHitSpan(hit
);
13496 getHitEl: function(hit
) {
13497 return this.dayGrid
.getHitEl(hit
);
13502 ------------------------------------------------------------------------------------------------------------------*/
13505 // Renders the given events onto the view and populates the segments array
13506 renderEvents: function(events
) {
13507 this.dayGrid
.renderEvents(events
);
13509 this.updateHeight(); // must compensate for events that overflow the row
13513 // Retrieves all segment objects that are rendered in the view
13514 getEventSegs: function() {
13515 return this.dayGrid
.getEventSegs();
13519 // Unrenders all event elements and clears internal segment data
13520 unrenderEvents: function() {
13521 this.dayGrid
.unrenderEvents();
13523 // we DON'T need to call updateHeight() because
13524 // a renderEvents() call always happens after this, which will eventually call updateHeight()
13528 /* Dragging (for both events and external elements)
13529 ------------------------------------------------------------------------------------------------------------------*/
13532 // A returned value of `true` signals that a mock "helper" event has been rendered.
13533 renderDrag: function(dropLocation
, seg
) {
13534 return this.dayGrid
.renderDrag(dropLocation
, seg
);
13538 unrenderDrag: function() {
13539 this.dayGrid
.unrenderDrag();
13544 ------------------------------------------------------------------------------------------------------------------*/
13547 // Renders a visual indication of a selection
13548 renderSelection: function(span
) {
13549 this.dayGrid
.renderSelection(span
);
13553 // Unrenders a visual indications of a selection
13554 unrenderSelection: function() {
13555 this.dayGrid
.unrenderSelection();
13561 // Methods that will customize the rendering behavior of the BasicView's dayGrid
13562 var basicDayGridMethods
= {
13565 // Generates the HTML that will go before the day-of week header cells
13566 renderHeadIntroHtml: function() {
13567 var view
= this.view
;
13569 if (view
.colWeekNumbersVisible
) {
13571 '<th class="fc-week-number ' + view
.widgetHeaderClass
+ '" ' + view
.weekNumberStyleAttr() + '>' +
13572 '<span>' + // needed for matchCellWidths
13573 htmlEscape(view
.opt('weekNumberTitle')) +
13582 // Generates the HTML that will go before content-skeleton cells that display the day/week numbers
13583 renderNumberIntroHtml: function(row
) {
13584 var view
= this.view
;
13585 var weekStart
= this.getCellDate(row
, 0);
13587 if (view
.colWeekNumbersVisible
) {
13589 '<td class="fc-week-number" ' + view
.weekNumberStyleAttr() + '>' +
13590 view
.buildGotoAnchorHtml( // aside from link, important for matchCellWidths
13591 { date
: weekStart
, type
: 'week', forceOff
: this.colCnt
=== 1 },
13592 weekStart
.format('w') // inner HTML
13601 // Generates the HTML that goes before the day bg cells for each day-row
13602 renderBgIntroHtml: function() {
13603 var view
= this.view
;
13605 if (view
.colWeekNumbersVisible
) {
13606 return '<td class="fc-week-number ' + view
.widgetContentClass
+ '" ' +
13607 view
.weekNumberStyleAttr() + '></td>';
13614 // Generates the HTML that goes before every other type of row generated by DayGrid.
13615 // Affects helper-skeleton and highlight-skeleton rows.
13616 renderIntroHtml: function() {
13617 var view
= this.view
;
13619 if (view
.colWeekNumbersVisible
) {
13620 return '<td class="fc-week-number" ' + view
.weekNumberStyleAttr() + '></td>';
13630 /* A month view with day cells running in rows (one-per-week) and columns
13631 ----------------------------------------------------------------------------------------------------------------------*/
13633 var MonthView
= FC
.MonthView
= BasicView
.extend({
13635 // Produces information about what range to display
13636 computeRange: function(date
) {
13637 var range
= BasicView
.prototype.computeRange
.call(this, date
); // get value from super-method
13641 if (this.isFixedWeeks()) {
13642 rowCnt
= Math
.ceil(range
.end
.diff(range
.start
, 'weeks', true)); // could be partial weeks due to hiddenDays
13643 range
.end
.add(6 - rowCnt
, 'weeks');
13650 // Overrides the default BasicView behavior to have special multi-week auto-height logic
13651 setGridHeight: function(height
, isAuto
) {
13653 // if auto, make the height of each row the height that it would be if there were 6 weeks
13655 height
*= this.rowCnt
/ 6;
13658 distributeHeight(this.dayGrid
.rowEls
, height
, !isAuto
); // if auto, don't compensate for height-hogging rows
13662 isFixedWeeks: function() {
13663 return this.opt('fixedWeekCount');
13674 fcViews
.basicDay
= {
13676 duration
: { days
: 1 }
13679 fcViews
.basicWeek
= {
13681 duration
: { weeks
: 1 }
13685 'class': MonthView
,
13686 duration
: { months
: 1 }, // important for prev/next
13688 fixedWeekCount
: true
13693 /* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically.
13694 ----------------------------------------------------------------------------------------------------------------------*/
13695 // Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on).
13696 // Responsible for managing width/height.
13698 var AgendaView
= FC
.AgendaView
= View
.extend({
13702 timeGridClass
: TimeGrid
, // class used to instantiate the timeGrid. subclasses can override
13703 timeGrid
: null, // the main time-grid subcomponent of this view
13705 dayGridClass
: DayGrid
, // class used to instantiate the dayGrid. subclasses can override
13706 dayGrid
: null, // the "all-day" subcomponent. if all-day is turned off, this will be null
13708 axisWidth
: null, // the width of the time axis running down the side
13710 headContainerEl
: null, // div that hold's the timeGrid's rendered date header
13711 noScrollRowEls
: null, // set of fake row elements that must compensate when scroller has scrollbars
13713 // when the time-grid isn't tall enough to occupy the given height, we render an <hr> underneath
13714 bottomRuleEl
: null,
13717 initialize: function() {
13718 this.timeGrid
= this.instantiateTimeGrid();
13720 if (this.opt('allDaySlot')) { // should we display the "all-day" area?
13721 this.dayGrid
= this.instantiateDayGrid(); // the all-day subcomponent of this view
13724 this.scroller
= new Scroller({
13725 overflowX
: 'hidden',
13731 // Instantiates the TimeGrid object this view needs. Draws from this.timeGridClass
13732 instantiateTimeGrid: function() {
13733 var subclass
= this.timeGridClass
.extend(agendaTimeGridMethods
);
13735 return new subclass(this);
13739 // Instantiates the DayGrid object this view might need. Draws from this.dayGridClass
13740 instantiateDayGrid: function() {
13741 var subclass
= this.dayGridClass
.extend(agendaDayGridMethods
);
13743 return new subclass(this);
13748 ------------------------------------------------------------------------------------------------------------------*/
13751 // Sets the display range and computes all necessary dates
13752 setRange: function(range
) {
13753 View
.prototype.setRange
.call(this, range
); // call the super-method
13755 this.timeGrid
.setRange(range
);
13756 if (this.dayGrid
) {
13757 this.dayGrid
.setRange(range
);
13762 // Renders the view into `this.el`, which has already been assigned
13763 renderDates: function() {
13765 this.el
.addClass('fc-agenda-view').html(this.renderSkeletonHtml());
13768 this.scroller
.render();
13769 var timeGridWrapEl
= this.scroller
.el
.addClass('fc-time-grid-container');
13770 var timeGridEl
= $('<div class="fc-time-grid" />').appendTo(timeGridWrapEl
);
13771 this.el
.find('.fc-body > tr > td').append(timeGridWrapEl
);
13773 this.timeGrid
.setElement(timeGridEl
);
13774 this.timeGrid
.renderDates();
13776 // the <hr> that sometimes displays under the time-grid
13777 this.bottomRuleEl
= $('<hr class="fc-divider ' + this.widgetHeaderClass
+ '"/>')
13778 .appendTo(this.timeGrid
.el
); // inject it into the time-grid
13780 if (this.dayGrid
) {
13781 this.dayGrid
.setElement(this.el
.find('.fc-day-grid'));
13782 this.dayGrid
.renderDates();
13784 // have the day-grid extend it's coordinate area over the <hr> dividing the two grids
13785 this.dayGrid
.bottomCoordPadding
= this.dayGrid
.el
.next('hr').outerHeight();
13788 this.noScrollRowEls
= this.el
.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller
13792 // render the day-of-week headers
13793 renderHead: function() {
13794 this.headContainerEl
=
13795 this.el
.find('.fc-head-container')
13796 .html(this.timeGrid
.renderHeadHtml());
13800 // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering,
13801 // always completely kill each grid's rendering.
13802 unrenderDates: function() {
13803 this.timeGrid
.unrenderDates();
13804 this.timeGrid
.removeElement();
13806 if (this.dayGrid
) {
13807 this.dayGrid
.unrenderDates();
13808 this.dayGrid
.removeElement();
13811 this.scroller
.destroy();
13815 // Builds the HTML skeleton for the view.
13816 // The day-grid and time-grid components will render inside containers defined by this HTML.
13817 renderSkeletonHtml: function() {
13820 '<thead class="fc-head">' +
13822 '<td class="fc-head-container ' + this.widgetHeaderClass
+ '"></td>' +
13825 '<tbody class="fc-body">' +
13827 '<td class="' + this.widgetContentClass
+ '">' +
13829 '<div class="fc-day-grid"/>' +
13830 '<hr class="fc-divider ' + this.widgetHeaderClass
+ '"/>' :
13840 // Generates an HTML attribute string for setting the width of the axis, if it is known
13841 axisStyleAttr: function() {
13842 if (this.axisWidth
!== null) {
13843 return 'style="width:' + this.axisWidth
+ 'px"';
13850 ------------------------------------------------------------------------------------------------------------------*/
13853 renderBusinessHours: function() {
13854 this.timeGrid
.renderBusinessHours();
13856 if (this.dayGrid
) {
13857 this.dayGrid
.renderBusinessHours();
13862 unrenderBusinessHours: function() {
13863 this.timeGrid
.unrenderBusinessHours();
13865 if (this.dayGrid
) {
13866 this.dayGrid
.unrenderBusinessHours();
13872 ------------------------------------------------------------------------------------------------------------------*/
13875 getNowIndicatorUnit: function() {
13876 return this.timeGrid
.getNowIndicatorUnit();
13880 renderNowIndicator: function(date
) {
13881 this.timeGrid
.renderNowIndicator(date
);
13885 unrenderNowIndicator: function() {
13886 this.timeGrid
.unrenderNowIndicator();
13891 ------------------------------------------------------------------------------------------------------------------*/
13894 updateSize: function(isResize
) {
13895 this.timeGrid
.updateSize(isResize
);
13897 View
.prototype.updateSize
.call(this, isResize
); // call the super-method
13901 // Refreshes the horizontal dimensions of the view
13902 updateWidth: function() {
13903 // make all axis cells line up, and record the width so newly created axis cells will have it
13904 this.axisWidth
= matchCellWidths(this.el
.find('.fc-axis'));
13908 // Adjusts the vertical dimensions of the view to the specified values
13909 setHeight: function(totalHeight
, isAuto
) {
13911 var scrollerHeight
;
13912 var scrollbarWidths
;
13914 // reset all dimensions back to the original state
13915 this.bottomRuleEl
.hide(); // .show() will be called later if this <hr> is necessary
13916 this.scroller
.clear(); // sets height to 'auto' and clears overflow
13917 uncompensateScroll(this.noScrollRowEls
);
13919 // limit number of events in the all-day area
13920 if (this.dayGrid
) {
13921 this.dayGrid
.removeSegPopover(); // kill the "more" popover if displayed
13923 eventLimit
= this.opt('eventLimit');
13924 if (eventLimit
&& typeof eventLimit
!== 'number') {
13925 eventLimit
= AGENDA_ALL_DAY_EVENT_LIMIT
; // make sure "auto" goes to a real number
13928 this.dayGrid
.limitRows(eventLimit
);
13932 if (!isAuto
) { // should we force dimensions of the scroll container?
13934 scrollerHeight
= this.computeScrollerHeight(totalHeight
);
13935 this.scroller
.setHeight(scrollerHeight
);
13936 scrollbarWidths
= this.scroller
.getScrollbarWidths();
13938 if (scrollbarWidths
.left
|| scrollbarWidths
.right
) { // using scrollbars?
13940 // make the all-day and header rows lines up
13941 compensateScroll(this.noScrollRowEls
, scrollbarWidths
);
13943 // the scrollbar compensation might have changed text flow, which might affect height, so recalculate
13944 // and reapply the desired height to the scroller.
13945 scrollerHeight
= this.computeScrollerHeight(totalHeight
);
13946 this.scroller
.setHeight(scrollerHeight
);
13949 // guarantees the same scrollbar widths
13950 this.scroller
.lockOverflow(scrollbarWidths
);
13952 // if there's any space below the slats, show the horizontal rule.
13953 // this won't cause any new overflow, because lockOverflow already called.
13954 if (this.timeGrid
.getTotalSlatHeight() < scrollerHeight
) {
13955 this.bottomRuleEl
.show();
13961 // given a desired total height of the view, returns what the height of the scroller should be
13962 computeScrollerHeight: function(totalHeight
) {
13963 return totalHeight
-
13964 subtractInnerElHeight(this.el
, this.scroller
.el
); // everything that's NOT the scroller
13969 ------------------------------------------------------------------------------------------------------------------*/
13972 // Computes the initial pre-configured scroll state prior to allowing the user to change it
13973 computeInitialScroll: function() {
13974 var scrollTime
= moment
.duration(this.opt('scrollTime'));
13975 var top
= this.timeGrid
.computeTimeTop(scrollTime
);
13977 // zoom can give weird floating-point values. rather scroll a little bit further
13978 top
= Math
.ceil(top
);
13981 top
++; // to overcome top border that slots beyond the first have. looks better
13984 return { top
: top
};
13988 queryScroll: function() {
13989 return { top
: this.scroller
.getScrollTop() };
13993 setScroll: function(scroll
) {
13994 this.scroller
.setScrollTop(scroll
.top
);
13999 ------------------------------------------------------------------------------------------------------------------*/
14000 // forward all hit-related method calls to the grids (dayGrid might not be defined)
14003 hitsNeeded: function() {
14004 this.timeGrid
.hitsNeeded();
14005 if (this.dayGrid
) {
14006 this.dayGrid
.hitsNeeded();
14011 hitsNotNeeded: function() {
14012 this.timeGrid
.hitsNotNeeded();
14013 if (this.dayGrid
) {
14014 this.dayGrid
.hitsNotNeeded();
14019 prepareHits: function() {
14020 this.timeGrid
.prepareHits();
14021 if (this.dayGrid
) {
14022 this.dayGrid
.prepareHits();
14027 releaseHits: function() {
14028 this.timeGrid
.releaseHits();
14029 if (this.dayGrid
) {
14030 this.dayGrid
.releaseHits();
14035 queryHit: function(left
, top
) {
14036 var hit
= this.timeGrid
.queryHit(left
, top
);
14038 if (!hit
&& this.dayGrid
) {
14039 hit
= this.dayGrid
.queryHit(left
, top
);
14046 getHitSpan: function(hit
) {
14047 // TODO: hit.component is set as a hack to identify where the hit came from
14048 return hit
.component
.getHitSpan(hit
);
14052 getHitEl: function(hit
) {
14053 // TODO: hit.component is set as a hack to identify where the hit came from
14054 return hit
.component
.getHitEl(hit
);
14059 ------------------------------------------------------------------------------------------------------------------*/
14062 // Renders events onto the view and populates the View's segment array
14063 renderEvents: function(events
) {
14064 var dayEvents
= [];
14065 var timedEvents
= [];
14070 // separate the events into all-day and timed
14071 for (i
= 0; i
< events
.length
; i
++) {
14072 if (events
[i
].allDay
) {
14073 dayEvents
.push(events
[i
]);
14076 timedEvents
.push(events
[i
]);
14080 // render the events in the subcomponents
14081 timedSegs
= this.timeGrid
.renderEvents(timedEvents
);
14082 if (this.dayGrid
) {
14083 daySegs
= this.dayGrid
.renderEvents(dayEvents
);
14086 // the all-day area is flexible and might have a lot of events, so shift the height
14087 this.updateHeight();
14091 // Retrieves all segment objects that are rendered in the view
14092 getEventSegs: function() {
14093 return this.timeGrid
.getEventSegs().concat(
14094 this.dayGrid
? this.dayGrid
.getEventSegs() : []
14099 // Unrenders all event elements and clears internal segment data
14100 unrenderEvents: function() {
14102 // unrender the events in the subcomponents
14103 this.timeGrid
.unrenderEvents();
14104 if (this.dayGrid
) {
14105 this.dayGrid
.unrenderEvents();
14108 // we DON'T need to call updateHeight() because
14109 // a renderEvents() call always happens after this, which will eventually call updateHeight()
14113 /* Dragging (for events and external elements)
14114 ------------------------------------------------------------------------------------------------------------------*/
14117 // A returned value of `true` signals that a mock "helper" event has been rendered.
14118 renderDrag: function(dropLocation
, seg
) {
14119 if (dropLocation
.start
.hasTime()) {
14120 return this.timeGrid
.renderDrag(dropLocation
, seg
);
14122 else if (this.dayGrid
) {
14123 return this.dayGrid
.renderDrag(dropLocation
, seg
);
14128 unrenderDrag: function() {
14129 this.timeGrid
.unrenderDrag();
14130 if (this.dayGrid
) {
14131 this.dayGrid
.unrenderDrag();
14137 ------------------------------------------------------------------------------------------------------------------*/
14140 // Renders a visual indication of a selection
14141 renderSelection: function(span
) {
14142 if (span
.start
.hasTime() || span
.end
.hasTime()) {
14143 this.timeGrid
.renderSelection(span
);
14145 else if (this.dayGrid
) {
14146 this.dayGrid
.renderSelection(span
);
14151 // Unrenders a visual indications of a selection
14152 unrenderSelection: function() {
14153 this.timeGrid
.unrenderSelection();
14154 if (this.dayGrid
) {
14155 this.dayGrid
.unrenderSelection();
14162 // Methods that will customize the rendering behavior of the AgendaView's timeGrid
14163 // TODO: move into TimeGrid
14164 var agendaTimeGridMethods
= {
14167 // Generates the HTML that will go before the day-of week header cells
14168 renderHeadIntroHtml: function() {
14169 var view
= this.view
;
14172 if (view
.opt('weekNumbers')) {
14173 weekText
= this.start
.format(view
.opt('smallWeekFormat'));
14176 '<th class="fc-axis fc-week-number ' + view
.widgetHeaderClass
+ '" ' + view
.axisStyleAttr() + '>' +
14177 view
.buildGotoAnchorHtml( // aside from link, important for matchCellWidths
14178 { date
: this.start
, type
: 'week', forceOff
: this.colCnt
> 1 },
14179 htmlEscape(weekText
) // inner HTML
14184 return '<th class="fc-axis ' + view
.widgetHeaderClass
+ '" ' + view
.axisStyleAttr() + '></th>';
14189 // Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column.
14190 renderBgIntroHtml: function() {
14191 var view
= this.view
;
14193 return '<td class="fc-axis ' + view
.widgetContentClass
+ '" ' + view
.axisStyleAttr() + '></td>';
14197 // Generates the HTML that goes before all other types of cells.
14198 // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid.
14199 renderIntroHtml: function() {
14200 var view
= this.view
;
14202 return '<td class="fc-axis" ' + view
.axisStyleAttr() + '></td>';
14208 // Methods that will customize the rendering behavior of the AgendaView's dayGrid
14209 var agendaDayGridMethods
= {
14212 // Generates the HTML that goes before the all-day cells
14213 renderBgIntroHtml: function() {
14214 var view
= this.view
;
14217 '<td class="fc-axis ' + view
.widgetContentClass
+ '" ' + view
.axisStyleAttr() + '>' +
14218 '<span>' + // needed for matchCellWidths
14219 view
.getAllDayHtml() +
14225 // Generates the HTML that goes before all other types of cells.
14226 // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid.
14227 renderIntroHtml: function() {
14228 var view
= this.view
;
14230 return '<td class="fc-axis" ' + view
.axisStyleAttr() + '></td>';
14237 var AGENDA_ALL_DAY_EVENT_LIMIT
= 5;
14239 // potential nice values for the slot-duration and interval-duration
14240 // from largest to smallest
14241 var AGENDA_STOCK_SUB_DURATIONS
= [
14250 'class': AgendaView
,
14253 slotDuration
: '00:30:00',
14254 minTime
: '00:00:00',
14255 maxTime
: '24:00:00',
14256 slotEventOverlap
: true // a bad name. confused with overlap/constraint system
14260 fcViews
.agendaDay
= {
14262 duration
: { days
: 1 }
14265 fcViews
.agendaWeek
= {
14267 duration
: { weeks
: 1 }
14272 Responsible for the scroller, and forwarding event-related actions into the "grid"
14274 var ListView
= View
.extend({
14279 initialize: function() {
14280 this.grid
= new ListViewGrid(this);
14281 this.scroller
= new Scroller({
14282 overflowX
: 'hidden',
14287 setRange: function(range
) {
14288 View
.prototype.setRange
.call(this, range
); // super
14290 this.grid
.setRange(range
); // needs to process range-related options
14293 renderSkeleton: function() {
14296 this.widgetContentClass
14299 this.scroller
.render();
14300 this.scroller
.el
.appendTo(this.el
);
14302 this.grid
.setElement(this.scroller
.scrollEl
);
14305 unrenderSkeleton: function() {
14306 this.scroller
.destroy(); // will remove the Grid too
14309 setHeight: function(totalHeight
, isAuto
) {
14310 this.scroller
.setHeight(this.computeScrollerHeight(totalHeight
));
14313 computeScrollerHeight: function(totalHeight
) {
14314 return totalHeight
-
14315 subtractInnerElHeight(this.el
, this.scroller
.el
); // everything that's NOT the scroller
14318 renderEvents: function(events
) {
14319 this.grid
.renderEvents(events
);
14322 unrenderEvents: function() {
14323 this.grid
.unrenderEvents();
14326 isEventResizable: function(event
) {
14330 isEventDraggable: function(event
) {
14337 Responsible for event rendering and user-interaction.
14338 Its "el" is the inner-content of the above view's scroller.
14340 var ListViewGrid
= Grid
.extend({
14342 segSelector
: '.fc-list-item', // which elements accept event actions
14343 hasDayInteractions
: false, // no day selection or day clicking
14346 spanToSegs: function(span
) {
14347 var view
= this.view
;
14348 var dayStart
= view
.start
.clone().time(0); // timed, so segs get times!
14353 while (dayStart
< view
.end
) {
14355 seg
= intersectRanges(span
, {
14357 end
: dayStart
.clone().add(1, 'day')
14361 seg
.dayIndex
= dayIndex
;
14365 dayStart
.add(1, 'day');
14368 // detect when span won't go fully into the next day,
14369 // and mutate the latest seg to the be the end.
14371 seg
&& !seg
.isEnd
&& span
.end
.hasTime() &&
14372 span
.end
< dayStart
.clone().add(this.view
.nextDayThreshold
)
14374 seg
.end
= span
.end
.clone();
14384 computeEventTimeFormat: function() {
14385 return this.view
.opt('mediumTimeFormat');
14388 // for events with a url, the whole <tr> should be clickable,
14389 // but it's impossible to wrap with an <a> tag. simulate this.
14390 handleSegClick: function(seg
, ev
) {
14393 Grid
.prototype.handleSegClick
.apply(this, arguments
); // super. might prevent the default action
14395 // not clicking on or within an <a> with an href
14396 if (!$(ev
.target
).closest('a[href]').length
) {
14397 url
= seg
.event
.url
;
14398 if (url
&& !ev
.isDefaultPrevented()) { // jsEvent not cancelled in handler
14399 window
.location
.href
= url
; // simulate link click
14404 // returns list of foreground segs that were actually rendered
14405 renderFgSegs: function(segs
) {
14406 segs
= this.renderFgSegEls(segs
); // might filter away hidden events
14408 if (!segs
.length
) {
14409 this.renderEmptyMessage();
14412 this.renderSegList(segs
);
14418 renderEmptyMessage: function() {
14420 '<div class="fc-list-empty-wrap2">' + // TODO: try less wraps
14421 '<div class="fc-list-empty-wrap1">' +
14422 '<div class="fc-list-empty">' +
14423 htmlEscape(this.view
.opt('noEventsMessage')) +
14430 // render the event segments in the view
14431 renderSegList: function(allSegs
) {
14432 var segsByDay
= this.groupSegsByDay(allSegs
); // sparse array
14436 var tableEl
= $('<table class="fc-list-table"><tbody/></table>');
14437 var tbodyEl
= tableEl
.find('tbody');
14439 for (dayIndex
= 0; dayIndex
< segsByDay
.length
; dayIndex
++) {
14440 daySegs
= segsByDay
[dayIndex
];
14441 if (daySegs
) { // sparse array, so might be undefined
14443 // append a day header
14444 tbodyEl
.append(this.dayHeaderHtml(
14445 this.view
.start
.clone().add(dayIndex
, 'days')
14448 this.sortEventSegs(daySegs
);
14450 for (i
= 0; i
< daySegs
.length
; i
++) {
14451 tbodyEl
.append(daySegs
[i
].el
); // append event row
14456 this.el
.empty().append(tableEl
);
14459 // Returns a sparse array of arrays, segs grouped by their dayIndex
14460 groupSegsByDay: function(segs
) {
14461 var segsByDay
= []; // sparse array
14464 for (i
= 0; i
< segs
.length
; i
++) {
14466 (segsByDay
[seg
.dayIndex
] || (segsByDay
[seg
.dayIndex
] = []))
14473 // generates the HTML for the day headers that live amongst the event rows
14474 dayHeaderHtml: function(dayDate
) {
14475 var view
= this.view
;
14476 var mainFormat
= view
.opt('listDayFormat');
14477 var altFormat
= view
.opt('listDayAltFormat');
14479 return '<tr class="fc-list-heading" data-date="' + dayDate
.format('YYYY-MM-DD') + '">' +
14480 '<td class="' + view
.widgetHeaderClass
+ '" colspan="3">' +
14482 view
.buildGotoAnchorHtml(
14484 { 'class': 'fc-list-heading-main' },
14485 htmlEscape(dayDate
.format(mainFormat
)) // inner HTML
14489 view
.buildGotoAnchorHtml(
14491 { 'class': 'fc-list-heading-alt' },
14492 htmlEscape(dayDate
.format(altFormat
)) // inner HTML
14499 // generates the HTML for a single event row
14500 fgSegHtml: function(seg
) {
14501 var view
= this.view
;
14502 var classes
= [ 'fc-list-item' ].concat(this.getSegCustomClasses(seg
));
14503 var bgColor
= this.getSegBackgroundColor(seg
);
14504 var event
= seg
.event
;
14505 var url
= event
.url
;
14508 if (event
.allDay
) {
14509 timeHtml
= view
.getAllDayHtml();
14511 else if (view
.isMultiDayEvent(event
)) { // if the event appears to span more than one day
14512 if (seg
.isStart
|| seg
.isEnd
) { // outer segment that probably lasts part of the day
14513 timeHtml
= htmlEscape(this.getEventTimeText(seg
));
14515 else { // inner segment that lasts the whole day
14516 timeHtml
= view
.getAllDayHtml();
14520 // Display the normal time text for the *event's* times
14521 timeHtml
= htmlEscape(this.getEventTimeText(event
));
14525 classes
.push('fc-has-url');
14528 return '<tr class="' + classes
.join(' ') + '">' +
14529 (this.displayEventTime
?
14530 '<td class="fc-list-item-time ' + view
.widgetContentClass
+ '">' +
14534 '<td class="fc-list-item-marker ' + view
.widgetContentClass
+ '">' +
14535 '<span class="fc-event-dot"' +
14537 ' style="background-color:' + bgColor
+ '"' :
14541 '<td class="fc-list-item-title ' + view
.widgetContentClass
+ '">' +
14542 '<a' + (url
? ' href="' + htmlEscape(url
) + '"' : '') + '>' +
14543 htmlEscape(seg
.event
.title
|| '') +
14555 buttonTextKey
: 'list', // what to lookup in locale files
14557 buttonText
: 'list', // text to display for English
14558 listDayFormat
: 'LL', // like "January 1, 2016"
14559 noEventsMessage
: 'No events to display'
14563 fcViews
.listDay
= {
14565 duration
: { days
: 1 },
14567 listDayFormat
: 'dddd' // day-of-week is all we need. full date is probably in header
14571 fcViews
.listWeek
= {
14573 duration
: { weeks
: 1 },
14575 listDayFormat
: 'dddd', // day-of-week is more important
14576 listDayAltFormat
: 'LL'
14580 fcViews
.listMonth
= {
14582 duration
: { month
: 1 },
14584 listDayAltFormat
: 'dddd' // day-of-week is nice-to-have
14588 fcViews
.listYear
= {
14590 duration
: { year
: 1 },
14592 listDayAltFormat
: 'dddd' // day-of-week is nice-to-have
14598 return FC
; // export for Node/CommonJS