You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by an...@apache.org on 2015/05/14 06:49:49 UTC
[66/90] [abbrv] incubator-ignite git commit: # ignite-843 WIP.
http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/32b52cb9/modules/webconfig/nodejs/node_modules/admin-lte/plugins/fullcalendar/fullcalendar.js
----------------------------------------------------------------------
diff --git a/modules/webconfig/nodejs/node_modules/admin-lte/plugins/fullcalendar/fullcalendar.js b/modules/webconfig/nodejs/node_modules/admin-lte/plugins/fullcalendar/fullcalendar.js
new file mode 100644
index 0000000..d52e824
--- /dev/null
+++ b/modules/webconfig/nodejs/node_modules/admin-lte/plugins/fullcalendar/fullcalendar.js
@@ -0,0 +1,9732 @@
+/*!
+ * FullCalendar v2.2.5
+ * Docs & License: http://arshaw.com/fullcalendar/
+ * (c) 2013 Adam Shaw
+ */
+
+(function(factory) {
+ if (typeof define === 'function' && define.amd) {
+ define([ 'jquery', 'moment' ], factory);
+ }
+ else {
+ factory(jQuery, moment);
+ }
+})(function($, moment) {
+
+ var defaults = {
+
+ titleRangeSeparator: ' \u2014 ', // emphasized dash
+ monthYearFormat: 'MMMM YYYY', // required for en. other languages rely on datepicker computable option
+
+ defaultTimedEventDuration: '02:00:00',
+ defaultAllDayEventDuration: { days: 1 },
+ forceEventDuration: false,
+ nextDayThreshold: '09:00:00', // 9am
+
+ // display
+ defaultView: 'month',
+ aspectRatio: 1.35,
+ header: {
+ left: 'title',
+ center: '',
+ right: 'today prev,next'
+ },
+ weekends: true,
+ weekNumbers: false,
+
+ weekNumberTitle: 'W',
+ weekNumberCalculation: 'local',
+
+ //editable: false,
+
+ // event ajax
+ lazyFetching: true,
+ startParam: 'start',
+ endParam: 'end',
+ timezoneParam: 'timezone',
+
+ timezone: false,
+
+ //allDayDefault: undefined,
+
+ // locale
+ isRTL: false,
+ defaultButtonText: {
+ prev: "prev",
+ next: "next",
+ prevYear: "prev year",
+ nextYear: "next year",
+ today: 'today',
+ month: 'month',
+ week: 'week',
+ day: 'day'
+ },
+
+ buttonIcons: {
+ prev: 'left-single-arrow',
+ next: 'right-single-arrow',
+ prevYear: 'left-double-arrow',
+ nextYear: 'right-double-arrow'
+ },
+
+ // jquery-ui theming
+ theme: false,
+ themeButtonIcons: {
+ prev: 'circle-triangle-w',
+ next: 'circle-triangle-e',
+ prevYear: 'seek-prev',
+ nextYear: 'seek-next'
+ },
+
+ dragOpacity: .75,
+ dragRevertDuration: 500,
+ dragScroll: true,
+
+ //selectable: false,
+ unselectAuto: true,
+
+ dropAccept: '*',
+
+ eventLimit: false,
+ eventLimitText: 'more',
+ eventLimitClick: 'popover',
+ dayPopoverFormat: 'LL',
+
+ handleWindowResize: true,
+ windowResizeDelay: 200 // milliseconds before an updateSize happens
+
+};
+
+
+var englishDefaults = {
+ dayPopoverFormat: 'dddd, MMMM D'
+};
+
+
+// right-to-left defaults
+var rtlDefaults = {
+ header: {
+ left: 'next,prev today',
+ center: '',
+ right: 'title'
+ },
+ buttonIcons: {
+ prev: 'right-single-arrow',
+ next: 'left-single-arrow',
+ prevYear: 'right-double-arrow',
+ nextYear: 'left-double-arrow'
+ },
+ themeButtonIcons: {
+ prev: 'circle-triangle-e',
+ next: 'circle-triangle-w',
+ nextYear: 'seek-prev',
+ prevYear: 'seek-next'
+ }
+};
+
+ var fc = $.fullCalendar = { version: "2.2.5" };
+var fcViews = fc.views = {};
+
+
+$.fn.fullCalendar = function(options) {
+ var args = Array.prototype.slice.call(arguments, 1); // for a possible method call
+ var res = this; // what this function will return (this jQuery object by default)
+
+ this.each(function(i, _element) { // loop each DOM element involved
+ var element = $(_element);
+ var calendar = element.data('fullCalendar'); // get the existing calendar object (if any)
+ var singleRes; // the returned value of this single method call
+
+ // a method call
+ if (typeof options === 'string') {
+ if (calendar && $.isFunction(calendar[options])) {
+ singleRes = calendar[options].apply(calendar, args);
+ if (!i) {
+ res = singleRes; // record the first method call result
+ }
+ if (options === 'destroy') { // for the destroy method, must remove Calendar object data
+ element.removeData('fullCalendar');
+ }
+ }
+ }
+ // a new calendar initialization
+ else if (!calendar) { // don't initialize twice
+ calendar = new Calendar(element, options);
+ element.data('fullCalendar', calendar);
+ calendar.render();
+ }
+ });
+
+ return res;
+};
+
+
+// function for adding/overriding defaults
+function setDefaults(d) {
+ mergeOptions(defaults, d);
+}
+
+
+// Recursively combines option hash-objects.
+// Better than `$.extend(true, ...)` because arrays are not traversed/copied.
+//
+// called like:
+// mergeOptions(target, obj1, obj2, ...)
+//
+function mergeOptions(target) {
+
+ function mergeIntoTarget(name, value) {
+ if ($.isPlainObject(value) && $.isPlainObject(target[name]) && !isForcedAtomicOption(name)) {
+ // merge into a new object to avoid destruction
+ target[name] = mergeOptions({}, target[name], value); // combine. `value` object takes precedence
+ }
+ else if (value !== undefined) { // only use values that are set and not undefined
+ target[name] = value;
+ }
+ }
+
+ for (var i=1; i<arguments.length; i++) {
+ $.each(arguments[i], mergeIntoTarget);
+ }
+
+ return target;
+}
+
+
+// overcome sucky view-option-hash and option-merging behavior messing with options it shouldn't
+function isForcedAtomicOption(name) {
+ // Any option that ends in "Time" or "Duration" is probably a Duration,
+ // and these will commonly be specified as plain objects, which we don't want to mess up.
+ return /(Time|Duration)$/.test(name);
+}
+// FIX: find a different solution for view-option-hashes and have a whitelist
+// for options that can be recursively merged.
+
+ var langOptionHash = fc.langs = {}; // initialize and expose
+
+
+// TODO: document the structure and ordering of a FullCalendar lang file
+// TODO: rename everything "lang" to "locale", like what the moment project did
+
+
+// Initialize jQuery UI datepicker translations while using some of the translations
+// Will set this as the default language for datepicker.
+fc.datepickerLang = function(langCode, dpLangCode, dpOptions) {
+
+ // get the FullCalendar internal option hash for this language. create if necessary
+ var fcOptions = langOptionHash[langCode] || (langOptionHash[langCode] = {});
+
+ // transfer some simple options from datepicker to fc
+ fcOptions.isRTL = dpOptions.isRTL;
+ fcOptions.weekNumberTitle = dpOptions.weekHeader;
+
+ // compute some more complex options from datepicker
+ $.each(dpComputableOptions, function(name, func) {
+ fcOptions[name] = func(dpOptions);
+ });
+
+ // is jQuery UI Datepicker is on the page?
+ if ($.datepicker) {
+
+ // Register the language data.
+ // FullCalendar and MomentJS use language codes like "pt-br" but Datepicker
+ // does it like "pt-BR" or if it doesn't have the language, maybe just "pt".
+ // Make an alias so the language can be referenced either way.
+ $.datepicker.regional[dpLangCode] =
+ $.datepicker.regional[langCode] = // alias
+ dpOptions;
+
+ // Alias 'en' to the default language data. Do this every time.
+ $.datepicker.regional.en = $.datepicker.regional[''];
+
+ // Set as Datepicker's global defaults.
+ $.datepicker.setDefaults(dpOptions);
+ }
+};
+
+
+// Sets FullCalendar-specific translations. Will set the language as the global default.
+fc.lang = function(langCode, newFcOptions) {
+ var fcOptions;
+ var momOptions;
+
+ // get the FullCalendar internal option hash for this language. create if necessary
+ fcOptions = langOptionHash[langCode] || (langOptionHash[langCode] = {});
+
+ // provided new options for this language? merge them in
+ if (newFcOptions) {
+ mergeOptions(fcOptions, newFcOptions);
+ }
+
+ // compute language options that weren't defined.
+ // always do this. newFcOptions can be undefined when initializing from i18n file,
+ // so no way to tell if this is an initialization or a default-setting.
+ momOptions = getMomentLocaleData(langCode); // will fall back to en
+ $.each(momComputableOptions, function(name, func) {
+ if (fcOptions[name] === undefined) {
+ fcOptions[name] = func(momOptions, fcOptions);
+ }
+ });
+
+ // set it as the default language for FullCalendar
+ defaults.lang = langCode;
+};
+
+
+// NOTE: can't guarantee any of these computations will run because not every language has datepicker
+// configs, so make sure there are English fallbacks for these in the defaults file.
+var dpComputableOptions = {
+
+ defaultButtonText: function(dpOptions) {
+ return {
+ // the translations sometimes wrongly contain HTML entities
+ prev: stripHtmlEntities(dpOptions.prevText),
+ next: stripHtmlEntities(dpOptions.nextText),
+ today: stripHtmlEntities(dpOptions.currentText)
+ };
+ },
+
+ // Produces format strings like "MMMM YYYY" -> "September 2014"
+ monthYearFormat: function(dpOptions) {
+ return dpOptions.showMonthAfterYear ?
+ 'YYYY[' + dpOptions.yearSuffix + '] MMMM' :
+ 'MMMM YYYY[' + dpOptions.yearSuffix + ']';
+ }
+
+};
+
+var momComputableOptions = {
+
+ // Produces format strings like "ddd MM/DD" -> "Fri 12/10"
+ dayOfMonthFormat: function(momOptions, fcOptions) {
+ var format = momOptions.longDateFormat('l'); // for the format like "M/D/YYYY"
+
+ // strip the year off the edge, as well as other misc non-whitespace chars
+ format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, '');
+
+ if (fcOptions.isRTL) {
+ format += ' ddd'; // for RTL, add day-of-week to end
+ }
+ else {
+ format = 'ddd ' + format; // for LTR, add day-of-week to beginning
+ }
+ return format;
+ },
+
+ // Produces format strings like "H(:mm)a" -> "6pm" or "6:30pm"
+ smallTimeFormat: function(momOptions) {
+ return momOptions.longDateFormat('LT')
+ .replace(':mm', '(:mm)')
+ .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs
+ .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
+ },
+
+ // Produces format strings like "H(:mm)t" -> "6p" or "6:30p"
+ extraSmallTimeFormat: function(momOptions) {
+ return momOptions.longDateFormat('LT')
+ .replace(':mm', '(:mm)')
+ .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs
+ .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand
+ },
+
+ // Produces format strings like "H:mm" -> "6:30" (with no AM/PM)
+ noMeridiemTimeFormat: function(momOptions) {
+ return momOptions.longDateFormat('LT')
+ .replace(/\s*a$/i, ''); // remove trailing AM/PM
+ }
+
+};
+
+
+// Returns moment's internal locale data. If doesn't exist, returns English.
+// Works with moment-pre-2.8
+function getMomentLocaleData(langCode) {
+ var func = moment.localeData || moment.langData;
+ return func.call(moment, langCode) ||
+ func.call(moment, 'en'); // the newer localData could return null, so fall back to en
+}
+
+
+// Initialize English by forcing computation of moment-derived options.
+// Also, sets it as the default.
+fc.lang('en', englishDefaults);
+
+// exports
+fc.intersectionToSeg = intersectionToSeg;
+fc.applyAll = applyAll;
+fc.debounce = debounce;
+
+
+/* FullCalendar-specific DOM Utilities
+----------------------------------------------------------------------------------------------------------------------*/
+
+
+// Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left
+// and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that.
+function compensateScroll(rowEls, scrollbarWidths) {
+ if (scrollbarWidths.left) {
+ rowEls.css({
+ 'border-left-width': 1,
+ 'margin-left': scrollbarWidths.left - 1
+ });
+ }
+ if (scrollbarWidths.right) {
+ rowEls.css({
+ 'border-right-width': 1,
+ 'margin-right': scrollbarWidths.right - 1
+ });
+ }
+}
+
+
+// Undoes compensateScroll and restores all borders/margins
+function uncompensateScroll(rowEls) {
+ rowEls.css({
+ 'margin-left': '',
+ 'margin-right': '',
+ 'border-left-width': '',
+ 'border-right-width': ''
+ });
+}
+
+
+// Make the mouse cursor express that an event is not allowed in the current area
+function disableCursor() {
+ $('body').addClass('fc-not-allowed');
+}
+
+
+// Returns the mouse cursor to its original look
+function enableCursor() {
+ $('body').removeClass('fc-not-allowed');
+}
+
+
+// Given a total available height to fill, have `els` (essentially child rows) expand to accomodate.
+// By default, all elements that are shorter than the recommended height are expanded uniformly, not considering
+// any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and
+// reduces the available height.
+function distributeHeight(els, availableHeight, shouldRedistribute) {
+
+ // *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions,
+ // and it is better to be shorter than taller, to avoid creating unnecessary scrollbars.
+
+ var minOffset1 = Math.floor(availableHeight / els.length); // for non-last element
+ var minOffset2 = Math.floor(availableHeight - minOffset1 * (els.length - 1)); // for last element *FLOORING NOTE*
+ var flexEls = []; // elements that are allowed to expand. array of DOM nodes
+ var flexOffsets = []; // amount of vertical space it takes up
+ var flexHeights = []; // actual css height
+ var usedHeight = 0;
+
+ undistributeHeight(els); // give all elements their natural height
+
+ // find elements that are below the recommended height (expandable).
+ // important to query for heights in a single first pass (to avoid reflow oscillation).
+ els.each(function(i, el) {
+ var minOffset = i === els.length - 1 ? minOffset2 : minOffset1;
+ var naturalOffset = $(el).outerHeight(true);
+
+ if (naturalOffset < minOffset) {
+ flexEls.push(el);
+ flexOffsets.push(naturalOffset);
+ flexHeights.push($(el).height());
+ }
+ else {
+ // this element stretches past recommended height (non-expandable). mark the space as occupied.
+ usedHeight += naturalOffset;
+ }
+ });
+
+ // readjust the recommended height to only consider the height available to non-maxed-out rows.
+ if (shouldRedistribute) {
+ availableHeight -= usedHeight;
+ minOffset1 = Math.floor(availableHeight / flexEls.length);
+ minOffset2 = Math.floor(availableHeight - minOffset1 * (flexEls.length - 1)); // *FLOORING NOTE*
+ }
+
+ // assign heights to all expandable elements
+ $(flexEls).each(function(i, el) {
+ var minOffset = i === flexEls.length - 1 ? minOffset2 : minOffset1;
+ var naturalOffset = flexOffsets[i];
+ var naturalHeight = flexHeights[i];
+ var newHeight = minOffset - (naturalOffset - naturalHeight); // subtract the margin/padding
+
+ if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things
+ $(el).height(newHeight);
+ }
+ });
+}
+
+
+// Undoes distrubuteHeight, restoring all els to their natural height
+function undistributeHeight(els) {
+ els.height('');
+}
+
+
+// Given `els`, a jQuery set of <td> cells, find the cell with the largest natural width and set the widths of all the
+// cells to be that width.
+// PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline
+function matchCellWidths(els) {
+ var maxInnerWidth = 0;
+
+ els.find('> *').each(function(i, innerEl) {
+ var innerWidth = $(innerEl).outerWidth();
+ if (innerWidth > maxInnerWidth) {
+ maxInnerWidth = innerWidth;
+ }
+ });
+
+ maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance
+
+ els.width(maxInnerWidth);
+
+ return maxInnerWidth;
+}
+
+
+// Turns a container element into a scroller if its contents is taller than the allotted height.
+// Returns true if the element is now a scroller, false otherwise.
+// NOTE: this method is best because it takes weird zooming dimensions into account
+function setPotentialScroller(containerEl, height) {
+ containerEl.height(height).addClass('fc-scroller');
+
+ // are scrollbars needed?
+ if (containerEl[0].scrollHeight - 1 > containerEl[0].clientHeight) { // !!! -1 because IE is often off-by-one :(
+ return true;
+ }
+
+ unsetScroller(containerEl); // undo
+ return false;
+}
+
+
+// Takes an element that might have been a scroller, and turns it back into a normal element.
+function unsetScroller(containerEl) {
+ containerEl.height('').removeClass('fc-scroller');
+}
+
+
+/* General DOM Utilities
+----------------------------------------------------------------------------------------------------------------------*/
+
+
+// borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51
+function getScrollParent(el) {
+ var position = el.css('position'),
+ scrollParent = el.parents().filter(function() {
+ var parent = $(this);
+ return (/(auto|scroll)/).test(
+ parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x')
+ );
+ }).eq(0);
+
+ return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent;
+}
+
+
+// Given a container element, return an object with the pixel values of the left/right scrollbars.
+// Left scrollbars might occur on RTL browsers (IE maybe?) but I have not tested.
+// PREREQUISITE: container element must have a single child with display:block
+function getScrollbarWidths(container) {
+ var containerLeft = container.offset().left;
+ var containerRight = containerLeft + container.width();
+ var inner = container.children();
+ var innerLeft = inner.offset().left;
+ var innerRight = innerLeft + inner.outerWidth();
+
+ return {
+ left: innerLeft - containerLeft,
+ right: containerRight - innerRight
+ };
+}
+
+
+// Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)
+function isPrimaryMouseButton(ev) {
+ return ev.which == 1 && !ev.ctrlKey;
+}
+
+
+/* FullCalendar-specific Misc Utilities
+----------------------------------------------------------------------------------------------------------------------*/
+
+
+// Creates a basic segment with the intersection of the two ranges. Returns undefined if no intersection.
+// Expects all dates to be normalized to the same timezone beforehand.
+// TODO: move to date section?
+function intersectionToSeg(subjectRange, constraintRange) {
+ var subjectStart = subjectRange.start;
+ var subjectEnd = subjectRange.end;
+ var constraintStart = constraintRange.start;
+ var constraintEnd = constraintRange.end;
+ var segStart, segEnd;
+ var isStart, isEnd;
+
+ if (subjectEnd > constraintStart && subjectStart < constraintEnd) { // in bounds at all?
+
+ if (subjectStart >= constraintStart) {
+ segStart = subjectStart.clone();
+ isStart = true;
+ }
+ else {
+ segStart = constraintStart.clone();
+ isStart = false;
+ }
+
+ if (subjectEnd <= constraintEnd) {
+ segEnd = subjectEnd.clone();
+ isEnd = true;
+ }
+ else {
+ segEnd = constraintEnd.clone();
+ isEnd = false;
+ }
+
+ return {
+ start: segStart,
+ end: segEnd,
+ isStart: isStart,
+ isEnd: isEnd
+ };
+ }
+}
+
+
+function smartProperty(obj, name) { // get a camel-cased/namespaced property of an object
+ obj = obj || {};
+ if (obj[name] !== undefined) {
+ return obj[name];
+ }
+ var parts = name.split(/(?=[A-Z])/),
+ i = parts.length - 1, res;
+ for (; i>=0; i--) {
+ res = obj[parts[i].toLowerCase()];
+ if (res !== undefined) {
+ return res;
+ }
+ }
+ return obj['default'];
+}
+
+
+/* Date Utilities
+----------------------------------------------------------------------------------------------------------------------*/
+
+var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ];
+var intervalUnits = [ 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond' ];
+
+
+// Diffs the two moments into a Duration where full-days are recorded first, then the remaining time.
+// Moments will have their timezones normalized.
+function diffDayTime(a, b) {
+ return moment.duration({
+ days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'),
+ ms: a.time() - b.time() // time-of-day from day start. disregards timezone
+ });
+}
+
+
+// Diffs the two moments via their start-of-day (regardless of timezone). Produces whole-day durations.
+function diffDay(a, b) {
+ return moment.duration({
+ days: a.clone().stripTime().diff(b.clone().stripTime(), 'days')
+ });
+}
+
+
+// Computes the larges whole-unit period of time, as a duration object.
+// For example, 48 hours will be {days:2} whereas 49 hours will be {hours:49}.
+// Accepts start/end, a range object, or an original duration object.
+/* (never used)
+function computeIntervalDuration(start, end) {
+ var durationInput = {};
+ var i, unit;
+ var val;
+
+ for (i = 0; i < intervalUnits.length; i++) {
+ unit = intervalUnits[i];
+ val = computeIntervalAs(unit, start, end);
+ if (val) {
+ break;
+ }
+ }
+
+ durationInput[unit] = val;
+ return moment.duration(durationInput);
+}
+*/
+
+
+// Computes the unit name of the largest whole-unit period of time.
+// For example, 48 hours will be "days" wherewas 49 hours will be "hours".
+// Accepts start/end, a range object, or an original duration object.
+function computeIntervalUnit(start, end) {
+ var i, unit;
+
+ for (i = 0; i < intervalUnits.length; i++) {
+ unit = intervalUnits[i];
+ if (computeIntervalAs(unit, start, end)) {
+ break;
+ }
+ }
+
+ return unit; // will be "milliseconds" if nothing else matches
+}
+
+
+// Computes the number of units the interval is cleanly comprised of.
+// If the given unit does not cleanly divide the interval a whole number of times, `false` is returned.
+// Accepts start/end, a range object, or an original duration object.
+function computeIntervalAs(unit, start, end) {
+ var val;
+
+ if (end != null) { // given start, end
+ val = end.diff(start, unit, true);
+ }
+ else if (moment.isDuration(start)) { // given duration
+ val = start.as(unit);
+ }
+ else { // given { start, end } range object
+ val = start.end.diff(start.start, unit, true);
+ }
+
+ if (val >= 1 && isInt(val)) {
+ return val;
+ }
+
+ return false;
+}
+
+
+function isNativeDate(input) {
+ return Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date;
+}
+
+
+// Returns a boolean about whether the given input is a time string, like "06:40:00" or "06:00"
+function isTimeString(str) {
+ return /^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str);
+}
+
+
+/* General Utilities
+----------------------------------------------------------------------------------------------------------------------*/
+
+var hasOwnPropMethod = {}.hasOwnProperty;
+
+
+// Create an object that has the given prototype. Just like Object.create
+function createObject(proto) {
+ var f = function() {};
+ f.prototype = proto;
+ return new f();
+}
+
+
+function copyOwnProps(src, dest) {
+ for (var name in src) {
+ if (hasOwnProp(src, name)) {
+ dest[name] = src[name];
+ }
+ }
+}
+
+
+function hasOwnProp(obj, name) {
+ return hasOwnPropMethod.call(obj, name);
+}
+
+
+// Is the given value a non-object non-function value?
+function isAtomic(val) {
+ return /undefined|null|boolean|number|string/.test($.type(val));
+}
+
+
+function applyAll(functions, thisObj, args) {
+ if ($.isFunction(functions)) {
+ functions = [ functions ];
+ }
+ if (functions) {
+ var i;
+ var ret;
+ for (i=0; i<functions.length; i++) {
+ ret = functions[i].apply(thisObj, args) || ret;
+ }
+ return ret;
+ }
+}
+
+
+function firstDefined() {
+ for (var i=0; i<arguments.length; i++) {
+ if (arguments[i] !== undefined) {
+ return arguments[i];
+ }
+ }
+}
+
+
+function htmlEscape(s) {
+ return (s + '').replace(/&/g, '&')
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/'/g, ''')
+ .replace(/"/g, '"')
+ .replace(/\n/g, '<br />');
+}
+
+
+function stripHtmlEntities(text) {
+ return text.replace(/&.*?;/g, '');
+}
+
+
+function capitaliseFirstLetter(str) {
+ return str.charAt(0).toUpperCase() + str.slice(1);
+}
+
+
+function compareNumbers(a, b) { // for .sort()
+ return a - b;
+}
+
+
+function isInt(n) {
+ return n % 1 === 0;
+}
+
+
+// Returns a function, that, as long as it continues to be invoked, will not
+// be triggered. The function will be called after it stops being called for
+// N milliseconds.
+// https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714
+function debounce(func, wait) {
+ var timeoutId;
+ var args;
+ var context;
+ var timestamp; // of most recent call
+ var later = function() {
+ var last = +new Date() - timestamp;
+ if (last < wait && last > 0) {
+ timeoutId = setTimeout(later, wait - last);
+ }
+ else {
+ timeoutId = null;
+ func.apply(context, args);
+ if (!timeoutId) {
+ context = args = null;
+ }
+ }
+ };
+
+ return function() {
+ context = this;
+ args = arguments;
+ timestamp = +new Date();
+ if (!timeoutId) {
+ timeoutId = setTimeout(later, wait);
+ }
+ };
+}
+
+ var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/;
+var ambigTimeOrZoneRegex =
+ /^\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+)?)?)?)?)?$/;
+var newMomentProto = moment.fn; // where we will attach our new methods
+var oldMomentProto = $.extend({}, newMomentProto); // copy of original moment methods
+var allowValueOptimization;
+var setUTCValues; // function defined below
+var setLocalValues; // function defined below
+
+
+// Creating
+// -------------------------------------------------------------------------------------------------
+
+// Creates a new moment, similar to the vanilla moment(...) constructor, but with
+// extra features (ambiguous time, enhanced formatting). When given an existing moment,
+// it will function as a clone (and retain the zone of the moment). Anything else will
+// result in a moment in the local zone.
+fc.moment = function() {
+ return makeMoment(arguments);
+};
+
+// Sames as fc.moment, but forces the resulting moment to be in the UTC timezone.
+fc.moment.utc = function() {
+ var mom = makeMoment(arguments, true);
+
+ // Force it into UTC because makeMoment doesn't guarantee it
+ // (if given a pre-existing moment for example)
+ if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone
+ mom.utc();
+ }
+
+ return mom;
+};
+
+// Same as fc.moment, but when given an ISO8601 string, the timezone offset is preserved.
+// ISO8601 strings with no timezone offset will become ambiguously zoned.
+fc.moment.parseZone = function() {
+ return makeMoment(arguments, true, true);
+};
+
+// Builds an enhanced moment from args. When given an existing moment, it clones. When given a
+// native Date, or called with no arguments (the current time), the resulting moment will be local.
+// Anything else needs to be "parsed" (a string or an array), and will be affected by:
+// parseAsUTC - if there is no zone information, should we parse the input in UTC?
+// parseZone - if there is zone information, should we force the zone of the moment?
+function makeMoment(args, parseAsUTC, parseZone) {
+ var input = args[0];
+ var isSingleString = args.length == 1 && typeof input === 'string';
+ var isAmbigTime;
+ var isAmbigZone;
+ var ambigMatch;
+ var mom;
+
+ if (moment.isMoment(input)) {
+ mom = moment.apply(null, args); // clone it
+ transferAmbigs(input, mom); // the ambig flags weren't transfered with the clone
+ }
+ else if (isNativeDate(input) || input === undefined) {
+ mom = moment.apply(null, args); // will be local
+ }
+ else { // "parsing" is required
+ isAmbigTime = false;
+ isAmbigZone = false;
+
+ if (isSingleString) {
+ if (ambigDateOfMonthRegex.test(input)) {
+ // accept strings like '2014-05', but convert to the first of the month
+ input += '-01';
+ args = [ input ]; // for when we pass it on to moment's constructor
+ isAmbigTime = true;
+ isAmbigZone = true;
+ }
+ else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) {
+ isAmbigTime = !ambigMatch[5]; // no time part?
+ isAmbigZone = true;
+ }
+ }
+ else if ($.isArray(input)) {
+ // arrays have no timezone information, so assume ambiguous zone
+ isAmbigZone = true;
+ }
+ // otherwise, probably a string with a format
+
+ if (parseAsUTC || isAmbigTime) {
+ mom = moment.utc.apply(moment, args);
+ }
+ else {
+ mom = moment.apply(null, args);
+ }
+
+ if (isAmbigTime) {
+ mom._ambigTime = true;
+ mom._ambigZone = true; // ambiguous time always means ambiguous zone
+ }
+ else if (parseZone) { // let's record the inputted zone somehow
+ if (isAmbigZone) {
+ mom._ambigZone = true;
+ }
+ else if (isSingleString) {
+ mom.zone(input); // if not a valid zone, will assign UTC
+ }
+ }
+ }
+
+ mom._fullCalendar = true; // flag for extended functionality
+
+ return mom;
+}
+
+
+// A clone method that works with the flags related to our enhanced functionality.
+// In the future, use moment.momentProperties
+newMomentProto.clone = function() {
+ var mom = oldMomentProto.clone.apply(this, arguments);
+
+ // these flags weren't transfered with the clone
+ transferAmbigs(this, mom);
+ if (this._fullCalendar) {
+ mom._fullCalendar = true;
+ }
+
+ return mom;
+};
+
+
+// Time-of-day
+// -------------------------------------------------------------------------------------------------
+
+// GETTER
+// Returns a Duration with the hours/minutes/seconds/ms values of the moment.
+// If the moment has an ambiguous time, a duration of 00:00 will be returned.
+//
+// SETTER
+// You can supply a Duration, a Moment, or a Duration-like argument.
+// When setting the time, and the moment has an ambiguous time, it then becomes unambiguous.
+newMomentProto.time = function(time) {
+
+ // Fallback to the original method (if there is one) if this moment wasn't created via FullCalendar.
+ // `time` is a generic enough method name where this precaution is necessary to avoid collisions w/ other plugins.
+ if (!this._fullCalendar) {
+ return oldMomentProto.time.apply(this, arguments);
+ }
+
+ if (time == null) { // getter
+ return moment.duration({
+ hours: this.hours(),
+ minutes: this.minutes(),
+ seconds: this.seconds(),
+ milliseconds: this.milliseconds()
+ });
+ }
+ else { // setter
+
+ this._ambigTime = false; // mark that the moment now has a time
+
+ if (!moment.isDuration(time) && !moment.isMoment(time)) {
+ time = moment.duration(time);
+ }
+
+ // The day value should cause overflow (so 24 hours becomes 00:00:00 of next day).
+ // Only for Duration times, not Moment times.
+ var dayHours = 0;
+ if (moment.isDuration(time)) {
+ dayHours = Math.floor(time.asDays()) * 24;
+ }
+
+ // We need to set the individual fields.
+ // Can't use startOf('day') then add duration. In case of DST at start of day.
+ return this.hours(dayHours + time.hours())
+ .minutes(time.minutes())
+ .seconds(time.seconds())
+ .milliseconds(time.milliseconds());
+ }
+};
+
+// Converts the moment to UTC, stripping out its time-of-day and timezone offset,
+// but preserving its YMD. A moment with a stripped time will display no time
+// nor timezone offset when .format() is called.
+newMomentProto.stripTime = function() {
+ var a;
+
+ if (!this._ambigTime) {
+
+ // get the values before any conversion happens
+ a = this.toArray(); // array of y/m/d/h/m/s/ms
+
+ this.utc(); // set the internal UTC flag (will clear the ambig flags)
+ setUTCValues(this, a.slice(0, 3)); // set the year/month/date. time will be zero
+
+ // Mark the time as ambiguous. This needs to happen after the .utc() call, which calls .zone(),
+ // which clears all ambig flags. Same with setUTCValues with moment-timezone.
+ this._ambigTime = true;
+ this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset
+ }
+
+ return this; // for chaining
+};
+
+// Returns if the moment has a non-ambiguous time (boolean)
+newMomentProto.hasTime = function() {
+ return !this._ambigTime;
+};
+
+
+// Timezone
+// -------------------------------------------------------------------------------------------------
+
+// Converts the moment to UTC, stripping out its timezone offset, but preserving its
+// YMD and time-of-day. A moment with a stripped timezone offset will display no
+// timezone offset when .format() is called.
+newMomentProto.stripZone = function() {
+ var a, wasAmbigTime;
+
+ if (!this._ambigZone) {
+
+ // get the values before any conversion happens
+ a = this.toArray(); // array of y/m/d/h/m/s/ms
+ wasAmbigTime = this._ambigTime;
+
+ this.utc(); // set the internal UTC flag (will clear the ambig flags)
+ setUTCValues(this, a); // will set the year/month/date/hours/minutes/seconds/ms
+
+ if (wasAmbigTime) {
+ // the above call to .utc()/.zone() unfortunately clears the ambig flags, so reassign
+ this._ambigTime = true;
+ }
+
+ // Mark the zone as ambiguous. This needs to happen after the .utc() call, which calls .zone(),
+ // which clears all ambig flags. Same with setUTCValues with moment-timezone.
+ this._ambigZone = true;
+ }
+
+ return this; // for chaining
+};
+
+// Returns of the moment has a non-ambiguous timezone offset (boolean)
+newMomentProto.hasZone = function() {
+ return !this._ambigZone;
+};
+
+// this method implicitly marks a zone (will get called upon .utc() and .local())
+newMomentProto.zone = function(tzo) {
+
+ if (tzo != null) { // setter
+ // these assignments needs to happen before the original zone method is called.
+ // I forget why, something to do with a browser crash.
+ this._ambigTime = false;
+ this._ambigZone = false;
+ }
+
+ return oldMomentProto.zone.apply(this, arguments);
+};
+
+// this method implicitly marks a zone
+newMomentProto.local = function() {
+ var a = this.toArray(); // year,month,date,hours,minutes,seconds,ms as an array
+ var wasAmbigZone = this._ambigZone;
+
+ oldMomentProto.local.apply(this, arguments); // will clear ambig flags
+
+ if (wasAmbigZone) {
+ // If the moment was ambiguously zoned, the date fields were stored as UTC.
+ // We want to preserve these, but in local time.
+ setLocalValues(this, a);
+ }
+
+ return this; // for chaining
+};
+
+
+// Formatting
+// -------------------------------------------------------------------------------------------------
+
+newMomentProto.format = function() {
+ if (this._fullCalendar && arguments[0]) { // an enhanced moment? and a format string provided?
+ return formatDate(this, arguments[0]); // our extended formatting
+ }
+ if (this._ambigTime) {
+ return oldMomentFormat(this, 'YYYY-MM-DD');
+ }
+ if (this._ambigZone) {
+ return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
+ }
+ return oldMomentProto.format.apply(this, arguments);
+};
+
+newMomentProto.toISOString = function() {
+ if (this._ambigTime) {
+ return oldMomentFormat(this, 'YYYY-MM-DD');
+ }
+ if (this._ambigZone) {
+ return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
+ }
+ return oldMomentProto.toISOString.apply(this, arguments);
+};
+
+
+// Querying
+// -------------------------------------------------------------------------------------------------
+
+// Is the moment within the specified range? `end` is exclusive.
+// FYI, this method is not a standard Moment method, so always do our enhanced logic.
+newMomentProto.isWithin = function(start, end) {
+ var a = commonlyAmbiguate([ this, start, end ]);
+ return a[0] >= a[1] && a[0] < a[2];
+};
+
+// When isSame is called with units, timezone ambiguity is normalized before the comparison happens.
+// If no units specified, the two moments must be identically the same, with matching ambig flags.
+newMomentProto.isSame = function(input, units) {
+ var a;
+
+ // only do custom logic if this is an enhanced moment
+ if (!this._fullCalendar) {
+ return oldMomentProto.isSame.apply(this, arguments);
+ }
+
+ if (units) {
+ a = commonlyAmbiguate([ this, input ], true); // normalize timezones but don't erase times
+ return oldMomentProto.isSame.call(a[0], a[1], units);
+ }
+ else {
+ input = fc.moment.parseZone(input); // normalize input
+ return oldMomentProto.isSame.call(this, input) &&
+ Boolean(this._ambigTime) === Boolean(input._ambigTime) &&
+ Boolean(this._ambigZone) === Boolean(input._ambigZone);
+ }
+};
+
+// Make these query methods work with ambiguous moments
+$.each([
+ 'isBefore',
+ 'isAfter'
+], function(i, methodName) {
+ newMomentProto[methodName] = function(input, units) {
+ var a;
+
+ // only do custom logic if this is an enhanced moment
+ if (!this._fullCalendar) {
+ return oldMomentProto[methodName].apply(this, arguments);
+ }
+
+ a = commonlyAmbiguate([ this, input ]);
+ return oldMomentProto[methodName].call(a[0], a[1], units);
+ };
+});
+
+
+// Misc Internals
+// -------------------------------------------------------------------------------------------------
+
+// given an array of moment-like inputs, return a parallel array w/ moments similarly ambiguated.
+// for example, of one moment has ambig time, but not others, all moments will have their time stripped.
+// set `preserveTime` to `true` to keep times, but only normalize zone ambiguity.
+// returns the original moments if no modifications are necessary.
+function commonlyAmbiguate(inputs, preserveTime) {
+ var anyAmbigTime = false;
+ var anyAmbigZone = false;
+ var len = inputs.length;
+ var moms = [];
+ var i, mom;
+
+ // parse inputs into real moments and query their ambig flags
+ for (i = 0; i < len; i++) {
+ mom = inputs[i];
+ if (!moment.isMoment(mom)) {
+ mom = fc.moment.parseZone(mom);
+ }
+ anyAmbigTime = anyAmbigTime || mom._ambigTime;
+ anyAmbigZone = anyAmbigZone || mom._ambigZone;
+ moms.push(mom);
+ }
+
+ // strip each moment down to lowest common ambiguity
+ // use clones to avoid modifying the original moments
+ for (i = 0; i < len; i++) {
+ mom = moms[i];
+ if (!preserveTime && anyAmbigTime && !mom._ambigTime) {
+ moms[i] = mom.clone().stripTime();
+ }
+ else if (anyAmbigZone && !mom._ambigZone) {
+ moms[i] = mom.clone().stripZone();
+ }
+ }
+
+ return moms;
+}
+
+// Transfers all the flags related to ambiguous time/zone from the `src` moment to the `dest` moment
+function transferAmbigs(src, dest) {
+ if (src._ambigTime) {
+ dest._ambigTime = true;
+ }
+ else if (dest._ambigTime) {
+ dest._ambigTime = false;
+ }
+
+ if (src._ambigZone) {
+ dest._ambigZone = true;
+ }
+ else if (dest._ambigZone) {
+ dest._ambigZone = false;
+ }
+}
+
+
+// Sets the year/month/date/etc values of the moment from the given array.
+// Inefficient because it calls each individual setter.
+function setMomentValues(mom, a) {
+ mom.year(a[0] || 0)
+ .month(a[1] || 0)
+ .date(a[2] || 0)
+ .hours(a[3] || 0)
+ .minutes(a[4] || 0)
+ .seconds(a[5] || 0)
+ .milliseconds(a[6] || 0);
+}
+
+// Can we set the moment's internal date directly?
+allowValueOptimization = '_d' in moment() && 'updateOffset' in moment;
+
+// Utility function. Accepts a moment and an array of the UTC year/month/date/etc values to set.
+// Assumes the given moment is already in UTC mode.
+setUTCValues = allowValueOptimization ? function(mom, a) {
+ // simlate what moment's accessors do
+ mom._d.setTime(Date.UTC.apply(Date, a));
+ moment.updateOffset(mom, false); // keepTime=false
+} : setMomentValues;
+
+// Utility function. Accepts a moment and an array of the local year/month/date/etc values to set.
+// Assumes the given moment is already in local mode.
+setLocalValues = allowValueOptimization ? function(mom, a) {
+ // simlate what moment's accessors do
+ mom._d.setTime(+new Date( // FYI, there is now way to apply an array of args to a constructor
+ a[0] || 0,
+ a[1] || 0,
+ a[2] || 0,
+ a[3] || 0,
+ a[4] || 0,
+ a[5] || 0,
+ a[6] || 0
+ ));
+ moment.updateOffset(mom, false); // keepTime=false
+} : setMomentValues;
+
+// Single Date Formatting
+// -------------------------------------------------------------------------------------------------
+
+
+// call this if you want Moment's original format method to be used
+function oldMomentFormat(mom, formatStr) {
+ return oldMomentProto.format.call(mom, formatStr); // oldMomentProto defined in moment-ext.js
+}
+
+
+// Formats `date` with a Moment formatting string, but allow our non-zero areas and
+// additional token.
+function formatDate(date, formatStr) {
+ return formatDateWithChunks(date, getFormatStringChunks(formatStr));
+}
+
+
+function formatDateWithChunks(date, chunks) {
+ var s = '';
+ var i;
+
+ for (i=0; i<chunks.length; i++) {
+ s += formatDateWithChunk(date, chunks[i]);
+ }
+
+ return s;
+}
+
+
+// addition formatting tokens we want recognized
+var tokenOverrides = {
+ t: function(date) { // "a" or "p"
+ return oldMomentFormat(date, 'a').charAt(0);
+ },
+ T: function(date) { // "A" or "P"
+ return oldMomentFormat(date, 'A').charAt(0);
+ }
+};
+
+
+function formatDateWithChunk(date, chunk) {
+ var token;
+ var maybeStr;
+
+ if (typeof chunk === 'string') { // a literal string
+ return chunk;
+ }
+ else if ((token = chunk.token)) { // a token, like "YYYY"
+ if (tokenOverrides[token]) {
+ return tokenOverrides[token](date); // use our custom token
+ }
+ return oldMomentFormat(date, token);
+ }
+ else if (chunk.maybe) { // a grouping of other chunks that must be non-zero
+ maybeStr = formatDateWithChunks(date, chunk.maybe);
+ if (maybeStr.match(/[1-9]/)) {
+ return maybeStr;
+ }
+ }
+
+ return '';
+}
+
+
+// Date Range Formatting
+// -------------------------------------------------------------------------------------------------
+// TODO: make it work with timezone offset
+
+// Using a formatting string meant for a single date, generate a range string, like
+// "Sep 2 - 9 2013", that intelligently inserts a separator where the dates differ.
+// If the dates are the same as far as the format string is concerned, just return a single
+// rendering of one date, without any separator.
+function formatRange(date1, date2, formatStr, separator, isRTL) {
+ var localeData;
+
+ date1 = fc.moment.parseZone(date1);
+ date2 = fc.moment.parseZone(date2);
+
+ localeData = (date1.localeData || date1.lang).call(date1); // works with moment-pre-2.8
+
+ // Expand localized format strings, like "LL" -> "MMMM D YYYY"
+ formatStr = localeData.longDateFormat(formatStr) || formatStr;
+ // BTW, this is not important for `formatDate` because it is impossible to put custom tokens
+ // or non-zero areas in Moment's localized format strings.
+
+ separator = separator || ' - ';
+
+ return formatRangeWithChunks(
+ date1,
+ date2,
+ getFormatStringChunks(formatStr),
+ separator,
+ isRTL
+ );
+}
+fc.formatRange = formatRange; // expose
+
+
+function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) {
+ var chunkStr; // the rendering of the chunk
+ var leftI;
+ var leftStr = '';
+ var rightI;
+ var rightStr = '';
+ var middleI;
+ var middleStr1 = '';
+ var middleStr2 = '';
+ var middleStr = '';
+
+ // Start at the leftmost side of the formatting string and continue until you hit a token
+ // that is not the same between dates.
+ for (leftI=0; leftI<chunks.length; leftI++) {
+ chunkStr = formatSimilarChunk(date1, date2, chunks[leftI]);
+ if (chunkStr === false) {
+ break;
+ }
+ leftStr += chunkStr;
+ }
+
+ // Similarly, start at the rightmost side of the formatting string and move left
+ for (rightI=chunks.length-1; rightI>leftI; rightI--) {
+ chunkStr = formatSimilarChunk(date1, date2, chunks[rightI]);
+ if (chunkStr === false) {
+ break;
+ }
+ rightStr = chunkStr + rightStr;
+ }
+
+ // The area in the middle is different for both of the dates.
+ // Collect them distinctly so we can jam them together later.
+ for (middleI=leftI; middleI<=rightI; middleI++) {
+ middleStr1 += formatDateWithChunk(date1, chunks[middleI]);
+ middleStr2 += formatDateWithChunk(date2, chunks[middleI]);
+ }
+
+ if (middleStr1 || middleStr2) {
+ if (isRTL) {
+ middleStr = middleStr2 + separator + middleStr1;
+ }
+ else {
+ middleStr = middleStr1 + separator + middleStr2;
+ }
+ }
+
+ return leftStr + middleStr + rightStr;
+}
+
+
+var similarUnitMap = {
+ Y: 'year',
+ M: 'month',
+ D: 'day', // day of month
+ d: 'day', // day of week
+ // prevents a separator between anything time-related...
+ A: 'second', // AM/PM
+ a: 'second', // am/pm
+ T: 'second', // A/P
+ t: 'second', // a/p
+ H: 'second', // hour (24)
+ h: 'second', // hour (12)
+ m: 'second', // minute
+ s: 'second' // second
+};
+// TODO: week maybe?
+
+
+// Given a formatting chunk, and given that both dates are similar in the regard the
+// formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`.
+function formatSimilarChunk(date1, date2, chunk) {
+ var token;
+ var unit;
+
+ if (typeof chunk === 'string') { // a literal string
+ return chunk;
+ }
+ else if ((token = chunk.token)) {
+ unit = similarUnitMap[token.charAt(0)];
+ // are the dates the same for this unit of measurement?
+ if (unit && date1.isSame(date2, unit)) {
+ return oldMomentFormat(date1, token); // would be the same if we used `date2`
+ // BTW, don't support custom tokens
+ }
+ }
+
+ return false; // the chunk is NOT the same for the two dates
+ // BTW, don't support splitting on non-zero areas
+}
+
+
+// Chunking Utils
+// -------------------------------------------------------------------------------------------------
+
+
+var formatStringChunkCache = {};
+
+
+function getFormatStringChunks(formatStr) {
+ if (formatStr in formatStringChunkCache) {
+ return formatStringChunkCache[formatStr];
+ }
+ return (formatStringChunkCache[formatStr] = chunkFormatString(formatStr));
+}
+
+
+// Break the formatting string into an array of chunks
+function chunkFormatString(formatStr) {
+ var chunks = [];
+ var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LT|(\w)\4*o?)|([^\w\[\(]+)/g; // TODO: more descrimination
+ var match;
+
+ while ((match = chunker.exec(formatStr))) {
+ if (match[1]) { // a literal string inside [ ... ]
+ chunks.push(match[1]);
+ }
+ else if (match[2]) { // non-zero formatting inside ( ... )
+ chunks.push({ maybe: chunkFormatString(match[2]) });
+ }
+ else if (match[3]) { // a formatting token
+ chunks.push({ token: match[3] });
+ }
+ else if (match[5]) { // an unenclosed literal string
+ chunks.push(match[5]);
+ }
+ }
+
+ return chunks;
+}
+
+ fc.Class = Class; // export
+
+// class that all other classes will inherit from
+function Class() { }
+
+// called upon a class to create a subclass
+Class.extend = function(members) {
+ var superClass = this;
+ var subClass;
+
+ members = members || {};
+
+ // ensure a constructor for the subclass, forwarding all arguments to the super-constructor if it doesn't exist
+ if (hasOwnProp(members, 'constructor')) {
+ subClass = members.constructor;
+ }
+ if (typeof subClass !== 'function') {
+ subClass = members.constructor = function() {
+ superClass.apply(this, arguments);
+ };
+ }
+
+ // build the base prototype for the subclass, which is an new object chained to the superclass's prototype
+ subClass.prototype = createObject(superClass.prototype);
+
+ // copy each member variable/method onto the the subclass's prototype
+ copyOwnProps(members, subClass.prototype);
+
+ // copy over all class variables/methods to the subclass, such as `extend` and `mixin`
+ copyOwnProps(superClass, subClass);
+
+ return subClass;
+};
+
+// adds new member variables/methods to the class's prototype.
+// can be called with another class, or a plain object hash containing new members.
+Class.mixin = function(members) {
+ copyOwnProps(members.prototype || members, this.prototype);
+};
+ /* A rectangular panel that is absolutely positioned over other content
+------------------------------------------------------------------------------------------------------------------------
+Options:
+ - className (string)
+ - content (HTML string or jQuery element set)
+ - parentEl
+ - top
+ - left
+ - right (the x coord of where the right edge should be. not a "CSS" right)
+ - autoHide (boolean)
+ - show (callback)
+ - hide (callback)
+*/
+
+var Popover = Class.extend({
+
+ isHidden: true,
+ options: null,
+ el: null, // the container element for the popover. generated by this object
+ documentMousedownProxy: null, // document mousedown handler bound to `this`
+ margin: 10, // the space required between the popover and the edges of the scroll container
+
+
+ constructor: function(options) {
+ this.options = options || {};
+ },
+
+
+ // Shows the popover on the specified position. Renders it if not already
+ show: function() {
+ if (this.isHidden) {
+ if (!this.el) {
+ this.render();
+ }
+ this.el.show();
+ this.position();
+ this.isHidden = false;
+ this.trigger('show');
+ }
+ },
+
+
+ // Hides the popover, through CSS, but does not remove it from the DOM
+ hide: function() {
+ if (!this.isHidden) {
+ this.el.hide();
+ this.isHidden = true;
+ this.trigger('hide');
+ }
+ },
+
+
+ // Creates `this.el` and renders content inside of it
+ render: function() {
+ var _this = this;
+ var options = this.options;
+
+ this.el = $('<div class="fc-popover"/>')
+ .addClass(options.className || '')
+ .css({
+ // position initially to the top left to avoid creating scrollbars
+ top: 0,
+ left: 0
+ })
+ .append(options.content)
+ .appendTo(options.parentEl);
+
+ // when a click happens on anything inside with a 'fc-close' className, hide the popover
+ this.el.on('click', '.fc-close', function() {
+ _this.hide();
+ });
+
+ if (options.autoHide) {
+ $(document).on('mousedown', this.documentMousedownProxy = $.proxy(this, 'documentMousedown'));
+ }
+ },
+
+
+ // Triggered when the user clicks *anywhere* in the document, for the autoHide feature
+ documentMousedown: function(ev) {
+ // only hide the popover if the click happened outside the popover
+ if (this.el && !$(ev.target).closest(this.el).length) {
+ this.hide();
+ }
+ },
+
+
+ // Hides and unregisters any handlers
+ destroy: function() {
+ this.hide();
+
+ if (this.el) {
+ this.el.remove();
+ this.el = null;
+ }
+
+ $(document).off('mousedown', this.documentMousedownProxy);
+ },
+
+
+ // Positions the popover optimally, using the top/left/right options
+ position: function() {
+ var options = this.options;
+ var origin = this.el.offsetParent().offset();
+ var width = this.el.outerWidth();
+ var height = this.el.outerHeight();
+ var windowEl = $(window);
+ var viewportEl = getScrollParent(this.el);
+ var viewportTop;
+ var viewportLeft;
+ var viewportOffset;
+ var top; // the "position" (not "offset") values for the popover
+ var left; //
+
+ // compute top and left
+ top = options.top || 0;
+ if (options.left !== undefined) {
+ left = options.left;
+ }
+ else if (options.right !== undefined) {
+ left = options.right - width; // derive the left value from the right value
+ }
+ else {
+ left = 0;
+ }
+
+ if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result
+ viewportEl = windowEl;
+ viewportTop = 0; // the window is always at the top left
+ viewportLeft = 0; // (and .offset() won't work if called here)
+ }
+ else {
+ viewportOffset = viewportEl.offset();
+ viewportTop = viewportOffset.top;
+ viewportLeft = viewportOffset.left;
+ }
+
+ // if the window is scrolled, it causes the visible area to be further down
+ viewportTop += windowEl.scrollTop();
+ viewportLeft += windowEl.scrollLeft();
+
+ // constrain to the view port. if constrained by two edges, give precedence to top/left
+ if (options.viewportConstrain !== false) {
+ top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin);
+ top = Math.max(top, viewportTop + this.margin);
+ left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin);
+ left = Math.max(left, viewportLeft + this.margin);
+ }
+
+ this.el.css({
+ top: top - origin.top,
+ left: left - origin.left
+ });
+ },
+
+
+ // Triggers a callback. Calls a function in the option hash of the same name.
+ // Arguments beyond the first `name` are forwarded on.
+ // TODO: better code reuse for this. Repeat code
+ trigger: function(name) {
+ if (this.options[name]) {
+ this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
+ }
+ }
+
+});
+
+ /* A "coordinate map" converts pixel coordinates into an associated cell, which has an associated date
+------------------------------------------------------------------------------------------------------------------------
+Common interface:
+
+ CoordMap.prototype = {
+ build: function() {},
+ getCell: function(x, y) {}
+ };
+
+*/
+
+/* Coordinate map for a grid component
+----------------------------------------------------------------------------------------------------------------------*/
+
+var GridCoordMap = Class.extend({
+
+ grid: null, // reference to the Grid
+ rowCoords: null, // array of {top,bottom} objects
+ colCoords: null, // array of {left,right} objects
+
+ containerEl: null, // container element that all coordinates are constrained to. optionally assigned
+ minX: null,
+ maxX: null, // exclusive
+ minY: null,
+ maxY: null, // exclusive
+
+
+ constructor: function(grid) {
+ this.grid = grid;
+ },
+
+
+ // Queries the grid for the coordinates of all the cells
+ build: function() {
+ this.rowCoords = this.grid.computeRowCoords();
+ this.colCoords = this.grid.computeColCoords();
+ this.computeBounds();
+ },
+
+
+ // Clears the coordinates data to free up memory
+ clear: function() {
+ this.rowCoords = null;
+ this.colCoords = null;
+ },
+
+
+ // Given a coordinate of the document, gets the associated cell. If no cell is underneath, returns null
+ getCell: function(x, y) {
+ var rowCoords = this.rowCoords;
+ var colCoords = this.colCoords;
+ var hitRow = null;
+ var hitCol = null;
+ var i, coords;
+ var cell;
+
+ if (this.inBounds(x, y)) {
+
+ for (i = 0; i < rowCoords.length; i++) {
+ coords = rowCoords[i];
+ if (y >= coords.top && y < coords.bottom) {
+ hitRow = i;
+ break;
+ }
+ }
+
+ for (i = 0; i < colCoords.length; i++) {
+ coords = colCoords[i];
+ if (x >= coords.left && x < coords.right) {
+ hitCol = i;
+ break;
+ }
+ }
+
+ if (hitRow !== null && hitCol !== null) {
+ cell = this.grid.getCell(hitRow, hitCol);
+ cell.grid = this.grid; // for DragListener's isCellsEqual. dragging between grids
+ return cell;
+ }
+ }
+
+ return null;
+ },
+
+
+ // If there is a containerEl, compute the bounds into min/max values
+ computeBounds: function() {
+ var containerOffset;
+
+ if (this.containerEl) {
+ containerOffset = this.containerEl.offset();
+ this.minX = containerOffset.left;
+ this.maxX = containerOffset.left + this.containerEl.outerWidth();
+ this.minY = containerOffset.top;
+ this.maxY = containerOffset.top + this.containerEl.outerHeight();
+ }
+ },
+
+
+ // Determines if the given coordinates are in bounds. If no `containerEl`, always true
+ inBounds: function(x, y) {
+ if (this.containerEl) {
+ return x >= this.minX && x < this.maxX && y >= this.minY && y < this.maxY;
+ }
+ return true;
+ }
+
+});
+
+
+/* Coordinate map that is a combination of multiple other coordinate maps
+----------------------------------------------------------------------------------------------------------------------*/
+
+var ComboCoordMap = Class.extend({
+
+ coordMaps: null, // an array of CoordMaps
+
+
+ constructor: function(coordMaps) {
+ this.coordMaps = coordMaps;
+ },
+
+
+ // Builds all coordMaps
+ build: function() {
+ var coordMaps = this.coordMaps;
+ var i;
+
+ for (i = 0; i < coordMaps.length; i++) {
+ coordMaps[i].build();
+ }
+ },
+
+
+ // Queries all coordMaps for the cell underneath the given coordinates, returning the first result
+ getCell: function(x, y) {
+ var coordMaps = this.coordMaps;
+ var cell = null;
+ var i;
+
+ for (i = 0; i < coordMaps.length && !cell; i++) {
+ cell = coordMaps[i].getCell(x, y);
+ }
+
+ return cell;
+ },
+
+
+ // Clears all coordMaps
+ clear: function() {
+ var coordMaps = this.coordMaps;
+ var i;
+
+ for (i = 0; i < coordMaps.length; i++) {
+ coordMaps[i].clear();
+ }
+ }
+
+});
+
+ /* Tracks mouse movements over a CoordMap and raises events about which cell the mouse is over.
+----------------------------------------------------------------------------------------------------------------------*/
+// TODO: very useful to have a handler that gets called upon cellOut OR when dragging stops (for cleanup)
+
+var DragListener = Class.extend({
+
+ coordMap: null,
+ options: null,
+
+ isListening: false,
+ isDragging: false,
+
+ // the cell the mouse was over when listening started
+ origCell: null,
+
+ // the cell the mouse is over
+ cell: null,
+
+ // coordinates of the initial mousedown
+ mouseX0: null,
+ mouseY0: null,
+
+ // handler attached to the document, bound to the DragListener's `this`
+ mousemoveProxy: null,
+ mouseupProxy: null,
+
+ scrollEl: null,
+ scrollBounds: null, // { top, bottom, left, right }
+ scrollTopVel: null, // pixels per second
+ scrollLeftVel: null, // pixels per second
+ scrollIntervalId: null, // ID of setTimeout for scrolling animation loop
+ scrollHandlerProxy: null, // this-scoped function for handling when scrollEl is scrolled
+
+ scrollSensitivity: 30, // pixels from edge for scrolling to start
+ scrollSpeed: 200, // pixels per second, at maximum speed
+ scrollIntervalMs: 50, // millisecond wait between scroll increment
+
+
+ constructor: function(coordMap, options) {
+ this.coordMap = coordMap;
+ this.options = options || {};
+ },
+
+
+ // Call this when the user does a mousedown. Will probably lead to startListening
+ mousedown: function(ev) {
+ if (isPrimaryMouseButton(ev)) {
+
+ ev.preventDefault(); // prevents native selection in most browsers
+
+ this.startListening(ev);
+
+ // start the drag immediately if there is no minimum distance for a drag start
+ if (!this.options.distance) {
+ this.startDrag(ev);
+ }
+ }
+ },
+
+
+ // Call this to start tracking mouse movements
+ startListening: function(ev) {
+ var scrollParent;
+ var cell;
+
+ if (!this.isListening) {
+
+ // grab scroll container and attach handler
+ if (ev && this.options.scroll) {
+ scrollParent = getScrollParent($(ev.target));
+ if (!scrollParent.is(window) && !scrollParent.is(document)) {
+ this.scrollEl = scrollParent;
+
+ // scope to `this`, and use `debounce` to make sure rapid calls don't happen
+ this.scrollHandlerProxy = debounce($.proxy(this, 'scrollHandler'), 100);
+ this.scrollEl.on('scroll', this.scrollHandlerProxy);
+ }
+ }
+
+ this.computeCoords(); // relies on `scrollEl`
+
+ // get info on the initial cell and its coordinates
+ if (ev) {
+ cell = this.getCell(ev);
+ this.origCell = cell;
+
+ this.mouseX0 = ev.pageX;
+ this.mouseY0 = ev.pageY;
+ }
+
+ $(document)
+ .on('mousemove', this.mousemoveProxy = $.proxy(this, 'mousemove'))
+ .on('mouseup', this.mouseupProxy = $.proxy(this, 'mouseup'))
+ .on('selectstart', this.preventDefault); // prevents native selection in IE<=8
+
+ this.isListening = true;
+ this.trigger('listenStart', ev);
+ }
+ },
+
+
+ // Recomputes the drag-critical positions of elements
+ computeCoords: function() {
+ this.coordMap.build();
+ this.computeScrollBounds();
+ },
+
+
+ // Called when the user moves the mouse
+ mousemove: function(ev) {
+ var minDistance;
+ var distanceSq; // current distance from mouseX0/mouseY0, squared
+
+ if (!this.isDragging) { // if not already dragging...
+ // then start the drag if the minimum distance criteria is met
+ minDistance = this.options.distance || 1;
+ distanceSq = Math.pow(ev.pageX - this.mouseX0, 2) + Math.pow(ev.pageY - this.mouseY0, 2);
+ if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem
+ this.startDrag(ev);
+ }
+ }
+
+ if (this.isDragging) {
+ this.drag(ev); // report a drag, even if this mousemove initiated the drag
+ }
+ },
+
+
+ // Call this to initiate a legitimate drag.
+ // This function is called internally from this class, but can also be called explicitly from outside
+ startDrag: function(ev) {
+ var cell;
+
+ if (!this.isListening) { // startDrag must have manually initiated
+ this.startListening();
+ }
+
+ if (!this.isDragging) {
+ this.isDragging = true;
+ this.trigger('dragStart', ev);
+
+ // report the initial cell the mouse is over
+ // especially important if no min-distance and drag starts immediately
+ cell = this.getCell(ev); // this might be different from this.origCell if the min-distance is large
+ if (cell) {
+ this.cellOver(cell);
+ }
+ }
+ },
+
+
+ // Called while the mouse is being moved and when we know a legitimate drag is taking place
+ drag: function(ev) {
+ var cell;
+
+ if (this.isDragging) {
+ cell = this.getCell(ev);
+
+ if (!isCellsEqual(cell, this.cell)) { // a different cell than before?
+ if (this.cell) {
+ this.cellOut();
+ }
+ if (cell) {
+ this.cellOver(cell);
+ }
+ }
+
+ this.dragScroll(ev); // will possibly cause scrolling
+ }
+ },
+
+
+ // Called when a the mouse has just moved over a new cell
+ cellOver: function(cell) {
+ this.cell = cell;
+ this.trigger('cellOver', cell, isCellsEqual(cell, this.origCell));
+ },
+
+
+ // Called when the mouse has just moved out of a cell
+ cellOut: function() {
+ if (this.cell) {
+ this.trigger('cellOut', this.cell);
+ this.cell = null;
+ }
+ },
+
+
+ // Called when the user does a mouseup
+ mouseup: function(ev) {
+ this.stopDrag(ev);
+ this.stopListening(ev);
+ },
+
+
+ // Called when the drag is over. Will not cause listening to stop however.
+ // A concluding 'cellOut' event will NOT be triggered.
+ stopDrag: function(ev) {
+ if (this.isDragging) {
+ this.stopScrolling();
+ this.trigger('dragStop', ev);
+ this.isDragging = false;
+ }
+ },
+
+
+ // Call this to stop listening to the user's mouse events
+ stopListening: function(ev) {
+ if (this.isListening) {
+
+ // remove the scroll handler if there is a scrollEl
+ if (this.scrollEl) {
+ this.scrollEl.off('scroll', this.scrollHandlerProxy);
+ this.scrollHandlerProxy = null;
+ }
+
+ $(document)
+ .off('mousemove', this.mousemoveProxy)
+ .off('mouseup', this.mouseupProxy)
+ .off('selectstart', this.preventDefault);
+
+ this.mousemoveProxy = null;
+ this.mouseupProxy = null;
+
+ this.isListening = false;
+ this.trigger('listenStop', ev);
+
+ this.origCell = this.cell = null;
+ this.coordMap.clear();
+ }
+ },
+
+
+ // Gets the cell underneath the coordinates for the given mouse event
+ getCell: function(ev) {
+ return this.coordMap.getCell(ev.pageX, ev.pageY);
+ },
+
+
+ // Triggers a callback. Calls a function in the option hash of the same name.
+ // Arguments beyond the first `name` are forwarded on.
+ trigger: function(name) {
+ if (this.options[name]) {
+ this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
+ }
+ },
+
+
+ // Stops a given mouse event from doing it's native browser action. In our case, text selection.
+ preventDefault: function(ev) {
+ ev.preventDefault();
+ },
+
+
+ /* Scrolling
+ ------------------------------------------------------------------------------------------------------------------*/
+
+
+ // Computes and stores the bounding rectangle of scrollEl
+ computeScrollBounds: function() {
+ var el = this.scrollEl;
+ var offset;
+
+ if (el) {
+ offset = el.offset();
+ this.scrollBounds = {
+ top: offset.top,
+ left: offset.left,
+ bottom: offset.top + el.outerHeight(),
+ right: offset.left + el.outerWidth()
+ };
+ }
+ },
+
+
+ // Called when the dragging is in progress and scrolling should be updated
+ dragScroll: function(ev) {
+ var sensitivity = this.scrollSensitivity;
+ var bounds = this.scrollBounds;
+ var topCloseness, bottomCloseness;
+ var leftCloseness, rightCloseness;
+ var topVel = 0;
+ var leftVel = 0;
+
+ if (bounds) { // only scroll if scrollEl exists
+
+ // compute closeness to edges. valid range is from 0.0 - 1.0
+ topCloseness = (sensitivity - (ev.pageY - bounds.top)) / sensitivity;
+ bottomCloseness = (sensitivity - (bounds.bottom - ev.pageY)) / sensitivity;
+ leftCloseness = (sensitivity - (ev.pageX - bounds.left)) / sensitivity;
+ rightCloseness = (sensitivity - (bounds.right - ev.pageX)) / sensitivity;
+
+ // translate vertical closeness into velocity.
+ // mouse must be completely in bounds for velocity to happen.
+ if (topCloseness >= 0 && topCloseness <= 1) {
+ topVel = topCloseness * this.scrollSpeed * -1; // negative. for scrolling up
+ }
+ else if (bottomCloseness >= 0 && bottomCloseness <= 1) {
+ topVel = bottomCloseness * this.scrollSpeed;
+ }
+
+ // translate horizontal closeness into velocity
+ if (leftCloseness >= 0 && leftCloseness <= 1) {
+ leftVel = leftCloseness * this.scrollSpeed * -1; // negative. for scrolling left
+ }
+ else if (rightCloseness >= 0 && rightCloseness <= 1) {
+ leftVel = rightCloseness * this.scrollSpeed;
+ }
+ }
+
+ this.setScrollVel(topVel, leftVel);
+ },
+
+
+ // Sets the speed-of-scrolling for the scrollEl
+ setScrollVel: function(topVel, leftVel) {
+
+ this.scrollTopVel = topVel;
+ this.scrollLeftVel = leftVel;
+
+ this.constrainScrollVel(); // massages into realistic values
+
+ // if there is non-zero velocity, and an animation loop hasn't already started, then START
+ if ((this.scrollTopVel || this.scrollLeftVel) && !this.scrollIntervalId) {
+ this.scrollIntervalId = setInterval(
+ $.proxy(this, 'scrollIntervalFunc'), // scope to `this`
+ this.scrollIntervalMs
+ );
+ }
+ },
+
+
+ // Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way
+ constrainScrollVel: function() {
+ var el = this.scrollEl;
+
+ if (this.scrollTopVel < 0) { // scrolling up?
+ if (el.scrollTop() <= 0) { // already scrolled all the way up?
+ this.scrollTopVel = 0;
+ }
+ }
+ else if (this.scrollTopVel > 0) { // scrolling down?
+ if (el.scrollTop() + el[0].clientHeight >= el[0].scrollHeight) { // already scrolled all the way down?
+ this.scrollTopVel = 0;
+ }
+ }
+
+ if (this.scrollLeftVel < 0) { // scrolling left?
+ if (el.scrollLeft() <= 0) { // already scrolled all the left?
+ this.scrollLeftVel = 0;
+ }
+ }
+ else if (this.scrollLeftVel > 0) { // scrolling right?
+ if (el.scrollLeft() + el[0].clientWidth >= el[0].scrollWidth) { // already scrolled all the way right?
+ this.scrollLeftVel = 0;
+ }
+ }
+ },
+
+
+ // This function gets called during every iteration of the scrolling animation loop
+ scrollIntervalFunc: function() {
+ var el = this.scrollEl;
+ var frac = this.scrollIntervalMs / 1000; // considering animation frequency, what the vel should be mult'd by
+
+ // change the value of scrollEl's scroll
+ if (this.scrollTopVel) {
+ el.scrollTop(el.scrollTop() + this.scrollTopVel * frac);
+ }
+ if (this.scrollLeftVel) {
+ el.scrollLeft(el.scrollLeft() + this.scrollLeftVel * frac);
+ }
+
+ this.constrainScrollVel(); // since the scroll values changed, recompute the velocities
+
+ // if scrolled all the way, which causes the vels to be zero, stop the animation loop
+ if (!this.scrollTopVel && !this.scrollLeftVel) {
+ this.stopScrolling();
+ }
+ },
+
+
+ // Kills any existing scrolling animation loop
+ stopScrolling: function() {
+ if (this.scrollIntervalId) {
+ clearInterval(this.scrollIntervalId);
+ this.scrollIntervalId = null;
+
+ // when all done with scrolling, recompute positions since they probably changed
+ this.computeCoords();
+ }
+ },
+
+
+ // Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce)
+ scrollHandler: function() {
+ // recompute all coordinates, but *only* if this is *not* part of our scrolling animation
+ if (!this.scrollIntervalId) {
+ this.computeCoords();
+ }
+ }
+
+});
+
+
+// Returns `true` if the cells are identically equal. `false` otherwise.
+// They must have the same row, col, and be from the same grid.
+// Two null values will be considered equal, as two "out of the grid" states are the same.
+function isCellsEqual(cell1, cell2) {
+
+ if (!cell1 && !cell2) {
+ return true;
+ }
+
+ if (cell1 && cell2) {
+ return cell1.grid === cell2.grid &&
+ cell1.row === cell2.row &&
+ cell1.col === cell2.col;
+ }
+
+ return false;
+}
+
+ /* Creates a clone of an element and lets it track the mouse as it moves
+----------------------------------------------------------------------------------------------------------------------*/
+
+var MouseFollower = Class.extend({
+
+ options: null,
+
+ sourceEl: null, // the element that will be cloned and made to look like it is dragging
+ el: null, // the clone of `sourceEl` that will track the mouse
+ parentEl: null, // the element that `el` (the clone) will be attached to
+
+ // the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl
+ top0: null,
+ left0: null,
+
+ // the initial position of the mouse
+ mouseY0: null,
+ mouseX0: null,
+
+ // the number of pixels the mouse has moved from its initial position
+ topDelta: null,
+ leftDelta: null,
+
+ mousemoveProxy: null, // document mousemove handler, bound to the MouseFollower's `this`
+
+ isFollowing: false,
+ isHidden: false,
+ isAnimating: false, // doing the revert animation?
+
+ constructor: function(sourceEl, options) {
+ this.options = options = options || {};
+ this.sourceEl = sourceEl;
+ this.parentEl = options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent
+ },
+
+
+ // Causes the element to start following the mouse
+ start: function(ev) {
+ if (!this.isFollowing) {
+ this.isFollowing = true;
+
+ this.mouseY0 = ev.pageY;
+ this.mouseX0 = ev.pageX;
+ this.topDelta = 0;
+ this.leftDelta = 0;
+
+ if (!this.isHidden) {
+ this.updatePosition();
+ }
+
+ $(document).on('mousemove', this.mousemoveProxy = $.proxy(this, 'mousemove'));
+ }
+ },
+
+
+ // Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position.
+ // `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately.
+ stop: function(shouldRevert, callback) {
+ var _this = this;
+ var revertDuration = this.options.revertDuration;
+
+ function complete() {
+ this.isAnimating = false;
+ _this.destroyEl();
+
+ this.top0 = this.left0 = null; // reset state for future updatePosition calls
+
+ if (callback) {
+ callback();
+ }
+ }
+
+ if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time
+ this.isFollowing = false;
+
+ $(document).off('mousemove', this.mousemoveProxy);
+
+ if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation?
+ this.isAnimating = true;
+ this.el.animate({
+ top: this.top0,
+ left: this.left0
+ }, {
+ duration: revertDuration,
+ complete: complete
+ });
+ }
+ else {
+ complete();
+ }
+ }
+ },
+
+
+ // Gets the tracking element. Create it if necessary
+ getEl: function() {
+ var el = this.el;
+
+ if (!el) {
+ this.sourceEl.width(); // hack to force IE8 to compute correct bounding box
+ el = this.el = this.sourceEl.clone()
+ .css({
+ position: 'absolute',
+ visibility: '', // in case original element was hidden (commonly through hideEvents())
+ display: this.isHidden ? 'none' : '', // for when initially hidden
+ margin: 0,
+ right: 'auto', // erase and set width instead
+ bottom: 'auto', // erase and set height instead
+ width: this.sourceEl.width(), // explicit height in case there was a 'right' value
+ height: this.sourceEl.height(), // explicit width in case there was a 'bottom' value
+ opacity: this.options.opacity || '',
+ zIndex: this.options.zIndex
+ })
+ .appendTo(this.parentEl);
+ }
+
+ return el;
+ },
+
+
+ // Removes the tracking element if it has already been created
+ destroyEl: function() {
+ if (this.el) {
+ this.el.remove();
+ this.el = null;
+ }
+ },
+
+
+ // Update the CSS position of the tracking element
+ updatePosition: function() {
+ var sourceOffset;
+ var origin;
+
+ this.getEl(); // ensure this.el
+
+ // make sure origin info was computed
+ if (this.top0 === null) {
+ this.sourceEl.width(); // hack to force IE8 to compute correct bounding box
+ sourceOffset = this.sourceEl.offset();
+ origin = this.el.offsetParent().offset();
+ this.top0 = sourceOffset.top - origin.top;
+ this.left0 = sourceOffset.left - origin.left;
+ }
+
+ this.el.css({
+ top: this.top0 + this.topDelta,
+ left: this.left0 + this.leftDelta
+ });
+ },
+
+
+ // Gets called when the user moves the mouse
+ mousemove: function(ev) {
+ this.topDelta = ev.pageY - this.mouseY0;
+ this.leftDelta = ev.pageX - this.mouseX0;
+
+ if (!this.isHidden) {
+ this.updatePosition();
+ }
+ },
+
+
+ // Temporarily makes the tracking element invisible. Can be called before following starts
+ hide: function() {
+ if (!this.isHidden) {
+ this.isHidden = true;
+ if (this.el) {
+ this.el.hide();
+ }
+ }
+ },
+
+
+ // Show the tracking element after it has been temporarily hidden
+ show: function() {
+ if (this.isHidden) {
+ this.isHidden = false;
+ this.updatePosition();
+ this.getEl().show();
+ }
+ }
+
+});
+
+ /* A utility class for rendering <tr> rows.
+----------------------------------------------------------------------------------------------------------------------*/
+// It leverages methods of the subclass and the View to determine custom rendering behavior for each row "type"
+// (such as highlight rows, day rows, helper rows, etc).
+
+var RowRenderer = Class.extend({
+
+ view: null, // a View object
+ isRTL: null, // shortcut to the view's isRTL option
+ cellHtml: '<td/>', // plain default HTML used for a cell when no other is available
+
+
+ constructor: function(view) {
+ this.view = view;
+ this.isRTL = view.opt('isRTL');
+ },
+
+
+ // Renders the HTML for a row, leveraging custom cell-HTML-renderers based on the `rowType`.
+ // Also applies the "intro" and "outro" cells, which are specified by the subclass and views.
+ // `row` is an optional row number.
+ rowHtml: function(rowType, row) {
+ var renderCell = this.getHtmlRenderer('cell', rowType);
+ var rowCellHtml = '';
+ var col;
+ var cell;
+
+ row = row || 0;
+
+ for (col = 0; col < this.colCnt; col++) {
+ cell = this.getCell(row, col);
+ rowCellHtml += renderCell(cell);
+ }
+
+ rowCellHtml = this.bookendCells(rowCellHtml, rowType, row); // apply intro and outro
+
+ return '<tr>' + rowCellHtml + '</tr>';
+ },
+
+
+ // Applies the "intro" and "outro" HTML to the given cells.
+ // Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro.
+ // `cells` can be an HTML string of <td>'s or a jQuery <tr> element
+ // `row` is an optional row number.
+ bookendCells: function(cells, rowType, row) {
+ var intro = this.getHtmlRenderer('intro', rowType)(row || 0);
+ var outro = this.getHtmlRenderer('outro', rowType)(row || 0);
+ var prependHtml = this.isRTL ? outro : intro;
+ var appendHtml = this.isRTL ? intro : outro;
+
+ if (typeof cells === 'string') {
+ return prependHtml + cells + appendHtml;
+ }
+ else { // a jQuery <tr> element
+ return cells.prepend(prependHtml).append(appendHtml);
+ }
+ },
+
+
+ // Returns an HTML-rendering function given a specific `rendererName` (like cell, intro, or outro) and a specific
+ // `rowType` (like day, eventSkeleton, helperSkeleton), which is optional.
+ // If a renderer for the specific rowType doesn't exist, it will fall back to a generic renderer.
+ // We will query the View object first for any custom rendering functions, then the methods of the subclass.
+ getHtmlRenderer: function(rendererName, rowType) {
+ var view = this.view;
+ var generalName; // like "cellHtml"
+ var specificName; // like "dayCellHtml". based on rowType
+ var provider; // either the View or the RowRenderer subclass, whichever provided the method
+ var renderer;
+
+ generalName = rendererName + 'Html';
+ if (rowType) {
+ specificName = rowType + capitaliseFirstLetter(rendererName) + 'Html';
+ }
+
+ if (specificName && (renderer = view[specificName])) {
+ provider = view;
+ }
+ else if (specificName && (renderer = this[specificName])) {
+ provider = this;
+ }
+ else if ((renderer = view[generalName])) {
+ provider = view;
+ }
+ else if ((renderer = this[generalName])) {
+ provider = this;
+ }
+
+ if (typeof renderer === 'function') {
+ return function() {
+ return renderer.apply(provider, arguments) || ''; // use correct `this` and always return a string
+ };
+ }
+
+ // the rendered can be a plain string as well. if not specified, always an empty string.
+ return function() {
+ return renderer || '';
+ };
+ }
+
+});
+
+ /* An abstract class comprised of a "grid" of cells that each represent a specific datetime
+----------------------------------------------------------------------------------------------------------------------*/
+
+var Grid = fc.Grid = RowRenderer.extend({
+
+ start: null, // the date of the first cell
+ end: null, // the date after the last cell
+
+ rowCnt: 0, // number of rows
+ colCnt: 0, // number of cols
+ rowData: null, // array of objects, holding misc data for each row
+ colData: null, // array of objects, holding misc data for each column
+
+ el: null, // the containing element
+ coordMap: null, // a GridCoordMap that converts pixel values to datetimes
+ elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name.
+
+ documentDragStartProxy: null, // binds the Grid's scope to documentDragStart (in DayGrid.events)
+
+ // derived from options
+ colHeadFormat: null, // TODO: move to another class. not applicable to all Grids
+ eventTimeFormat: null,
+ displayEventEnd: null,
+
+
+ constructor: function() {
+ RowRenderer.apply(this, arguments); // call the super-constructor
+
+ this.coordMap = new GridCoordMap(this);
+ this.elsByFill = {};
+ this.documentDragStartProxy = $.proxy(this, 'documentDragStart');
+ },
+
+
+ // Renders the grid into the `el` element.
+ // Subclasses should override and call this super-method when done.
+ render: function() {
+ this.bindHandlers();
+ },
+
+
+ // Called when the grid's resources need to be cleaned up
+ destroy: function() {
+ this.unbindHandlers();
+ },
+
+
+ /* Options
+ ------------------------------------------------------------------------------------------------------------------*/
+
+
+ // Generates the format string used for the text in column headers, if not explicitly defined by 'columnFormat'
+ // TODO: move to another class. not applicable to all Grids
+ computeColHeadFormat: function() {
+ // subclasses must implement if they want to use headHtml()
+ },
+
+
+ // Generates the format string used for event time text, if not explicitly defined by 'timeFormat'
+ computeEventTimeFormat: function() {
+ return this.view.opt('smallTimeFormat');
+ },
+
+
+ // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd'
+ computeDisplayEventEnd: function() {
+ return false;
+ },
+
+
+ /* Dates
+ ------------------------------------------------------------------------------------------------------------------*/
+
+
+ // Tells the grid about what period of time to display. Grid will subsequently compute dates for cell system.
+ setRange: function(range) {
+ var view = this.view;
+
+ this.start = range.start.clone();
+ this.end = range.end.clone();
+
+ this.rowData = [];
+ this.colData = [];
+ this.updateCells();
+
+ // Populate option-derived settings. Look for override first, then compute if necessary.
+ this.colHeadFormat = view.opt('columnFormat') || this.computeColHeadFormat();
+ this.eventTimeFormat = view.opt('timeFormat') || this.computeEventTimeFormat();
+ this.displayEventEnd = view.opt('displayEventEnd');
+ if (this.displayEventEnd == null) {
+ this.displayEventEnd = this.computeDisplayEventEnd();
+ }
+ },
+
+
+ // Responsible for setting rowCnt/colCnt and any other row/col data
+ updateCells: function() {
+ // subclasses must implement
+ },
+
+
+ // Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects
+ rangeToSegs: function(range) {
+ // subclasses must implement
+ },
+
+
+ /* Cells
+ ------------------------------------------------------------------------------------------------------------------*/
+ // NOTE: columns are ordered left-to-right
+
+
+ // Gets an object containing row/col number, misc data, and range information about the cell.
+ // Accepts row/col values, an object with row/col properties, or a single-number offset from the first cell.
+ getCell: function(row, col) {
+ var cell;
+
+ if (col == null) {
+ if (typeof row === 'number') { // a single-number offset
+ col = row % this.colCnt;
+ row = Math.floor(row / this.colCnt);
+ }
+ else { // an object with row/col properties
+ col = row.col;
+ row = row.row;
+ }
+ }
+
+ cell = { row: row, col: col };
+
+ $.extend(cell, this.getRowData(row), this.getColData(col));
+ $.extend(cell, this.computeCellRange(cell));
+
+ return cell;
+ },
+
+
+ // Given a cell object with index and misc data, generates a range object
+ computeCellRange: function(cell) {
+ // subclasses must implement
+ },
+
+
+ // Retrieves misc data about the given row
+ getRowData: function(row) {
+ return this.rowData[row] || {};
+ },
+
+
+ // Retrieves misc data baout the given column
+ getColData: function(col) {
+ return this.colData[col] || {};
+ },
+
+
+ // Retrieves the element representing the given row
+ getRowEl: function(row) {
+ // subclasses should implement if leveraging the default getCellDayEl() or computeRowCoords()
+ },
+
+
+ // Retrieves the element representing the given column
+ getColEl: function(col) {
+ // subclasses should implement if leveraging the default getCellDayEl() or computeColCoords()
+ },
+
+
+ // Given a cell object, returns the element that represents the cell's whole-day
+ getCellDayEl: function(cell) {
+ return this.getColEl(cell.col) || this.getRowEl(cell.row);
+ },
+
+
+ /* Cell Coordinates
+ ------------------------------------------------------------------------------------------------------------------*/
+
+
+ // Computes the top/bottom coordinates of all rows.
+ // By default, queries the dimensions of the element provided by getRowEl().
+ computeRowCoords: function() {
+ var items = [];
+ var i, el;
+ var item;
+
+ for (i = 0; i < this.rowCnt; i++) {
+ el = this.getRowEl(i);
+ item = {
+ top: el.offset().top
+ };
+ if (i > 0) {
+ items[i - 1].bottom = item.top;
+ }
+ items.push(item);
+ }
+ item.bottom = item.top + el.outerHeight();
+
+ return items;
+ },
+
+
+ // Computes the left/right coordinates of all rows.
+ // By default, queries the dimensions of the element provided by getColEl().
+ computeColCoords: function() {
+ var items = [];
+ var i, el;
+ var item;
+
+ for (i = 0; i < this.colCnt; i++) {
+ el = this.getColEl(i);
+ item = {
+ left: el.offset().left
+ };
+ if (i > 0) {
+ items[i - 1].right = item.left;
+ }
+ items.push(item);
+ }
+ item.right = item.left + el.outerWidth();
+
+ return items;
+ },
+
+
+ /* Handlers
+ ------------------------------------------------------------------------------------------------------------------*/
+
+
+ // Attaches handlers to DOM
+ bindHandlers: function() {
+ var _this = this;
+
+ // attach a handler to the grid's root element.
+ // we don't need to clean up in unbindHandlers or destroy, because when jQuery removes the element from the
+ // DOM it automatically unregisters the handlers.
+ this.el.on('mousedown', function(ev) {
+ if (
+ !$(ev.target).is('.fc-event-container *, .fc-more') && // not an an event element, or "more.." link
+ !$(ev.target).closest('.fc-popover').length // not on a popover (like the "more.." events one)
+ ) {
+ _this.dayMousedown(ev);
+ }
+ });
+
+ // attach event-element-related handlers. in Grid.events
+ // same garbage collection note as above.
+ this.bindSegHandlers();
+
+ $(document).on('dragstart', this.documentDragStartProxy); // jqui drag
+ },
+
+
+ // Unattaches handlers from the DOM
+ unbindHandlers: function() {
+ $(document).off('dragstart', this.documentDragStartProxy); // jqui drag
+ },
+
+
+ // Process a mousedown on an element that represents a day. For day clicking and selecting.
+ dayMousedown: function(ev) {
+ var _this = this;
+ var view = this.view;
+ var isSelectable = view.opt('selectable');
+ var dayClickCell; // null if invalid dayClick
+ var selectionRange; // null if invalid selection
+
+ // this listener tracks a mousedown on a day element, and a subsequent drag.
+ // if the drag ends on the same day, it is a 'dayClick'.
+ // if 'selectable' is enabled, this listener also detects selections.
+ var dragListener = new DragListener(this.coordMap, {
+ //distance: 5, // needs more work if we want dayClick to fire correctly
+ scroll: view.opt('dragScroll'),
+ dragStart: function() {
+ view.unselect(); // since we could be rendering a new selection, we want to clear any old one
+ },
+ cellOver: function(cell, isOrig) {
+ var origCell = dragListener.origCell;
+ if (origCell) { // click needs to have started on a cell
+ dayClickCell = isOrig ? cell : null; // single-cell selection is a day click
+ if (isSelectable) {
+ selectionRange = _this.computeSelection(origCell, cell);
+ if (selectionRange) {
+ _this.renderSelection(selectionRange);
+ }
+ else {
+ disableCursor();
+ }
+ }
+ }
+ },
+ cellOut: function(cell) {
+ dayClickCell = null;
+ selectionRange = null;
+ _this.destroySelection();
+ enableCursor();
+ },
+ listenStop: function(ev) {
+ if (dayClickCell) {
+ view.trigger('dayClick', _this.getCellDayEl(dayClickCell), dayClickCell.start, ev);
+ }
+ if (selectionRange) {
+ // the selection will already have been rendered. just report it
+ view.reportSelection(selectionRange, ev);
+ }
+ enableCursor();
+ }
+ });
+
+ dragListener.mousedown(ev); // start listening, which will eventually initiate a dragStart
+ },
+
+
+ /* Event Helper
+ ------------------------------------------------------------------------------------------------------------------*/
+ // TODO: should probably move this to Grid.events, like we did event dragging / resizing
+
+
+ // Renders a mock event over the given range.
+ // The range's end can be null, in which case the mock event that is rendered will have a null end time.
+ // `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging.
+ renderRangeHelper: function(range, sourceSeg) {
+ var fakeEvent;
+
+ fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible
+ fakeEvent.start = range.start.clone();
+ fakeEvent.end = range.end ? range.end.clone() : null;
+ fakeEvent.allDay = null; // force it to be freshly computed by normalizeEventDateProps
+ this.view.calendar.normalizeEventDateProps(fakeEvent);
+
+ // this extra className will be useful for differentiating real events from mock events in CSS
+ fakeEvent.className = (fakeEvent.className || []).concat('fc-helper');
+
+ // if something external is being dragged in, don't render a resizer
+ if (!sourceSeg) {
+ fakeEvent.editable = false;
+ }
+
+ this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering
+ },
+
+
+ // Renders a mock event
+ renderHelper: function(event, sourceSeg) {
+ // subclasses must implement
+ },
+
+
+ // Unrenders a mock event
+ destroyHelper: function() {
+ // subclasses must implement
+ },
+
+
+ /* Selection
+ ------------------------------------------------------------------------------------------------------------------*/
+
+
+ // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses.
+ renderSelection: function(range) {
+ this.renderHighlight(range);
+ },
+
+
+ // Unrenders any visual indications of a selection. Will unrender a highlight by default.
+ destroySelection: function() {
+ this.destroyHighlight();
+ },
+
+
+ // Given the first and last cells of a selection, returns a range object.
+ // Will return something falsy if the selection is invalid (when outside of selectionConstraint for example).
+ // Subclasses can override and provide additional data in the range object. Will be passed to renderSelection().
+ computeSelection: function(firstCell, lastCell) {
+ var dates = [
+ firstCell.start,
+ firstCell.end,
+ lastCell.start,
+ lastCell.end
+ ];
+ var range;
+
+ dates.sort(compareNumbers); // sorts chronologically. works with Moments
+
+ range = {
+ start: dates[0].clone(),
+ end: dates[3].clone()
+ };
+
+ if (!this.view.calendar.isSelectionRangeAllowed(range)) {
+ return null;
+ }
+
+ return range;
+ },
+
+
+ /* Highlight
+ ------------------------------------------------------------------------------------------------------------------*/
+
+
+ // Renders an emphasis on the given date range. `start` is inclusive. `end` is exclusive.
+ renderHighlight: function(range) {
+ this.renderFill('highlight', this.rangeToSegs(range));
<TRUNCATED>