UNPKG

gentelella

Version:

Gentellela Admin is a free to use Bootstrap admin template

1,737 lines (1,383 loc) 354 kB
/*! * FullCalendar v2.7.1 * Docs & License: http://fullcalendar.io/ * (c) 2016 Adam Shaw */ (function(factory) { if (typeof define === 'function' && define.amd) { define([ 'jquery', 'moment' ], factory); } else if (typeof exports === 'object') { // Node/CommonJS module.exports = factory(require('jquery'), require('moment')); } else { factory(jQuery, moment); } })(function($, moment) { ;; var FC = $.fullCalendar = { version: "2.7.1", internalApiVersion: 3 }; var fcViews = FC.views = {}; FC.isTouch = 'ontouchstart' in document; $.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; }; var complexOptions = [ // names of options that are objects whose properties should be combined 'header', 'buttonText', 'buttonIcons', 'themeButtonIcons' ]; // Merges an array of option objects into a single object function mergeOptions(optionObjs) { return mergeProps(optionObjs, complexOptions); } // Given options specified for the calendar's constructor, massages any legacy options into a non-legacy form. // Converts View-Option-Hashes into the View-Specific-Options format. function massageOverrides(input) { var overrides = { views: input.views || {} }; // the output. ensure a `views` hash var subObj; // iterate through all option override properties (except `views`) $.each(input, function(name, val) { if (name != 'views') { // could the value be a legacy View-Option-Hash? if ( $.isPlainObject(val) && !/(time|duration|interval)$/i.test(name) && // exclude duration options. might be given as objects $.inArray(name, complexOptions) == -1 // complex options aren't allowed to be View-Option-Hashes ) { subObj = null; // iterate through the properties of this possible View-Option-Hash value $.each(val, function(subName, subVal) { // is the property targeting a view? if (/^(month|week|day|default|basic(Week|Day)?|agenda(Week|Day)?)$/.test(subName)) { if (!overrides.views[subName]) { // ensure the view-target entry exists overrides.views[subName] = {}; } overrides.views[subName][name] = subVal; // record the value in the `views` object } else { // a non-View-Option-Hash property if (!subObj) { subObj = {}; } subObj[subName] = subVal; // accumulate these unrelated values for later } }); if (subObj) { // non-View-Option-Hash properties? transfer them as-is overrides[name] = subObj; } } else { overrides[name] = val; // transfer normal options as-is } } }); return overrides; } ;; // exports FC.intersectRanges = intersectRanges; FC.applyAll = applyAll; FC.debounce = debounce; FC.isInt = isInt; FC.htmlEscape = htmlEscape; FC.cssToStr = cssToStr; FC.proxy = proxy; FC.capitaliseFirstLetter = capitaliseFirstLetter; /* 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('> span').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; } // Given one element that resides inside another, // Subtracts the height of the inner element from the outer element. function subtractInnerElHeight(outerEl, innerEl) { var both = outerEl.add(innerEl); var diff; // effin' IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked both.css({ position: 'relative', // cause a reflow, which will force fresh dimension recalculation left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll }); diff = outerEl.outerHeight() - innerEl.outerHeight(); // grab the dimensions both.css({ position: '', left: '' }); // undo hack return diff; } /* Element Geom Utilities ----------------------------------------------------------------------------------------------------------------------*/ FC.getOuterRect = getOuterRect; FC.getClientRect = getClientRect; FC.getContentRect = getContentRect; FC.getScrollbarWidths = getScrollbarWidths; // 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; } // Queries the outer bounding area of a jQuery element. // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive). // Origin is optional. function getOuterRect(el, origin) { var offset = el.offset(); var left = offset.left - (origin ? origin.left : 0); var top = offset.top - (origin ? origin.top : 0); return { left: left, right: left + el.outerWidth(), top: top, bottom: top + el.outerHeight() }; } // Queries the area within the margin/border/scrollbars of a jQuery element. Does not go within the padding. // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive). // Origin is optional. // NOTE: should use clientLeft/clientTop, but very unreliable cross-browser. function getClientRect(el, origin) { var offset = el.offset(); var scrollbarWidths = getScrollbarWidths(el); var left = offset.left + getCssFloat(el, 'border-left-width') + scrollbarWidths.left - (origin ? origin.left : 0); var top = offset.top + getCssFloat(el, 'border-top-width') + scrollbarWidths.top - (origin ? origin.top : 0); return { left: left, right: left + el[0].clientWidth, // clientWidth includes padding but NOT scrollbars top: top, bottom: top + el[0].clientHeight // clientHeight includes padding but NOT scrollbars }; } // Queries the area within the margin/border/padding of a jQuery element. Assumed not to have scrollbars. // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive). // Origin is optional. function getContentRect(el, origin) { var offset = el.offset(); // just outside of border, margin not included var left = offset.left + getCssFloat(el, 'border-left-width') + getCssFloat(el, 'padding-left') - (origin ? origin.left : 0); var top = offset.top + getCssFloat(el, 'border-top-width') + getCssFloat(el, 'padding-top') - (origin ? origin.top : 0); return { left: left, right: left + el.width(), top: top, bottom: top + el.height() }; } // Returns the computed left/right/top/bottom scrollbar widths for the given jQuery element. // NOTE: should use clientLeft/clientTop, but very unreliable cross-browser. function getScrollbarWidths(el) { var leftRightWidth = el.innerWidth() - el[0].clientWidth; // the paddings cancel out, leaving the scrollbars var widths = { left: 0, right: 0, top: 0, bottom: el.innerHeight() - el[0].clientHeight // the paddings cancel out, leaving the bottom scrollbar }; if (getIsLeftRtlScrollbars() && el.css('direction') == 'rtl') { // is the scrollbar on the left side? widths.left = leftRightWidth; } else { widths.right = leftRightWidth; } return widths; } // Logic for determining if, when the element is right-to-left, the scrollbar appears on the left side var _isLeftRtlScrollbars = null; function getIsLeftRtlScrollbars() { // responsible for caching the computation if (_isLeftRtlScrollbars === null) { _isLeftRtlScrollbars = computeIsLeftRtlScrollbars(); } return _isLeftRtlScrollbars; } function computeIsLeftRtlScrollbars() { // creates an offscreen test element, then removes it var el = $('<div><div/></div>') .css({ position: 'absolute', top: -1000, left: 0, border: 0, padding: 0, overflow: 'scroll', direction: 'rtl' }) .appendTo('body'); var innerEl = el.children(); var res = innerEl.offset().left > el.offset().left; // is the inner div shifted to accommodate a left scrollbar? el.remove(); return res; } // Retrieves a jQuery element's computed CSS value as a floating-point number. // If the queried value is non-numeric (ex: IE can return "medium" for border width), will just return zero. function getCssFloat(el, prop) { return parseFloat(el.css(prop)) || 0; } /* Mouse / Touch Utilities ----------------------------------------------------------------------------------------------------------------------*/ FC.preventDefault = preventDefault; // Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac) function isPrimaryMouseButton(ev) { return ev.which == 1 && !ev.ctrlKey; } function getEvX(ev) { if (ev.pageX !== undefined) { return ev.pageX; } var touches = ev.originalEvent.touches; if (touches) { return touches[0].pageX; } } function getEvY(ev) { if (ev.pageY !== undefined) { return ev.pageY; } var touches = ev.originalEvent.touches; if (touches) { return touches[0].pageY; } } function getEvIsTouch(ev) { return /^touch/.test(ev.type); } function preventSelection(el) { el.addClass('fc-unselectable') .on('selectstart', preventDefault); } // Stops a mouse/touch event from doing it's native browser action function preventDefault(ev) { ev.preventDefault(); } /* General Geometry Utils ----------------------------------------------------------------------------------------------------------------------*/ FC.intersectRects = intersectRects; // Returns a new rectangle that is the intersection of the two rectangles. If they don't intersect, returns false function intersectRects(rect1, rect2) { var res = { left: Math.max(rect1.left, rect2.left), right: Math.min(rect1.right, rect2.right), top: Math.max(rect1.top, rect2.top), bottom: Math.min(rect1.bottom, rect2.bottom) }; if (res.left < res.right && res.top < res.bottom) { return res; } return false; } // Returns a new point that will have been moved to reside within the given rectangle function constrainPoint(point, rect) { return { left: Math.min(Math.max(point.left, rect.left), rect.right), top: Math.min(Math.max(point.top, rect.top), rect.bottom) }; } // Returns a point that is the center of the given rectangle function getRectCenter(rect) { return { left: (rect.left + rect.right) / 2, top: (rect.top + rect.bottom) / 2 }; } // Subtracts point2's coordinates from point1's coordinates, returning a delta function diffPoints(point1, point2) { return { left: point1.left - point2.left, top: point1.top - point2.top }; } /* Object Ordering by Field ----------------------------------------------------------------------------------------------------------------------*/ FC.parseFieldSpecs = parseFieldSpecs; FC.compareByFieldSpecs = compareByFieldSpecs; FC.compareByFieldSpec = compareByFieldSpec; FC.flexibleCompare = flexibleCompare; function parseFieldSpecs(input) { var specs = []; var tokens = []; var i, token; if (typeof input === 'string') { tokens = input.split(/\s*,\s*/); } else if (typeof input === 'function') { tokens = [ input ]; } else if ($.isArray(input)) { tokens = input; } for (i = 0; i < tokens.length; i++) { token = tokens[i]; if (typeof token === 'string') { specs.push( token.charAt(0) == '-' ? { field: token.substring(1), order: -1 } : { field: token, order: 1 } ); } else if (typeof token === 'function') { specs.push({ func: token }); } } return specs; } function compareByFieldSpecs(obj1, obj2, fieldSpecs) { var i; var cmp; for (i = 0; i < fieldSpecs.length; i++) { cmp = compareByFieldSpec(obj1, obj2, fieldSpecs[i]); if (cmp) { return cmp; } } return 0; } function compareByFieldSpec(obj1, obj2, fieldSpec) { if (fieldSpec.func) { return fieldSpec.func(obj1, obj2); } return flexibleCompare(obj1[fieldSpec.field], obj2[fieldSpec.field]) * (fieldSpec.order || 1); } function flexibleCompare(a, b) { if (!a && !b) { return 0; } if (b == null) { return -1; } if (a == null) { return 1; } if ($.type(a) === 'string' || $.type(b) === 'string') { return String(a).localeCompare(String(b)); } return a - b; } /* FullCalendar-specific Misc Utilities ----------------------------------------------------------------------------------------------------------------------*/ // Computes 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 intersectRanges(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 }; } } /* Date Utilities ----------------------------------------------------------------------------------------------------------------------*/ FC.computeIntervalUnit = computeIntervalUnit; FC.divideRangeByDuration = divideRangeByDuration; FC.divideDurationByDuration = divideDurationByDuration; FC.multiplyDuration = multiplyDuration; FC.durationHasTime = durationHasTime; 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') }); } // Diffs two moments, producing a duration, made of a whole-unit-increment of the given unit. Uses rounding. function diffByUnit(a, b, unit) { return moment.duration( Math.round(a.diff(b, unit, true)), // returnFloat=true unit ); } // Computes the unit name of the largest whole-unit period of time. // For example, 48 hours will be "days" whereas 49 hours will be "hours". // Accepts start/end, a range object, or an original duration object. function computeIntervalUnit(start, end) { var i, unit; var val; for (i = 0; i < intervalUnits.length; i++) { unit = intervalUnits[i]; val = computeRangeAs(unit, start, end); if (val >= 1 && isInt(val)) { break; } } return unit; // will be "milliseconds" if nothing else matches } // Computes the number of units (like "hours") in the given range. // Range can be a {start,end} object, separate start/end args, or a Duration. // Results are based on Moment's .as() and .diff() methods, so results can depend on internal handling // of month-diffing logic (which tends to vary from version to version). function computeRangeAs(unit, start, end) { if (end != null) { // given start, end return end.diff(start, unit, true); } else if (moment.isDuration(start)) { // given duration return start.as(unit); } else { // given { start, end } range object return start.end.diff(start.start, unit, true); } } // Intelligently divides a range (specified by a start/end params) by a duration function divideRangeByDuration(start, end, dur) { var months; if (durationHasTime(dur)) { return (end - start) / dur; } months = dur.asMonths(); if (Math.abs(months) >= 1 && isInt(months)) { return end.diff(start, 'months', true) / months; } return end.diff(start, 'days', true) / dur.asDays(); } // Intelligently divides one duration by another function divideDurationByDuration(dur1, dur2) { var months1, months2; if (durationHasTime(dur1) || durationHasTime(dur2)) { return dur1 / dur2; } months1 = dur1.asMonths(); months2 = dur2.asMonths(); if ( Math.abs(months1) >= 1 && isInt(months1) && Math.abs(months2) >= 1 && isInt(months2) ) { return months1 / months2; } return dur1.asDays() / dur2.asDays(); } // Intelligently multiplies a duration by a number function multiplyDuration(dur, n) { var months; if (durationHasTime(dur)) { return moment.duration(dur * n); } months = dur.asMonths(); if (Math.abs(months) >= 1 && isInt(months)) { return moment.duration({ months: months * n }); } return moment.duration({ days: dur.asDays() * n }); } // Returns a boolean about whether the given duration has any time parts (hours/minutes/seconds/ms) function durationHasTime(dur) { return Boolean(dur.hours() || dur.minutes() || dur.seconds() || dur.milliseconds()); } 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); } /* Logging and Debug ----------------------------------------------------------------------------------------------------------------------*/ FC.log = function() { var console = window.console; if (console && console.log) { return console.log.apply(console, arguments); } }; FC.warn = function() { var console = window.console; if (console && console.warn) { return console.warn.apply(console, arguments); } else { return FC.log.apply(FC, arguments); } }; /* General Utilities ----------------------------------------------------------------------------------------------------------------------*/ var hasOwnPropMethod = {}.hasOwnProperty; // Merges an array of objects into a single object. // The second argument allows for an array of property names who's object values will be merged together. function mergeProps(propObjs, complexProps) { var dest = {}; var i, name; var complexObjs; var j, val; var props; if (complexProps) { for (i = 0; i < complexProps.length; i++) { name = complexProps[i]; complexObjs = []; // collect the trailing object values, stopping when a non-object is discovered for (j = propObjs.length - 1; j >= 0; j--) { val = propObjs[j][name]; if (typeof val === 'object') { complexObjs.unshift(val); } else if (val !== undefined) { dest[name] = val; // if there were no objects, this value will be used break; } } // if the trailing values were objects, use the merged value if (complexObjs.length) { dest[name] = mergeProps(complexObjs); } } } // copy values into the destination, going from last to first for (i = propObjs.length - 1; i >= 0; i--) { props = propObjs[i]; for (name in props) { if (!(name in dest)) { // if already assigned by previous props or complex props, don't reassign dest[name] = props[name]; } } } return dest; } // 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]; } } } // Copies over certain methods with the same names as Object.prototype methods. Overcomes an IE<=8 bug: // https://developer.mozilla.org/en-US/docs/ECMAScript_DontEnum_attribute#JScript_DontEnum_Bug function copyNativeMethods(src, dest) { var names = [ 'constructor', 'toString', 'valueOf' ]; var i, name; for (i = 0; i < names.length; i++) { name = names[i]; if (src[name] !== Object.prototype[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, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/'/g, '&#039;') .replace(/"/g, '&quot;') .replace(/\n/g, '<br />'); } function stripHtmlEntities(text) { return text.replace(/&.*?;/g, ''); } // Given a hash of CSS properties, returns a string of CSS. // Uses property names as-is (no camel-case conversion). Will not make statements for null/undefined values. function cssToStr(cssProps) { var statements = []; $.each(cssProps, function(name, val) { if (val != null) { statements.push(name + ':' + val); } }); return statements.join(';'); } 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 method bound to the given object context. // Just like one of the jQuery.proxy signatures, but without the undesired behavior of treating the same method with // different contexts as identical when binding/unbinding events. function proxy(obj, methodName) { var method = obj[methodName]; return function() { return method.apply(obj, arguments); }; } // 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. If `immediate` is passed, trigger the function on the // leading edge, instead of the trailing. // https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714 function debounce(func, wait, immediate) { var timeout, args, context, timestamp, result; var later = function() { var last = +new Date() - timestamp; if (last < wait) { timeout = setTimeout(later, wait - last); } else { timeout = null; if (!immediate) { result = func.apply(context, args); context = args = null; } } }; return function() { context = this; args = arguments; timestamp = +new Date(); var callNow = immediate && !timeout; if (!timeout) { timeout = setTimeout(later, wait); } if (callNow) { result = func.apply(context, args); context = args = null; } return result; }; } ;; 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) { if (mom.utcOffset) { mom.utcOffset(input); // if not a valid zone, will assign UTC } else { mom.zone(input); // for moment-pre-2.9 } } } } 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; }; // Week Number // ------------------------------------------------------------------------------------------------- // Returns the week number, considering the locale's custom week number calcuation // `weeks` is an alias for `week` newMomentProto.week = newMomentProto.weeks = function(input) { var weekCalc = (this._locale || this._lang) // works pre-moment-2.8 ._fullCalendar_weekCalc; if (input == null && typeof weekCalc === 'function') { // custom function only works for getter return weekCalc(this); } else if (weekCalc === 'ISO') { return oldMomentProto.isoWeek.apply(this, arguments); // ISO getter/setter } return oldMomentProto.week.apply(this, arguments); // local getter/setter }; // 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 // TODO: use keepLocalTime in the future 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 might call .utcOffset(), // 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. // TODO: look into Moment's keepLocalTime functionality 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 (might clear the ambig flags, depending on Moment internals) setUTCValues(this, a); // will set the year/month/date/hours/minutes/seconds/ms // the above call to .utc()/.utcOffset() unfortunately might clear the ambig flags, so restore this._ambigTime = wasAmbigTime || false; // Mark the zone as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(), // which clears the 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 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); // ensure non-ambiguous // this probably already happened via local() -> utcOffset(), but don't rely on Moment's internals this._ambigTime = false; this._ambigZone = false; if (wasAmbigZone) { // If the moment was ambiguously zoned, the date fields were stored as UTC. // We want to preserve these, but in local time. // TODO: look into Moment's keepLocalTime functionality setLocalValues(this, a); } return this; // for chaining }; // implicitly marks a zone newMomentProto.utc = function() { oldMomentProto.utc.apply(this, arguments); // ensure non-ambiguous // this probably already happened via utc() -> utcOffset(), but don't rely on Moment's internals this._ambigTime = false; this._ambigZone = false; return this; }; // methods for arbitrarily manipulating timezone offset. // should clear time/zone ambiguity when called. $.each([ 'zone', // only in moment-pre-2.9. deprecated afterwards 'utcOffset' ], function(i, name) { if (oldMomentProto[name]) { // original method exists? // this method implicitly marks a zone (will probably get called upon .utc() and .local()) newMomentProto[name] = 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[name].apply(this, arguments); }; } }); // 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 // TODO: look into moment.momentProperties for this. 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 unzonedDate1 = date1.clone().stripZone(); // for formatSimilarChunk var unzonedDate2 = date2.clone().stripZone(); // " 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, unzonedDate1, unzonedDate2, 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, unzonedDate1, unzonedDate2, 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, unzonedDate1, unzonedDate2, 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? // use the unzoned dates for this calculation because unreliable when near DST (bug #2396) if (unit && unzonedDate1.isSame(unzonedDate2, 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 // -----------------------------------------------------------------------