diff --git a/src/calendar/lib/fullcalendar.js b/src/calendar/lib/fullcalendar.js index 78fe3aa..b2addb2 100644 --- a/src/calendar/lib/fullcalendar.js +++ b/src/calendar/lib/fullcalendar.js @@ -1,5 +1,5 @@ /*! - * FullCalendar v2.7.0-beta + * FullCalendar v2.7.1 * Docs & License: http://fullcalendar.io/ * (c) 2015 Adam Shaw */ @@ -19,12533 +19,12551 @@ } })(function($, moment) { -;; - -var FC = $.fullCalendar = { - version: "2.7.0-beta", - internalApiVersion: 3 -}; -var fcViews = FC.views = {}; - - -FC.isTouchEnabled = ('ontouchstart' in document) && - !window.__karma__; // workaround for phantomjs falsely reporting touch - - -$.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 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; - - // fuckin 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 = $('
') - .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 && touches.length == 1) { - return touches[0].pageX; - } -} - - -function getEvY(ev) { - if (ev.pageY !== undefined) { - return ev.pageY; - } - var touches = ev.originalEvent.touches; - if (touches && touches.length == 1) { - 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/g, '>') - .replace(/'/g, ''') - .replace(/"/g, '"') - .replace(/\n/g, '
'); -} - - -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 "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; leftIleftI; 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 -// ------------------------------------------------------------------------------------------------- - - -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 = /\[([^\]]*)\]|\(([^\)]*)\)|(LTS|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 on a class to create a subclass. -// Last argument contains instance methods. Any argument before the last are considered mixins. -Class.extend = function() { - var len = arguments.length; - var i; - var members; - - for (i = 0; i < len; i++) { - members = arguments[i]; - if (i < len - 1) { // not the last argument? - mixIntoClass(this, members); - } - } - - return extendClass(this, members || {}); // members will be undefined if no arguments -}; - - -// 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) { - mixIntoClass(this, members); -}; - - -function extendClass(superClass, members) { - var subClass; - - // 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); + 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 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 = $('
') + .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]; - // copy each member variable/method onto the the subclass's prototype - copyOwnProps(members, subClass.prototype); - copyNativeMethods(members, subClass.prototype); // hack for IE8 + 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; + } - // copy over all class variables/methods to the subclass, such as `extend` and `mixin` - copyOwnProps(superClass, subClass); - return subClass; -} + // 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 mixIntoClass(theClass, members) { - copyOwnProps(members, theClass.prototype); // TODO: copyNativeMethods? -} -;; + function copyOwnProps(src, dest) { + for (var name in src) { + if (hasOwnProp(src, name)) { + dest[name] = src[name]; + } + } + } -var EmitterMixin = FC.EmitterMixin = { - callbackHash: null, + // 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]; + } + } + } - on: function(name, callback) { - this.loopCallbacks(name, 'add', [ callback ]); + 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/g, '>') + .replace(/'/g, ''') + .replace(/"/g, '"') + .replace(/\n/g, '
'); + } + + + 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; - return this; // for chaining - }, + 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 "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; leftIleftI; 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 + // ------------------------------------------------------------------------------------------------- + + + 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 = /\[([^\]]*)\]|\(([^\)]*)\)|(LTS|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]); + } + } - off: function(name, callback) { - this.loopCallbacks(name, 'remove', [ callback ]); + return chunks; + } - return this; // for chaining - }, + ;; + FC.Class = Class; // export - trigger: function(name) { // args... - var args = Array.prototype.slice.call(arguments, 1); + // Class that all other classes will inherit from + function Class() { } - this.triggerWith(name, this, args); - return this; // for chaining - }, + // Called on a class to create a subclass. + // Last argument contains instance methods. Any argument before the last are considered mixins. + Class.extend = function() { + var len = arguments.length; + var i; + var members; + for (i = 0; i < len; i++) { + members = arguments[i]; + if (i < len - 1) { // not the last argument? + mixIntoClass(this, members); + } + } - triggerWith: function(name, context, args) { - this.loopCallbacks(name, 'fireWith', [ context, args ]); + return extendClass(this, members || {}); // members will be undefined if no arguments + }; - return this; // for chaining - }, + // 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) { + mixIntoClass(this, members); + }; - /* - Given an event name string with possible namespaces, - call the given methodName on all the internal Callback object with the given arguments. - */ - loopCallbacks: function(name, methodName, args) { - var parts = name.split('.'); // "click.namespace" -> [ "click", "namespace" ] - var i, part; - var callbackObj; - for (i = 0; i < parts.length; i++) { - part = parts[i]; - if (part) { // in case no event name like "click" - callbackObj = this.ensureCallbackObj((i ? '.' : '') + part); // put periods in front of namespaces - callbackObj[methodName].apply(callbackObj, args); - } - } - }, + function extendClass(superClass, members) { + var subClass; + // 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); + }; + } - ensureCallbackObj: function(name) { - if (!this.callbackHash) { - this.callbackHash = {}; - } - if (!this.callbackHash[name]) { - this.callbackHash[name] = $.Callbacks(); - } - return this.callbackHash[name]; - } - -}; -;; - -/* -Utility methods for easily listening to events on another object, -and more importantly, easily unlistening from them. -*/ -var ListenerMixin = FC.ListenerMixin = (function() { - var guid = 0; - var ListenerMixin = { - - listenerId: null, - - /* - Given an `other` object that has on/off methods, bind the given `callback` to an event by the given name. - The `callback` will be called with the `this` context of the object that .listenTo is being called on. - Can be called: - .listenTo(other, eventName, callback) - OR - .listenTo(other, { - eventName1: callback1, - eventName2: callback2 - }) - */ - listenTo: function(other, arg, callback) { - if (typeof arg === 'object') { // given dictionary of callbacks - for (var eventName in arg) { - if (arg.hasOwnProperty(eventName)) { - this.listenTo(other, eventName, arg[eventName]); - } - } - } - else if (typeof arg === 'string') { - other.on( - arg + '.' + this.getListenerNamespace(), // use event namespacing to identify this object - $.proxy(callback, this) // always use `this` context - // the usually-undesired jQuery guid behavior doesn't matter, - // because we always unbind via namespace - ); - } - }, - - /* - Causes the current object to stop listening to events on the `other` object. - `eventName` is optional. If omitted, will stop listening to ALL events on `other`. - */ - stopListeningTo: function(other, eventName) { - other.off((eventName || '') + '.' + this.getListenerNamespace()); - }, - - /* - Returns a string, unique to this object, to be used for event namespacing - */ - getListenerNamespace: function() { - if (this.listenerId == null) { - this.listenerId = guid++; - } - return '_listener' + this.listenerId; - } - - }; - return ListenerMixin; -})(); -;; - -/* 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(ListenerMixin, { - - isHidden: true, - options: null, - el: null, // the container element for the popover. generated by this object - 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 = $('
') - .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) { - this.listenTo($(document), 'mousedown', 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 - removeElement: function() { - this.hide(); - - if (this.el) { - this.el.remove(); - this.el = null; - } - - this.stopListeningTo($(document), 'mousedown'); - }, - - - // 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 cache for the left/right/top/bottom/width/height values for one or more elements. -Works with both offset (from topleft document) and position (from offsetParent). - -options: -- els -- isHorizontal -- isVertical -*/ -var CoordCache = FC.CoordCache = Class.extend({ - - els: null, // jQuery set (assumed to be siblings) - forcedOffsetParentEl: null, // options can override the natural offsetParent - origin: null, // {left,top} position of offsetParent of els - boundingRect: null, // constrain cordinates to this rectangle. {left,right,top,bottom} or null - isHorizontal: false, // whether to query for left/right/width - isVertical: false, // whether to query for top/bottom/height - - // arrays of coordinates (offsets from topleft of document) - lefts: null, - rights: null, - tops: null, - bottoms: null, - - - constructor: function(options) { - this.els = $(options.els); - this.isHorizontal = options.isHorizontal; - this.isVertical = options.isVertical; - this.forcedOffsetParentEl = options.offsetParent ? $(options.offsetParent) : null; - }, - - - // Queries the els for coordinates and stores them. - // Call this method before using and of the get* methods below. - build: function() { - var offsetParentEl = this.forcedOffsetParentEl || this.els.eq(0).offsetParent(); - - this.origin = offsetParentEl.offset(); - this.boundingRect = this.queryBoundingRect(); - - if (this.isHorizontal) { - this.buildElHorizontals(); - } - if (this.isVertical) { - this.buildElVerticals(); - } - }, - - - // Destroys all internal data about coordinates, freeing memory - clear: function() { - this.origin = null; - this.boundingRect = null; - this.lefts = null; - this.rights = null; - this.tops = null; - this.bottoms = null; - }, - - - // When called, if coord caches aren't built, builds them - ensureBuilt: function() { - if (!this.origin) { - this.build(); - } - }, - - - // Compute and return what the elements' bounding rectangle is, from the user's perspective. - // Right now, only returns a rectangle if constrained by an overflow:scroll element. - queryBoundingRect: function() { - var scrollParentEl = getScrollParent(this.els.eq(0)); - - if (!scrollParentEl.is(document)) { - return getClientRect(scrollParentEl); - } - }, - - - // Populates the left/right internal coordinate arrays - buildElHorizontals: function() { - var lefts = []; - var rights = []; - - this.els.each(function(i, node) { - var el = $(node); - var left = el.offset().left; - var width = el.outerWidth(); - - lefts.push(left); - rights.push(left + width); - }); - - this.lefts = lefts; - this.rights = rights; - }, - - - // Populates the top/bottom internal coordinate arrays - buildElVerticals: function() { - var tops = []; - var bottoms = []; - - this.els.each(function(i, node) { - var el = $(node); - var top = el.offset().top; - var height = el.outerHeight(); - - tops.push(top); - bottoms.push(top + height); - }); - - this.tops = tops; - this.bottoms = bottoms; - }, - - - // Given a left offset (from document left), returns the index of the el that it horizontally intersects. - // If no intersection is made, or outside of the boundingRect, returns undefined. - getHorizontalIndex: function(leftOffset) { - this.ensureBuilt(); - - var boundingRect = this.boundingRect; - var lefts = this.lefts; - var rights = this.rights; - var len = lefts.length; - var i; - - if (!boundingRect || (leftOffset >= boundingRect.left && leftOffset < boundingRect.right)) { - for (i = 0; i < len; i++) { - if (leftOffset >= lefts[i] && leftOffset < rights[i]) { - return i; - } - } - } - }, - - - // Given a top offset (from document top), returns the index of the el that it vertically intersects. - // If no intersection is made, or outside of the boundingRect, returns undefined. - getVerticalIndex: function(topOffset) { - this.ensureBuilt(); - - var boundingRect = this.boundingRect; - var tops = this.tops; - var bottoms = this.bottoms; - var len = tops.length; - var i; - - if (!boundingRect || (topOffset >= boundingRect.top && topOffset < boundingRect.bottom)) { - for (i = 0; i < len; i++) { - if (topOffset >= tops[i] && topOffset < bottoms[i]) { - return i; - } - } - } - }, - - - // Gets the left offset (from document left) of the element at the given index - getLeftOffset: function(leftIndex) { - this.ensureBuilt(); - return this.lefts[leftIndex]; - }, - - - // Gets the left position (from offsetParent left) of the element at the given index - getLeftPosition: function(leftIndex) { - this.ensureBuilt(); - return this.lefts[leftIndex] - this.origin.left; - }, - - - // Gets the right offset (from document left) of the element at the given index. - // This value is NOT relative to the document's right edge, like the CSS concept of "right" would be. - getRightOffset: function(leftIndex) { - this.ensureBuilt(); - return this.rights[leftIndex]; - }, - - - // Gets the right position (from offsetParent left) of the element at the given index. - // This value is NOT relative to the offsetParent's right edge, like the CSS concept of "right" would be. - getRightPosition: function(leftIndex) { - this.ensureBuilt(); - return this.rights[leftIndex] - this.origin.left; - }, - - - // Gets the width of the element at the given index - getWidth: function(leftIndex) { - this.ensureBuilt(); - return this.rights[leftIndex] - this.lefts[leftIndex]; - }, - - - // Gets the top offset (from document top) of the element at the given index - getTopOffset: function(topIndex) { - this.ensureBuilt(); - return this.tops[topIndex]; - }, - - - // Gets the top position (from offsetParent top) of the element at the given position - getTopPosition: function(topIndex) { - this.ensureBuilt(); - return this.tops[topIndex] - this.origin.top; - }, - - // Gets the bottom offset (from the document top) of the element at the given index. - // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be. - getBottomOffset: function(topIndex) { - this.ensureBuilt(); - return this.bottoms[topIndex]; - }, - - - // Gets the bottom position (from the offsetParent top) of the element at the given index. - // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be. - getBottomPosition: function(topIndex) { - this.ensureBuilt(); - return this.bottoms[topIndex] - this.origin.top; - }, - - - // Gets the height of the element at the given index - getHeight: function(topIndex) { - this.ensureBuilt(); - return this.bottoms[topIndex] - this.tops[topIndex]; - } - -}); - -;; - -/* Tracks a drag's mouse movement, firing various handlers -----------------------------------------------------------------------------------------------------------------------*/ -// TODO: use Emitter - -var DragListener = FC.DragListener = Class.extend(ListenerMixin, { - - options: null, - - // for IE8 bug-fighting behavior - subjectEl: null, - subjectHref: null, - - // coordinates of the initial mousedown - originX: null, - originY: null, - - scrollEl: null, - - isInteracting: false, - isDistanceSurpassed: false, - isDelayEnded: false, - isDragging: false, - isTouch: false, - - delay: null, - delayTimeoutId: null, - minDistance: null, - - - constructor: function(options) { - this.options = options || {}; - }, - - - // Interaction (high-level) - // ----------------------------------------------------------------------------------------------------------------- - - - startInteraction: function(ev, extraOptions) { - var isTouch = getEvIsTouch(ev); - - if (ev.type === 'mousedown') { - if (!isPrimaryMouseButton(ev)) { - return; - } - else { - ev.preventDefault(); // prevents native selection in most browsers - } - } - - if (!this.isInteracting) { - - // process options - extraOptions = extraOptions || {}; - this.delay = firstDefined(extraOptions.delay, this.options.delay, 0); - this.minDistance = firstDefined(extraOptions.distance, this.options.distance, 0); - this.subjectEl = this.options.subjectEl; - - this.isInteracting = true; - this.isTouch = isTouch; - this.isDelayEnded = false; - this.isDistanceSurpassed = false; - - this.originX = getEvX(ev); - this.originY = getEvY(ev); - this.scrollEl = getScrollParent($(ev.target)); - - this.bindHandlers(); - this.initAutoScroll(); - this.handleInteractionStart(ev); - this.startDelay(ev); - - if (!this.minDistance) { - this.handleDistanceSurpassed(ev); - } - } - }, - - - handleInteractionStart: function(ev) { - this.trigger('interactionStart', ev); - }, - - - endInteraction: function(ev) { - if (this.isInteracting) { - this.endDrag(ev); - - if (this.delayTimeoutId) { - clearTimeout(this.delayTimeoutId); - this.delayTimeoutId = null; - } - - this.destroyAutoScroll(); - this.unbindHandlers(); - - this.isInteracting = false; - this.handleInteractionEnd(ev); - } - }, - - - handleInteractionEnd: function(ev) { - this.trigger('interactionEnd', ev); - }, - - - // Binding To DOM - // ----------------------------------------------------------------------------------------------------------------- - - - bindHandlers: function() { - var _this = this; - var touchStartIgnores = 1; - - if (this.isTouch) { - this.listenTo($(document), { - touchmove: this.handleTouchMove, - touchend: this.endInteraction, - touchcancel: this.endInteraction, - - // Sometimes touchend doesn't fire - // (can't figure out why. touchcancel doesn't fire either. has to do with scrolling?) - // If another touchstart happens, we know it's bogus, so cancel the drag. - // touchend will continue to be broken until user does a shorttap/scroll, but this is best we can do. - touchstart: function(ev) { - if (touchStartIgnores) { // bindHandlers is called from within a touchstart, - touchStartIgnores--; // and we don't want this to fire immediately, so ignore. - } - else { - _this.endInteraction(ev); - } - } - }); - } - else { - this.listenTo($(document), { - mousemove: this.handleMouseMove, - mouseup: this.endInteraction - }); - } - - this.listenTo($(document), { - selectstart: preventDefault, // don't allow selection while dragging - contextmenu: preventDefault // long taps would open menu on Chrome dev tools - }); - - if (this.scrollEl) { - this.listenTo(this.scrollEl, 'scroll', this.handleScroll); - } - }, - - - unbindHandlers: function() { - this.stopListeningTo($(document)); - - if (this.scrollEl) { - this.stopListeningTo(this.scrollEl); - } - }, - - - // Drag (high-level) - // ----------------------------------------------------------------------------------------------------------------- - - - // extraOptions ignored if drag already started - startDrag: function(ev, extraOptions) { - this.startInteraction(ev, extraOptions); // ensure interaction began - - if (!this.isDragging) { - this.isDragging = true; - this.handleDragStart(ev); - } - }, - - - handleDragStart: function(ev) { - this.trigger('dragStart', ev); - this.initHrefHack(); - }, - + // build the base prototype for the subclass, which is an new object chained to the superclass's prototype + subClass.prototype = createObject(superClass.prototype); - handleMove: function(ev) { - var dx = getEvX(ev) - this.originX; - var dy = getEvY(ev) - this.originY; - var minDistance = this.minDistance; - var distanceSq; // current distance from the origin, squared + // copy each member variable/method onto the the subclass's prototype + copyOwnProps(members, subClass.prototype); + copyNativeMethods(members, subClass.prototype); // hack for IE8 - if (!this.isDistanceSurpassed) { - distanceSq = dx * dx + dy * dy; - if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem - this.handleDistanceSurpassed(ev); - } - } + // copy over all class variables/methods to the subclass, such as `extend` and `mixin` + copyOwnProps(superClass, subClass); - if (this.isDragging) { - this.handleDrag(dx, dy, ev); - } - }, + return subClass; + } - // Called while the mouse is being moved and when we know a legitimate drag is taking place - handleDrag: function(dx, dy, ev) { - this.trigger('drag', dx, dy, ev); - this.updateAutoScroll(ev); // will possibly cause scrolling - }, + function mixIntoClass(theClass, members) { + copyOwnProps(members, theClass.prototype); // TODO: copyNativeMethods? + } + ;; + + var EmitterMixin = FC.EmitterMixin = { + + callbackHash: null, + + + on: function(name, callback) { + this.loopCallbacks(name, 'add', [ callback ]); + + return this; // for chaining + }, + + + off: function(name, callback) { + this.loopCallbacks(name, 'remove', [ callback ]); + + return this; // for chaining + }, + + + trigger: function(name) { // args... + var args = Array.prototype.slice.call(arguments, 1); + + this.triggerWith(name, this, args); + + return this; // for chaining + }, + + + triggerWith: function(name, context, args) { + this.loopCallbacks(name, 'fireWith', [ context, args ]); + + return this; // for chaining + }, + + + /* + Given an event name string with possible namespaces, + call the given methodName on all the internal Callback object with the given arguments. + */ + loopCallbacks: function(name, methodName, args) { + var parts = name.split('.'); // "click.namespace" -> [ "click", "namespace" ] + var i, part; + var callbackObj; + + for (i = 0; i < parts.length; i++) { + part = parts[i]; + if (part) { // in case no event name like "click" + callbackObj = this.ensureCallbackObj((i ? '.' : '') + part); // put periods in front of namespaces + callbackObj[methodName].apply(callbackObj, args); + } + } + }, + + + ensureCallbackObj: function(name) { + if (!this.callbackHash) { + this.callbackHash = {}; + } + if (!this.callbackHash[name]) { + this.callbackHash[name] = $.Callbacks(); + } + return this.callbackHash[name]; + } + + }; + ;; + + /* + Utility methods for easily listening to events on another object, + and more importantly, easily unlistening from them. + */ + var ListenerMixin = FC.ListenerMixin = (function() { + var guid = 0; + var ListenerMixin = { + + listenerId: null, + + /* + Given an `other` object that has on/off methods, bind the given `callback` to an event by the given name. + The `callback` will be called with the `this` context of the object that .listenTo is being called on. + Can be called: + .listenTo(other, eventName, callback) + OR + .listenTo(other, { + eventName1: callback1, + eventName2: callback2 + }) + */ + listenTo: function(other, arg, callback) { + if (typeof arg === 'object') { // given dictionary of callbacks + for (var eventName in arg) { + if (arg.hasOwnProperty(eventName)) { + this.listenTo(other, eventName, arg[eventName]); + } + } + } + else if (typeof arg === 'string') { + other.on( + arg + '.' + this.getListenerNamespace(), // use event namespacing to identify this object + $.proxy(callback, this) // always use `this` context + // the usually-undesired jQuery guid behavior doesn't matter, + // because we always unbind via namespace + ); + } + }, + + /* + Causes the current object to stop listening to events on the `other` object. + `eventName` is optional. If omitted, will stop listening to ALL events on `other`. + */ + stopListeningTo: function(other, eventName) { + other.off((eventName || '') + '.' + this.getListenerNamespace()); + }, + + /* + Returns a string, unique to this object, to be used for event namespacing + */ + getListenerNamespace: function() { + if (this.listenerId == null) { + this.listenerId = guid++; + } + return '_listener' + this.listenerId; + } + + }; + return ListenerMixin; + })(); + ;; + + /* 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(ListenerMixin, { + + isHidden: true, + options: null, + el: null, // the container element for the popover. generated by this object + 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 = $('
') + .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) { + this.listenTo($(document), 'mousedown', 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 + removeElement: function() { + this.hide(); + + if (this.el) { + this.el.remove(); + this.el = null; + } + + this.stopListeningTo($(document), 'mousedown'); + }, + + + // 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 cache for the left/right/top/bottom/width/height values for one or more elements. + Works with both offset (from topleft document) and position (from offsetParent). + + options: + - els + - isHorizontal + - isVertical + */ + var CoordCache = FC.CoordCache = Class.extend({ + + els: null, // jQuery set (assumed to be siblings) + forcedOffsetParentEl: null, // options can override the natural offsetParent + origin: null, // {left,top} position of offsetParent of els + boundingRect: null, // constrain cordinates to this rectangle. {left,right,top,bottom} or null + isHorizontal: false, // whether to query for left/right/width + isVertical: false, // whether to query for top/bottom/height + + // arrays of coordinates (offsets from topleft of document) + lefts: null, + rights: null, + tops: null, + bottoms: null, + + + constructor: function(options) { + this.els = $(options.els); + this.isHorizontal = options.isHorizontal; + this.isVertical = options.isVertical; + this.forcedOffsetParentEl = options.offsetParent ? $(options.offsetParent) : null; + }, + + + // Queries the els for coordinates and stores them. + // Call this method before using and of the get* methods below. + build: function() { + var offsetParentEl = this.forcedOffsetParentEl || this.els.eq(0).offsetParent(); + + this.origin = offsetParentEl.offset(); + this.boundingRect = this.queryBoundingRect(); + + if (this.isHorizontal) { + this.buildElHorizontals(); + } + if (this.isVertical) { + this.buildElVerticals(); + } + }, + + + // Destroys all internal data about coordinates, freeing memory + clear: function() { + this.origin = null; + this.boundingRect = null; + this.lefts = null; + this.rights = null; + this.tops = null; + this.bottoms = null; + }, + + + // When called, if coord caches aren't built, builds them + ensureBuilt: function() { + if (!this.origin) { + this.build(); + } + }, + + + // Compute and return what the elements' bounding rectangle is, from the user's perspective. + // Right now, only returns a rectangle if constrained by an overflow:scroll element. + queryBoundingRect: function() { + var scrollParentEl = getScrollParent(this.els.eq(0)); + + if (!scrollParentEl.is(document)) { + return getClientRect(scrollParentEl); + } + }, + + + // Populates the left/right internal coordinate arrays + buildElHorizontals: function() { + var lefts = []; + var rights = []; + + this.els.each(function(i, node) { + var el = $(node); + var left = el.offset().left; + var width = el.outerWidth(); + + lefts.push(left); + rights.push(left + width); + }); + + this.lefts = lefts; + this.rights = rights; + }, + + + // Populates the top/bottom internal coordinate arrays + buildElVerticals: function() { + var tops = []; + var bottoms = []; + + this.els.each(function(i, node) { + var el = $(node); + var top = el.offset().top; + var height = el.outerHeight(); + + tops.push(top); + bottoms.push(top + height); + }); + + this.tops = tops; + this.bottoms = bottoms; + }, + + + // Given a left offset (from document left), returns the index of the el that it horizontally intersects. + // If no intersection is made, or outside of the boundingRect, returns undefined. + getHorizontalIndex: function(leftOffset) { + this.ensureBuilt(); + + var boundingRect = this.boundingRect; + var lefts = this.lefts; + var rights = this.rights; + var len = lefts.length; + var i; + + if (!boundingRect || (leftOffset >= boundingRect.left && leftOffset < boundingRect.right)) { + for (i = 0; i < len; i++) { + if (leftOffset >= lefts[i] && leftOffset < rights[i]) { + return i; + } + } + } + }, + + + // Given a top offset (from document top), returns the index of the el that it vertically intersects. + // If no intersection is made, or outside of the boundingRect, returns undefined. + getVerticalIndex: function(topOffset) { + this.ensureBuilt(); + var boundingRect = this.boundingRect; + var tops = this.tops; + var bottoms = this.bottoms; + var len = tops.length; + var i; - endDrag: function(ev) { - if (this.isDragging) { - this.isDragging = false; - this.handleDragEnd(ev); - } - }, + if (!boundingRect || (topOffset >= boundingRect.top && topOffset < boundingRect.bottom)) { + for (i = 0; i < len; i++) { + if (topOffset >= tops[i] && topOffset < bottoms[i]) { + return i; + } + } + } + }, - handleDragEnd: function(ev) { - this.trigger('dragEnd', ev); - this.destroyHrefHack(); - }, + // Gets the left offset (from document left) of the element at the given index + getLeftOffset: function(leftIndex) { + this.ensureBuilt(); + return this.lefts[leftIndex]; + }, - // Delay - // ----------------------------------------------------------------------------------------------------------------- + // Gets the left position (from offsetParent left) of the element at the given index + getLeftPosition: function(leftIndex) { + this.ensureBuilt(); + return this.lefts[leftIndex] - this.origin.left; + }, - startDelay: function(initialEv) { - var _this = this; + // Gets the right offset (from document left) of the element at the given index. + // This value is NOT relative to the document's right edge, like the CSS concept of "right" would be. + getRightOffset: function(leftIndex) { + this.ensureBuilt(); + return this.rights[leftIndex]; + }, - if (this.delay) { - this.delayTimeoutId = setTimeout(function() { - _this.handleDelayEnd(initialEv); - }, this.delay); - } - else { - this.handleDelayEnd(initialEv); - } - }, + // Gets the right position (from offsetParent left) of the element at the given index. + // This value is NOT relative to the offsetParent's right edge, like the CSS concept of "right" would be. + getRightPosition: function(leftIndex) { + this.ensureBuilt(); + return this.rights[leftIndex] - this.origin.left; + }, - handleDelayEnd: function(initialEv) { - this.isDelayEnded = true; - if (this.isDistanceSurpassed) { - this.startDrag(initialEv); - } - }, + // Gets the width of the element at the given index + getWidth: function(leftIndex) { + this.ensureBuilt(); + return this.rights[leftIndex] - this.lefts[leftIndex]; + }, - // Distance - // ----------------------------------------------------------------------------------------------------------------- + // Gets the top offset (from document top) of the element at the given index + getTopOffset: function(topIndex) { + this.ensureBuilt(); + return this.tops[topIndex]; + }, - handleDistanceSurpassed: function(ev) { - this.isDistanceSurpassed = true; + // Gets the top position (from offsetParent top) of the element at the given position + getTopPosition: function(topIndex) { + this.ensureBuilt(); + return this.tops[topIndex] - this.origin.top; + }, - if (this.isDelayEnded) { - this.startDrag(ev); - } - }, + // Gets the bottom offset (from the document top) of the element at the given index. + // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be. + getBottomOffset: function(topIndex) { + this.ensureBuilt(); + return this.bottoms[topIndex]; + }, - // Mouse / Touch - // ----------------------------------------------------------------------------------------------------------------- + // Gets the bottom position (from the offsetParent top) of the element at the given index. + // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be. + getBottomPosition: function(topIndex) { + this.ensureBuilt(); + return this.bottoms[topIndex] - this.origin.top; + }, - handleTouchMove: function(ev) { - // prevent inertia and touchmove-scrolling while dragging - if (this.isDragging) { - ev.preventDefault(); - } + // Gets the height of the element at the given index + getHeight: function(topIndex) { + this.ensureBuilt(); + return this.bottoms[topIndex] - this.tops[topIndex]; + } - this.handleMove(ev); - }, + }); + ;; - handleMouseMove: function(ev) { - this.handleMove(ev); - }, + /* Tracks a drag's mouse movement, firing various handlers + ----------------------------------------------------------------------------------------------------------------------*/ + // TODO: use Emitter + var DragListener = FC.DragListener = Class.extend(ListenerMixin, { - // Scrolling (unrelated to auto-scroll) - // ----------------------------------------------------------------------------------------------------------------- + options: null, + // for IE8 bug-fighting behavior + subjectEl: null, + subjectHref: null, - handleScroll: function(ev) { - // if the drag is being initiated by touch, but a scroll happens before - // the drag-initiating delay is over, cancel the drag - if (this.isTouch && !this.isDragging) { - this.endInteraction(ev); - } - }, + // coordinates of the initial mousedown + originX: null, + originY: null, + scrollEl: null, - // HREF Hack - // ----------------------------------------------------------------------------------------------------------------- + isInteracting: false, + isDistanceSurpassed: false, + isDelayEnded: false, + isDragging: false, + isTouch: false, + delay: null, + delayTimeoutId: null, + minDistance: null, - initHrefHack: function() { - var subjectEl = this.subjectEl; - // remove a mousedown'd 's href so it is not visited (IE8 bug) - if ((this.subjectHref = subjectEl ? subjectEl.attr('href') : null)) { - subjectEl.removeAttr('href'); - } - }, + constructor: function(options) { + this.options = options || {}; + }, - destroyHrefHack: function() { - var subjectEl = this.subjectEl; - var subjectHref = this.subjectHref; + // Interaction (high-level) + // ----------------------------------------------------------------------------------------------------------------- - // restore a mousedown'd 's href (for IE8 bug) - setTimeout(function() { // must be outside of the click's execution - if (subjectHref) { - subjectEl.attr('href', subjectHref); - } - }, 0); - }, + startInteraction: function(ev, extraOptions) { + var isTouch = getEvIsTouch(ev); - // Utils - // ----------------------------------------------------------------------------------------------------------------- + if (ev.type === 'mousedown') { + if (!isPrimaryMouseButton(ev)) { + return; + } + else { + ev.preventDefault(); // prevents native selection in most browsers + } + } + if (!this.isInteracting) { - // 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)); - } - // makes _methods callable by event name. TODO: kill this - if (this['_' + name]) { - this['_' + name].apply(this, Array.prototype.slice.call(arguments, 1)); - } - } - - -}); - -;; -/* -this.scrollEl is set in DragListener -*/ -DragListener.mixin({ - - isAutoScroll: false, - - 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 - - // defaults - scrollSensitivity: 30, // pixels from edge for scrolling to start - scrollSpeed: 200, // pixels per second, at maximum speed - scrollIntervalMs: 50, // millisecond wait between scroll increment - - - initAutoScroll: function() { - var scrollEl = this.scrollEl; - - this.isAutoScroll = - this.options.scroll && - scrollEl && - !scrollEl.is(window) && - !scrollEl.is(document); - - if (this.isAutoScroll) { - // debounce makes sure rapid calls don't happen - this.listenTo(scrollEl, 'scroll', debounce(this.handleDebouncedScroll, 100)); - } - }, - - - destroyAutoScroll: function() { - this.endAutoScroll(); // kill any animation loop - - // remove the scroll handler if there is a scrollEl - if (this.isAutoScroll) { - this.stopListeningTo(this.scrollEl, 'scroll'); // will probably get removed by unbindHandlers too :( - } - }, - - - // Computes and stores the bounding rectangle of scrollEl - computeScrollBounds: function() { - if (this.isAutoScroll) { - this.scrollBounds = getOuterRect(this.scrollEl); - // TODO: use getClientRect in future. but prevents auto scrolling when on top of scrollbars - } - }, - - - // Called when the dragging is in progress and scrolling should be updated - updateAutoScroll: 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 - (getEvY(ev) - bounds.top)) / sensitivity; - bottomCloseness = (sensitivity - (bounds.bottom - getEvY(ev))) / sensitivity; - leftCloseness = (sensitivity - (getEvX(ev) - bounds.left)) / sensitivity; - rightCloseness = (sensitivity - (bounds.right - getEvX(ev))) / 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.endAutoScroll(); - } - }, - - - // Kills any existing scrolling animation loop - endAutoScroll: function() { - if (this.scrollIntervalId) { - clearInterval(this.scrollIntervalId); - this.scrollIntervalId = null; - - this.handleScrollEnd(); - } - }, - - - // Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce) - handleDebouncedScroll: function() { - // recompute all coordinates, but *only* if this is *not* part of our scrolling animation - if (!this.scrollIntervalId) { - this.handleScrollEnd(); - } - }, - - - // Called when scrolling has stopped, whether through auto scroll, or the user scrolling - handleScrollEnd: function() { - } - -}); -;; - -/* Tracks mouse movements over a component and raises events about which hit the mouse is over. ------------------------------------------------------------------------------------------------------------------------- -options: -- subjectEl -- subjectCenter -*/ - -var HitDragListener = DragListener.extend({ - - component: null, // converts coordinates to hits - // methods: prepareHits, releaseHits, queryHit - - origHit: null, // the hit the mouse was over when listening started - hit: null, // the hit the mouse is over - coordAdjust: null, // delta that will be added to the mouse coordinates when computing collisions - - - constructor: function(component, options) { - DragListener.call(this, options); // call the super-constructor - - this.component = component; - }, - - - // Called when drag listening starts (but a real drag has not necessarily began). - // ev might be undefined if dragging was started manually. - handleInteractionStart: function(ev) { - var subjectEl = this.subjectEl; - var subjectRect; - var origPoint; - var point; - - DragListener.prototype.handleInteractionStart.apply(this, arguments); // call the super-method - - this.computeCoords(); - - if (ev) { - origPoint = { left: getEvX(ev), top: getEvY(ev) }; - point = origPoint; - - // constrain the point to bounds of the element being dragged - if (subjectEl) { - subjectRect = getOuterRect(subjectEl); // used for centering as well - point = constrainPoint(point, subjectRect); - } - - this.origHit = this.queryHit(point.left, point.top); - - // treat the center of the subject as the collision point? - if (subjectEl && this.options.subjectCenter) { - - // only consider the area the subject overlaps the hit. best for large subjects. - // TODO: skip this if hit didn't supply left/right/top/bottom - if (this.origHit) { - subjectRect = intersectRects(this.origHit, subjectRect) || - subjectRect; // in case there is no intersection - } - - point = getRectCenter(subjectRect); - } - - this.coordAdjust = diffPoints(point, origPoint); // point - origPoint - } - else { - this.origHit = null; - this.coordAdjust = null; - } - }, - - - // Recomputes the drag-critical positions of elements - computeCoords: function() { - this.component.prepareHits(); - this.computeScrollBounds(); // why is this here?????? - }, - - - // Called when the actual drag has started - handleDragStart: function(ev) { - var hit; - - DragListener.prototype.handleDragStart.apply(this, arguments); // call the super-method - - // might be different from this.origHit if the min-distance is large - hit = this.queryHit(getEvX(ev), getEvY(ev)); + // process options + extraOptions = extraOptions || {}; + this.delay = firstDefined(extraOptions.delay, this.options.delay, 0); + this.minDistance = firstDefined(extraOptions.distance, this.options.distance, 0); + this.subjectEl = this.options.subjectEl; - // report the initial hit the mouse is over - // especially important if no min-distance and drag starts immediately - if (hit) { - this.handleHitOver(hit); - } - }, + this.isInteracting = true; + this.isTouch = isTouch; + this.isDelayEnded = false; + this.isDistanceSurpassed = false; + this.originX = getEvX(ev); + this.originY = getEvY(ev); + this.scrollEl = getScrollParent($(ev.target)); - // Called when the drag moves - handleDrag: function(dx, dy, ev) { - var hit; + this.bindHandlers(); + this.initAutoScroll(); + this.handleInteractionStart(ev); + this.startDelay(ev); - DragListener.prototype.handleDrag.apply(this, arguments); // call the super-method + if (!this.minDistance) { + this.handleDistanceSurpassed(ev); + } + } + }, - hit = this.queryHit(getEvX(ev), getEvY(ev)); - if (!isHitsEqual(hit, this.hit)) { // a different hit than before? - if (this.hit) { - this.handleHitOut(); - } - if (hit) { - this.handleHitOver(hit); - } - } - }, + handleInteractionStart: function(ev) { + this.trigger('interactionStart', ev); + }, - // Called when dragging has been stopped - handleDragEnd: function() { - this.handleHitDone(); - DragListener.prototype.handleDragEnd.apply(this, arguments); // call the super-method - }, + endInteraction: function(ev) { + if (this.isInteracting) { + this.endDrag(ev); + if (this.delayTimeoutId) { + clearTimeout(this.delayTimeoutId); + this.delayTimeoutId = null; + } - // Called when a the mouse has just moved over a new hit - handleHitOver: function(hit) { - var isOrig = isHitsEqual(hit, this.origHit); + this.destroyAutoScroll(); + this.unbindHandlers(); - this.hit = hit; + this.isInteracting = false; + this.handleInteractionEnd(ev); + } + }, - this.trigger('hitOver', this.hit, isOrig, this.origHit); - }, - - - // Called when the mouse has just moved out of a hit - handleHitOut: function() { - if (this.hit) { - this.trigger('hitOut', this.hit); - this.handleHitDone(); - this.hit = null; - } - }, - - - // Called after a hitOut. Also called before a dragStop - handleHitDone: function() { - if (this.hit) { - this.trigger('hitDone', this.hit); - } - }, - - - // Called when the interaction ends, whether there was a real drag or not - handleInteractionEnd: function() { - DragListener.prototype.handleInteractionEnd.apply(this, arguments); // call the super-method - - this.origHit = null; - this.hit = null; - - this.component.releaseHits(); - }, - - - // Called when scrolling has stopped, whether through auto scroll, or the user scrolling - handleScrollEnd: function() { - DragListener.prototype.handleScrollEnd.apply(this, arguments); // call the super-method - - this.computeCoords(); // hits' absolute positions will be in new places. recompute - }, - - - // Gets the hit underneath the coordinates for the given mouse event - queryHit: function(left, top) { - - if (this.coordAdjust) { - left += this.coordAdjust.left; - top += this.coordAdjust.top; - } - - return this.component.queryHit(left, top); - } - -}); - - -// Returns `true` if the hits are identically equal. `false` otherwise. Must be from the same component. -// Two null values will be considered equal, as two "out of the component" states are the same. -function isHitsEqual(hit0, hit1) { - - if (!hit0 && !hit1) { - return true; - } - - if (hit0 && hit1) { - return hit0.component === hit1.component && - isHitPropsWithin(hit0, hit1) && - isHitPropsWithin(hit1, hit0); // ensures all props are identical - } - - return false; -} - - -// Returns true if all of subHit's non-standard properties are within superHit -function isHitPropsWithin(subHit, superHit) { - for (var propName in subHit) { - if (!/^(component|left|right|top|bottom)$/.test(propName)) { - if (subHit[propName] !== superHit[propName]) { - return false; - } - } - } - return true; -} - -;; - -/* Creates a clone of an element and lets it track the mouse as it moves -----------------------------------------------------------------------------------------------------------------------*/ - -var MouseFollower = Class.extend(ListenerMixin, { - - 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 absolute coordinates of the initiating touch/mouse action - y0: null, - x0: null, - - // the number of pixels the mouse has moved from its initial position - topDelta: null, - leftDelta: null, - - 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.y0 = getEvY(ev); - this.x0 = getEvX(ev); - this.topDelta = 0; - this.leftDelta = 0; - - if (!this.isHidden) { - this.updatePosition(); - } - - if (getEvIsTouch(ev)) { - this.listenTo($(document), 'touchmove', this.handleMove); - } - else { - this.listenTo($(document), 'mousemove', this.handleMove); - } - } - }, - - - // 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.removeElement(); - - 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; - - this.stopListeningTo($(document)); - - 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() - .addClass(this.options.additionalClass || '') - .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 - }); - - // we don't want long taps or any mouse interaction causing selection/menus. - // would use preventSelection(), but that prevents selectstart, causing problems. - el.addClass('fc-unselectable'); - - el.appendTo(this.parentEl); - } - - return el; - }, - - - // Removes the tracking element if it has already been created - removeElement: 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 - handleMove: function(ev) { - this.topDelta = getEvY(ev) - this.y0; - this.leftDelta = getEvX(ev) - this.x0; - - 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(); - } - } - -}); -;; + handleInteractionEnd: function(ev) { + this.trigger('interactionEnd', ev); + }, -/* An abstract class comprised of a "grid" of areas that each represent a specific datetime -----------------------------------------------------------------------------------------------------------------------*/ -var Grid = FC.Grid = Class.extend(ListenerMixin, { + // Binding To DOM + // ----------------------------------------------------------------------------------------------------------------- - view: null, // a View object - isRTL: null, // shortcut to the view's isRTL option - start: null, - end: null, + bindHandlers: function() { + var _this = this; + var touchStartIgnores = 1; - el: null, // the containing element - elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name. + if (this.isTouch) { + this.listenTo($(document), { + touchmove: this.handleTouchMove, + touchend: this.endInteraction, + touchcancel: this.endInteraction, - // derived from options - eventTimeFormat: null, - displayEventTime: null, - displayEventEnd: null, + // Sometimes touchend doesn't fire + // (can't figure out why. touchcancel doesn't fire either. has to do with scrolling?) + // If another touchstart happens, we know it's bogus, so cancel the drag. + // touchend will continue to be broken until user does a shorttap/scroll, but this is best we can do. + touchstart: function(ev) { + if (touchStartIgnores) { // bindHandlers is called from within a touchstart, + touchStartIgnores--; // and we don't want this to fire immediately, so ignore. + } + else { + _this.endInteraction(ev); + } + } + }); - minResizeDuration: null, // TODO: hack. set by subclasses. minumum event resize duration + if (this.scrollEl) { + this.listenTo(this.scrollEl, 'scroll', this.handleTouchScroll); + } + } + else { + this.listenTo($(document), { + mousemove: this.handleMouseMove, + mouseup: this.endInteraction + }); + } - // if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity - // of the date areas. if not defined, assumes to be day and time granularity. - // TODO: port isTimeScale into same system? - largeUnit: null, + this.listenTo($(document), { + selectstart: preventDefault, // don't allow selection while dragging + contextmenu: preventDefault // long taps would open menu on Chrome dev tools + }); + }, - dayDragListener: null, - segDragListener: null, - segResizeListener: null, - externalDragListener: null, + unbindHandlers: function() { + this.stopListeningTo($(document)); - constructor: function(view) { - this.view = view; - this.isRTL = view.opt('isRTL'); - this.elsByFill = {}; - }, + if (this.scrollEl) { + this.stopListeningTo(this.scrollEl); + } + }, - /* Options - ------------------------------------------------------------------------------------------------------------------*/ + // Drag (high-level) + // ----------------------------------------------------------------------------------------------------------------- - // Generates the format string used for event time text, if not explicitly defined by 'timeFormat' - computeEventTimeFormat: function() { - return this.view.opt('smallTimeFormat'); - }, + // extraOptions ignored if drag already started + startDrag: function(ev, extraOptions) { + this.startInteraction(ev, extraOptions); // ensure interaction began + if (!this.isDragging) { + this.isDragging = true; + this.handleDragStart(ev); + } + }, - // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventTime'. - // Only applies to non-all-day events. - computeDisplayEventTime: function() { - return true; - }, + handleDragStart: function(ev) { + this.trigger('dragStart', ev); + this.initHrefHack(); + }, - // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd' - computeDisplayEventEnd: function() { - return true; - }, + handleMove: function(ev) { + var dx = getEvX(ev) - this.originX; + var dy = getEvY(ev) - this.originY; + var minDistance = this.minDistance; + var distanceSq; // current distance from the origin, squared - /* Dates - ------------------------------------------------------------------------------------------------------------------*/ + if (!this.isDistanceSurpassed) { + distanceSq = dx * dx + dy * dy; + if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem + this.handleDistanceSurpassed(ev); + } + } + if (this.isDragging) { + this.handleDrag(dx, dy, ev); + } + }, - // Tells the grid about what period of time to display. - // Any date-related internal data should be generated. - setRange: function(range) { - this.start = range.start.clone(); - this.end = range.end.clone(); - this.rangeUpdated(); - this.processRangeOptions(); - }, + // Called while the mouse is being moved and when we know a legitimate drag is taking place + handleDrag: function(dx, dy, ev) { + this.trigger('drag', dx, dy, ev); + this.updateAutoScroll(ev); // will possibly cause scrolling + }, - // Called when internal variables that rely on the range should be updated - rangeUpdated: function() { - }, + endDrag: function(ev) { + if (this.isDragging) { + this.isDragging = false; + this.handleDragEnd(ev); + } + }, - // Updates values that rely on options and also relate to range - processRangeOptions: function() { - var view = this.view; - var displayEventTime; - var displayEventEnd; + handleDragEnd: function(ev) { + this.trigger('dragEnd', ev); + this.destroyHrefHack(); + }, - this.eventTimeFormat = - view.opt('eventTimeFormat') || - view.opt('timeFormat') || // deprecated - this.computeEventTimeFormat(); - displayEventTime = view.opt('displayEventTime'); - if (displayEventTime == null) { - displayEventTime = this.computeDisplayEventTime(); // might be based off of range - } + // Delay + // ----------------------------------------------------------------------------------------------------------------- - displayEventEnd = view.opt('displayEventEnd'); - if (displayEventEnd == null) { - displayEventEnd = this.computeDisplayEventEnd(); // might be based off of range - } - this.displayEventTime = displayEventTime; - this.displayEventEnd = displayEventEnd; - }, + startDelay: function(initialEv) { + var _this = this; + if (this.delay) { + this.delayTimeoutId = setTimeout(function() { + _this.handleDelayEnd(initialEv); + }, this.delay); + } + else { + this.handleDelayEnd(initialEv); + } + }, - // Converts a span (has unzoned start/end and any other grid-specific location information) - // into an array of segments (pieces of events whose format is decided by the grid). - spanToSegs: function(span) { - // subclasses must implement - }, + handleDelayEnd: function(initialEv) { + this.isDelayEnded = true; - // Diffs the two dates, returning a duration, based on granularity of the grid - // TODO: port isTimeScale into this system? - diffDates: function(a, b) { - if (this.largeUnit) { - return diffByUnit(a, b, this.largeUnit); - } - else { - return diffDayTime(a, b); - } - }, + if (this.isDistanceSurpassed) { + this.startDrag(initialEv); + } + }, - /* Hit Area - ------------------------------------------------------------------------------------------------------------------*/ + // Distance + // ----------------------------------------------------------------------------------------------------------------- - // Called before one or more queryHit calls might happen. Should prepare any cached coordinates for queryHit - prepareHits: function() { - }, + handleDistanceSurpassed: function(ev) { + this.isDistanceSurpassed = true; + if (this.isDelayEnded) { + this.startDrag(ev); + } + }, - // Called when queryHit calls have subsided. Good place to clear any coordinate caches. - releaseHits: function() { - }, + // Mouse / Touch + // ----------------------------------------------------------------------------------------------------------------- - // Given coordinates from the topleft of the document, return data about the date-related area underneath. - // Can return an object with arbitrary properties (although top/right/left/bottom are encouraged). - // Must have a `grid` property, a reference to this current grid. TODO: avoid this - // The returned object will be processed by getHitSpan and getHitEl. - queryHit: function(leftOffset, topOffset) { - }, + handleTouchMove: function(ev) { + // prevent inertia and touchmove-scrolling while dragging + if (this.isDragging) { + ev.preventDefault(); + } - // Given position-level information about a date-related area within the grid, - // should return an object with at least a start/end date. Can provide other information as well. - getHitSpan: function(hit) { - }, + this.handleMove(ev); + }, - // Given position-level information about a date-related area within the grid, - // should return a jQuery element that best represents it. passed to dayClick callback. - getHitEl: function(hit) { - }, + handleMouseMove: function(ev) { + this.handleMove(ev); + }, - /* Rendering - ------------------------------------------------------------------------------------------------------------------*/ + // Scrolling (unrelated to auto-scroll) + // ----------------------------------------------------------------------------------------------------------------- - // Sets the container element that the grid should render inside of. - // Does other DOM-related initializations. - setElement: function(el) { - this.el = el; - preventSelection(el); + handleTouchScroll: function(ev) { + // if the drag is being initiated by touch, but a scroll happens before + // the drag-initiating delay is over, cancel the drag + if (!this.isDragging) { + this.endInteraction(ev); + } + }, - if (FC.isTouchEnabled) { - this.bindDayHandler('touchstart', this.dayTouchStart); - } - else { - this.bindDayHandler('mousedown', this.dayMousedown); - } - // attach event-element-related handlers. in Grid.events - // same garbage collection note as above. - this.bindSegHandlers(); + // HREF Hack + // ----------------------------------------------------------------------------------------------------------------- - this.bindGlobalHandlers(); - }, + initHrefHack: function() { + var subjectEl = this.subjectEl; - bindDayHandler: function(name, handler) { - var _this = this; + // remove a mousedown'd 's href so it is not visited (IE8 bug) + if ((this.subjectHref = subjectEl ? subjectEl.attr('href') : null)) { + subjectEl.removeAttr('href'); + } + }, - // attach a handler to the grid's root element. - // jQuery will take care of unregistering them when removeElement gets called. - this.el.on(name, 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) - ) { - return handler.call(_this, ev); - } - }); - }, + destroyHrefHack: function() { + var subjectEl = this.subjectEl; + var subjectHref = this.subjectHref; - // Removes the grid's container element from the DOM. Undoes any other DOM-related attachments. - // DOES NOT remove any content beforehand (doesn't clear events or call unrenderDates), unlike View - removeElement: function() { - this.unbindGlobalHandlers(); - this.clearDragListeners(); - - this.el.remove(); - - // NOTE: we don't null-out this.el for the same reasons we don't do it within View::removeElement - }, - - - // Renders the basic structure of grid view before any content is rendered - renderSkeleton: function() { - // subclasses should implement - }, - - - // Renders the grid's date-related content (like areas that represent days/times). - // Assumes setRange has already been called and the skeleton has already been rendered. - renderDates: function() { - // subclasses should implement - }, - - - // Unrenders the grid's date-related content - unrenderDates: function() { - // subclasses should implement - }, - - - /* Handlers - ------------------------------------------------------------------------------------------------------------------*/ - - - // Binds DOM handlers to elements that reside outside the grid, such as the document - bindGlobalHandlers: function() { - this.listenTo($(document), { - dragstart: this.externalDragStart, // jqui - sortstart: this.externalDragStart // jqui - }); - }, - - - // Unbinds DOM handlers from elements that reside outside the grid - unbindGlobalHandlers: function() { - this.stopListeningTo($(document)); - }, - - - // Process a mousedown on an element that represents a day. For day clicking and selecting. - dayMousedown: function(ev) { - this.clearDragListeners(); - this.buildDayDragListener().startInteraction(ev, { - //distance: 5, // needs more work if we want dayClick to fire correctly - }); - }, - - - dayTouchStart: function(ev) { - this.clearDragListeners(); - this.buildDayDragListener().startInteraction(ev, { - delay: this.view.opt('longPressDelay') - }); - }, - - - // Creates a listener that tracks the user's drag across day elements. - // For day clicking and selecting. - buildDayDragListener: function() { - var _this = this; - var view = this.view; - var isSelectable = view.opt('selectable'); - var dayClickHit; // null if invalid dayClick - var selectionSpan; // 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 = this.dayDragListener = new HitDragListener(this, { - scroll: view.opt('dragScroll'), - dragStart: function() { - view.unselect(); // since we could be rendering a new selection, we want to clear any old one - }, - hitOver: function(hit, isOrig, origHit) { - if (origHit) { // click needs to have started on a hit - dayClickHit = isOrig ? hit : null; // single-hit selection is a day click - if (isSelectable) { - selectionSpan = _this.computeSelection( - _this.getHitSpan(origHit), - _this.getHitSpan(hit) - ); - if (selectionSpan) { - _this.renderSelection(selectionSpan); - } - else if (selectionSpan === false) { - disableCursor(); - } - } - } - }, - hitOut: function() { - dayClickHit = null; - selectionSpan = null; - _this.unrenderSelection(); - enableCursor(); - }, - interactionEnd: function(ev) { - if (dayClickHit) { - view.triggerDayClick( - _this.getHitSpan(dayClickHit), - _this.getHitEl(dayClickHit), - ev - ); - } - if (selectionSpan) { - // the selection will already have been rendered. just report it - view.reportSelection(selectionSpan, ev); - } - enableCursor(); - _this.dayDragListener = null; - } - }); - - return dragListener; - }, - - - // Kills all in-progress dragging. - // Useful for when public API methods that result in re-rendering are invoked during a drag. - // Also useful for when touch devices misbehave and don't fire their touchend. - clearDragListeners: function() { - if (this.dayDragListener) { - this.dayDragListener.endInteraction(); // will clear this.dayDragListener - } - if (this.segDragListener) { - this.segDragListener.endInteraction(); // will clear this.segDragListener - } - if (this.segResizeListener) { - this.segResizeListener.endInteraction(); // will clear this.segResizeListener - } - if (this.externalDragListener) { - this.externalDragListener.endInteraction(); // will clear this.externalDragListener - } - }, - - - /* Event Helper - ------------------------------------------------------------------------------------------------------------------*/ - // TODO: should probably move this to Grid.events, like we did event dragging / resizing - - - // Renders a mock event at the given event location, which contains zoned start/end properties. - // Returns all mock event elements. - renderEventLocationHelper: function(eventLocation, sourceSeg) { - var fakeEvent = this.fabricateHelperEvent(eventLocation, sourceSeg); - - return this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering - }, - - - // Builds a fake event given zoned event date properties and a segment is should be inspired from. - // 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. - fabricateHelperEvent: function(eventLocation, sourceSeg) { - var fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible - - fakeEvent.start = eventLocation.start.clone(); - fakeEvent.end = eventLocation.end ? eventLocation.end.clone() : null; - fakeEvent.allDay = null; // force it to be freshly computed by normalizeEventDates - this.view.calendar.normalizeEventDates(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; - } + // restore a mousedown'd 's href (for IE8 bug) + setTimeout(function() { // must be outside of the click's execution + if (subjectHref) { + subjectEl.attr('href', subjectHref); + } + }, 0); + }, - return fakeEvent; - }, + // Utils + // ----------------------------------------------------------------------------------------------------------------- - // Renders a mock event. Given zoned event date properties. - // Must return all mock event elements. - renderHelper: function(eventLocation, sourceSeg) { - // subclasses must implement - }, + // 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)); + } + // makes _methods callable by event name. TODO: kill this + if (this['_' + name]) { + this['_' + name].apply(this, Array.prototype.slice.call(arguments, 1)); + } + } - // Unrenders a mock event - unrenderHelper: function() { - // subclasses must implement - }, + }); - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ + ;; + /* + this.scrollEl is set in DragListener + */ + DragListener.mixin({ + isAutoScroll: false, - // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses. - // Given a span (unzoned start/end and other misc data) - renderSelection: function(span) { - this.renderHighlight(span); - }, + 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 + // defaults + scrollSensitivity: 30, // pixels from edge for scrolling to start + scrollSpeed: 200, // pixels per second, at maximum speed + scrollIntervalMs: 50, // millisecond wait between scroll increment - // Unrenders any visual indications of a selection. Will unrender a highlight by default. - unrenderSelection: function() { - this.unrenderHighlight(); - }, + initAutoScroll: function() { + var scrollEl = this.scrollEl; - // Given the first and last date-spans of a selection, returns another date-span object. - // Subclasses can override and provide additional data in the span object. Will be passed to renderSelection(). - // Will return false if the selection is invalid and this should be indicated to the user. - // Will return null/undefined if a selection invalid but no error should be reported. - computeSelection: function(span0, span1) { - var span = this.computeSelectionSpan(span0, span1); + this.isAutoScroll = + this.options.scroll && + scrollEl && + !scrollEl.is(window) && + !scrollEl.is(document); - if (span && !this.view.calendar.isSelectionSpanAllowed(span)) { - return false; - } + if (this.isAutoScroll) { + // debounce makes sure rapid calls don't happen + this.listenTo(scrollEl, 'scroll', debounce(this.handleDebouncedScroll, 100)); + } + }, + + + destroyAutoScroll: function() { + this.endAutoScroll(); // kill any animation loop + + // remove the scroll handler if there is a scrollEl + if (this.isAutoScroll) { + this.stopListeningTo(this.scrollEl, 'scroll'); // will probably get removed by unbindHandlers too :( + } + }, + + + // Computes and stores the bounding rectangle of scrollEl + computeScrollBounds: function() { + if (this.isAutoScroll) { + this.scrollBounds = getOuterRect(this.scrollEl); + // TODO: use getClientRect in future. but prevents auto scrolling when on top of scrollbars + } + }, + + + // Called when the dragging is in progress and scrolling should be updated + updateAutoScroll: 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 - (getEvY(ev) - bounds.top)) / sensitivity; + bottomCloseness = (sensitivity - (bounds.bottom - getEvY(ev))) / sensitivity; + leftCloseness = (sensitivity - (getEvX(ev) - bounds.left)) / sensitivity; + rightCloseness = (sensitivity - (bounds.right - getEvX(ev))) / 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.endAutoScroll(); + } + }, + + + // Kills any existing scrolling animation loop + endAutoScroll: function() { + if (this.scrollIntervalId) { + clearInterval(this.scrollIntervalId); + this.scrollIntervalId = null; + + this.handleScrollEnd(); + } + }, + + + // Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce) + handleDebouncedScroll: function() { + // recompute all coordinates, but *only* if this is *not* part of our scrolling animation + if (!this.scrollIntervalId) { + this.handleScrollEnd(); + } + }, + + + // Called when scrolling has stopped, whether through auto scroll, or the user scrolling + handleScrollEnd: function() { + } - return span; - }, + }); + ;; + /* Tracks mouse movements over a component and raises events about which hit the mouse is over. + ------------------------------------------------------------------------------------------------------------------------ + options: + - subjectEl + - subjectCenter + */ - // Given two spans, must return the combination of the two. - // TODO: do this separation of concerns (combining VS validation) for event dnd/resize too. - computeSelectionSpan: function(span0, span1) { - var dates = [ span0.start, span0.end, span1.start, span1.end ]; + var HitDragListener = DragListener.extend({ + + component: null, // converts coordinates to hits + // methods: prepareHits, releaseHits, queryHit + + origHit: null, // the hit the mouse was over when listening started + hit: null, // the hit the mouse is over + coordAdjust: null, // delta that will be added to the mouse coordinates when computing collisions + + + constructor: function(component, options) { + DragListener.call(this, options); // call the super-constructor - dates.sort(compareNumbers); // sorts chronologically. works with Moments + this.component = component; + }, - return { start: dates[0].clone(), end: dates[3].clone() }; - }, + // Called when drag listening starts (but a real drag has not necessarily began). + // ev might be undefined if dragging was started manually. + handleInteractionStart: function(ev) { + var subjectEl = this.subjectEl; + var subjectRect; + var origPoint; + var point; + + this.computeCoords(); - /* Highlight - ------------------------------------------------------------------------------------------------------------------*/ + if (ev) { + origPoint = { left: getEvX(ev), top: getEvY(ev) }; + point = origPoint; + // constrain the point to bounds of the element being dragged + if (subjectEl) { + subjectRect = getOuterRect(subjectEl); // used for centering as well + point = constrainPoint(point, subjectRect); + } - // Renders an emphasis on the given date range. Given a span (unzoned start/end and other misc data) - renderHighlight: function(span) { - this.renderFill('highlight', this.spanToSegs(span)); - }, + this.origHit = this.queryHit(point.left, point.top); + // treat the center of the subject as the collision point? + if (subjectEl && this.options.subjectCenter) { - // Unrenders the emphasis on a date range - unrenderHighlight: function() { - this.unrenderFill('highlight'); - }, + // only consider the area the subject overlaps the hit. best for large subjects. + // TODO: skip this if hit didn't supply left/right/top/bottom + if (this.origHit) { + subjectRect = intersectRects(this.origHit, subjectRect) || + subjectRect; // in case there is no intersection + } + point = getRectCenter(subjectRect); + } - // Generates an array of classNames for rendering the highlight. Used by the fill system. - highlightSegClasses: function() { - return [ 'fc-highlight' ]; - }, + this.coordAdjust = diffPoints(point, origPoint); // point - origPoint + } + else { + this.origHit = null; + this.coordAdjust = null; + } + // call the super-method. do it after origHit has been computed + DragListener.prototype.handleInteractionStart.apply(this, arguments); + }, - /* Business Hours - ------------------------------------------------------------------------------------------------------------------*/ + // Recomputes the drag-critical positions of elements + computeCoords: function() { + this.component.prepareHits(); + this.computeScrollBounds(); // why is this here?????? + }, - renderBusinessHours: function() { - }, + // Called when the actual drag has started + handleDragStart: function(ev) { + var hit; - unrenderBusinessHours: function() { - }, + DragListener.prototype.handleDragStart.apply(this, arguments); // call the super-method + // might be different from this.origHit if the min-distance is large + hit = this.queryHit(getEvX(ev), getEvY(ev)); - /* Now Indicator - ------------------------------------------------------------------------------------------------------------------*/ + // report the initial hit the mouse is over + // especially important if no min-distance and drag starts immediately + if (hit) { + this.handleHitOver(hit); + } + }, - getNowIndicatorUnit: function() { - }, + // Called when the drag moves + handleDrag: function(dx, dy, ev) { + var hit; + DragListener.prototype.handleDrag.apply(this, arguments); // call the super-method - renderNowIndicator: function(date) { - }, + hit = this.queryHit(getEvX(ev), getEvY(ev)); + if (!isHitsEqual(hit, this.hit)) { // a different hit than before? + if (this.hit) { + this.handleHitOut(); + } + if (hit) { + this.handleHitOver(hit); + } + } + }, - unrenderNowIndicator: function() { - }, + // Called when dragging has been stopped + handleDragEnd: function() { + this.handleHitDone(); + DragListener.prototype.handleDragEnd.apply(this, arguments); // call the super-method + }, - /* Fill System (highlight, background events, business hours) - -------------------------------------------------------------------------------------------------------------------- - TODO: remove this system. like we did in TimeGrid - */ + // Called when a the mouse has just moved over a new hit + handleHitOver: function(hit) { + var isOrig = isHitsEqual(hit, this.origHit); - // Renders a set of rectangles over the given segments of time. - // MUST RETURN a subset of segs, the segs that were actually rendered. - // Responsible for populating this.elsByFill. TODO: better API for expressing this requirement - renderFill: function(type, segs) { - // subclasses must implement - }, + this.hit = hit; + this.trigger('hitOver', this.hit, isOrig, this.origHit); + }, - // Unrenders a specific type of fill that is currently rendered on the grid - unrenderFill: function(type) { - var el = this.elsByFill[type]; - if (el) { - el.remove(); - delete this.elsByFill[type]; - } - }, + // Called when the mouse has just moved out of a hit + handleHitOut: function() { + if (this.hit) { + this.trigger('hitOut', this.hit); + this.handleHitDone(); + this.hit = null; + } + }, - // Renders and assigns an `el` property for each fill segment. Generic enough to work with different types. - // Only returns segments that successfully rendered. - // To be harnessed by renderFill (implemented by subclasses). - // Analagous to renderFgSegEls. - renderFillSegEls: function(type, segs) { - var _this = this; - var segElMethod = this[type + 'SegEl']; - var html = ''; - var renderedSegs = []; - var i; + // Called after a hitOut. Also called before a dragStop + handleHitDone: function() { + if (this.hit) { + this.trigger('hitDone', this.hit); + } + }, - if (segs.length) { - // build a large concatenation of segment HTML - for (i = 0; i < segs.length; i++) { - html += this.fillSegHtml(type, segs[i]); - } + // Called when the interaction ends, whether there was a real drag or not + handleInteractionEnd: function() { + DragListener.prototype.handleInteractionEnd.apply(this, arguments); // call the super-method - // Grab individual elements from the combined HTML string. Use each as the default rendering. - // Then, compute the 'el' for each segment. - $(html).each(function(i, node) { - var seg = segs[i]; - var el = $(node); + this.origHit = null; + this.hit = null; - // allow custom filter methods per-type - if (segElMethod) { - el = segElMethod.call(_this, seg, el); - } + this.component.releaseHits(); + }, - if (el) { // custom filters did not cancel the render - el = $(el); // allow custom filter to return raw DOM node - // correct element type? (would be bad if a non-TD were inserted into a table for example) - if (el.is(_this.fillSegTag)) { - seg.el = el; - renderedSegs.push(seg); - } - } - }); - } + // Called when scrolling has stopped, whether through auto scroll, or the user scrolling + handleScrollEnd: function() { + DragListener.prototype.handleScrollEnd.apply(this, arguments); // call the super-method - return renderedSegs; - }, + this.computeCoords(); // hits' absolute positions will be in new places. recompute + }, - fillSegTag: 'div', // subclasses can override + // Gets the hit underneath the coordinates for the given mouse event + queryHit: function(left, top) { + if (this.coordAdjust) { + left += this.coordAdjust.left; + top += this.coordAdjust.top; + } - // Builds the HTML needed for one fill segment. Generic enought o work with different types. - fillSegHtml: function(type, seg) { + return this.component.queryHit(left, top); + } - // custom hooks per-type - var classesMethod = this[type + 'SegClasses']; - var cssMethod = this[type + 'SegCss']; + }); - var classes = classesMethod ? classesMethod.call(this, seg) : []; - var css = cssToStr(cssMethod ? cssMethod.call(this, seg) : {}); - return '<' + this.fillSegTag + - (classes.length ? ' class="' + classes.join(' ') + '"' : '') + - (css ? ' style="' + css + '"' : '') + - ' />'; - }, + // Returns `true` if the hits are identically equal. `false` otherwise. Must be from the same component. + // Two null values will be considered equal, as two "out of the component" states are the same. + function isHitsEqual(hit0, hit1) { + if (!hit0 && !hit1) { + return true; + } + if (hit0 && hit1) { + return hit0.component === hit1.component && + isHitPropsWithin(hit0, hit1) && + isHitPropsWithin(hit1, hit0); // ensures all props are identical + } - /* Generic rendering utilities for subclasses - ------------------------------------------------------------------------------------------------------------------*/ + return false; + } - // Computes HTML classNames for a single-day element - getDayClasses: function(date) { - var view = this.view; - var today = view.calendar.getNow(); - var classes = [ 'fc-' + dayIDs[date.day()] ]; - - if ( - view.intervalDuration.as('months') == 1 && - date.month() != view.intervalStart.month() - ) { - classes.push('fc-other-month'); - } - - if (date.isSame(today, 'day')) { - classes.push( - 'fc-today', - view.highlightStateClass - ); - } - else if (date < today) { - classes.push('fc-past'); - } - else { - classes.push('fc-future'); - } - - return classes; - } + // Returns true if all of subHit's non-standard properties are within superHit + function isHitPropsWithin(subHit, superHit) { + for (var propName in subHit) { + if (!/^(component|left|right|top|bottom)$/.test(propName)) { + if (subHit[propName] !== superHit[propName]) { + return false; + } + } + } + return true; + } -}); + ;; -;; + /* Creates a clone of an element and lets it track the mouse as it moves + ----------------------------------------------------------------------------------------------------------------------*/ -/* Event-rendering and event-interaction methods for the abstract Grid class -----------------------------------------------------------------------------------------------------------------------*/ + var MouseFollower = Class.extend(ListenerMixin, { -Grid.mixin({ + options: null, - mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing - isDraggingSeg: false, // is a segment being dragged? boolean - isResizingSeg: false, // is a segment being resized? boolean - isDraggingExternal: false, // jqui-dragging an external element? boolean - segs: null, // the *event* segments currently rendered in the grid. TODO: rename to `eventSegs` + 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 absolute coordinates of the initiating touch/mouse action + y0: null, + x0: null, + + // the number of pixels the mouse has moved from its initial position + topDelta: null, + leftDelta: null, + + 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 + }, - // Renders the given events onto the grid - renderEvents: function(events) { - var bgEvents = []; - var fgEvents = []; - var i; - for (i = 0; i < events.length; i++) { - (isBgEvent(events[i]) ? bgEvents : fgEvents).push(events[i]); - } + // Causes the element to start following the mouse + start: function(ev) { + if (!this.isFollowing) { + this.isFollowing = true; + + this.y0 = getEvY(ev); + this.x0 = getEvX(ev); + this.topDelta = 0; + this.leftDelta = 0; + + if (!this.isHidden) { + this.updatePosition(); + } + + if (getEvIsTouch(ev)) { + this.listenTo($(document), 'touchmove', this.handleMove); + } + else { + this.listenTo($(document), 'mousemove', this.handleMove); + } + } + }, + + + // 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.removeElement(); + + 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; + + this.stopListeningTo($(document)); + + 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() + .addClass(this.options.additionalClass || '') + .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 + }); + + // we don't want long taps or any mouse interaction causing selection/menus. + // would use preventSelection(), but that prevents selectstart, causing problems. + el.addClass('fc-unselectable'); + + el.appendTo(this.parentEl); + } + + return el; + }, + + + // Removes the tracking element if it has already been created + removeElement: 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 + }); + }, - this.segs = [].concat( // record all segs - this.renderBgEvents(bgEvents), - this.renderFgEvents(fgEvents) - ); - }, + // Gets called when the user moves the mouse + handleMove: function(ev) { + this.topDelta = getEvY(ev) - this.y0; + this.leftDelta = getEvX(ev) - this.x0; + + if (!this.isHidden) { + this.updatePosition(); + } + }, - renderBgEvents: function(events) { - var segs = this.eventsToSegs(events); - // renderBgSegs might return a subset of segs, segs that were actually rendered - return this.renderBgSegs(segs) || segs; - }, + // 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(); + } + } + }, - renderFgEvents: function(events) { - var segs = this.eventsToSegs(events); + // Show the tracking element after it has been temporarily hidden + show: function() { + if (this.isHidden) { + this.isHidden = false; + this.updatePosition(); + this.getEl().show(); + } + } - // renderFgSegs might return a subset of segs, segs that were actually rendered - return this.renderFgSegs(segs) || segs; - }, + }); + ;; - // Unrenders all events currently rendered on the grid - unrenderEvents: function() { - this.handleSegMouseout(); // trigger an eventMouseout if user's mouse is over an event - this.clearDragListeners(); + /* An abstract class comprised of a "grid" of areas that each represent a specific datetime + ----------------------------------------------------------------------------------------------------------------------*/ - this.unrenderFgSegs(); - this.unrenderBgSegs(); + var Grid = FC.Grid = Class.extend(ListenerMixin, { - this.segs = null; - }, + view: null, // a View object + isRTL: null, // shortcut to the view's isRTL option + start: null, + end: null, - // Retrieves all rendered segment objects currently rendered on the grid - getEventSegs: function() { - return this.segs || []; - }, + el: null, // the containing element + elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name. + // derived from options + eventTimeFormat: null, + displayEventTime: null, + displayEventEnd: null, - /* Foreground Segment Rendering - ------------------------------------------------------------------------------------------------------------------*/ + minResizeDuration: null, // TODO: hack. set by subclasses. minumum event resize duration + // if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity + // of the date areas. if not defined, assumes to be day and time granularity. + // TODO: port isTimeScale into same system? + largeUnit: null, - // Renders foreground event segments onto the grid. May return a subset of segs that were rendered. - renderFgSegs: function(segs) { - // subclasses must implement - }, + dayDragListener: null, + segDragListener: null, + segResizeListener: null, + externalDragListener: null, - // Unrenders all currently rendered foreground segments - unrenderFgSegs: function() { - // subclasses must implement - }, + constructor: function(view) { + this.view = view; + this.isRTL = view.opt('isRTL'); + this.elsByFill = {}; + }, - // Renders and assigns an `el` property for each foreground event segment. - // Only returns segments that successfully rendered. - // A utility that subclasses may use. - renderFgSegEls: function(segs, disableResizing) { - var view = this.view; - var html = ''; - var renderedSegs = []; - var i; + /* Options + ------------------------------------------------------------------------------------------------------------------*/ - if (segs.length) { // don't build an empty html string - // build a large concatenation of event segment HTML - for (i = 0; i < segs.length; i++) { - html += this.fgSegHtml(segs[i], disableResizing); - } + // Generates the format string used for event time text, if not explicitly defined by 'timeFormat' + computeEventTimeFormat: function() { + return this.view.opt('smallTimeFormat'); + }, - // Grab individual elements from the combined HTML string. Use each as the default rendering. - // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false. - $(html).each(function(i, node) { - var seg = segs[i]; - var el = view.resolveEventEl(seg.event, $(node)); - if (el) { - el.data('fc-seg', seg); // used by handlers - seg.el = el; - renderedSegs.push(seg); - } - }); - } + // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventTime'. + // Only applies to non-all-day events. + computeDisplayEventTime: function() { + return true; + }, - return renderedSegs; - }, + // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd' + computeDisplayEventEnd: function() { + return true; + }, - // Generates the HTML for the default rendering of a foreground event segment. Used by renderFgSegEls() - fgSegHtml: function(seg, disableResizing) { - // subclasses should implement - }, + /* Dates + ------------------------------------------------------------------------------------------------------------------*/ - /* Background Segment Rendering - ------------------------------------------------------------------------------------------------------------------*/ + // Tells the grid about what period of time to display. + // Any date-related internal data should be generated. + setRange: function(range) { + this.start = range.start.clone(); + this.end = range.end.clone(); - // Renders the given background event segments onto the grid. - // Returns a subset of the segs that were actually rendered. - renderBgSegs: function(segs) { - return this.renderFill('bgEvent', segs); - }, + this.rangeUpdated(); + this.processRangeOptions(); + }, - // Unrenders all the currently rendered background event segments - unrenderBgSegs: function() { - this.unrenderFill('bgEvent'); - }, + // Called when internal variables that rely on the range should be updated + rangeUpdated: function() { + }, - // Renders a background event element, given the default rendering. Called by the fill system. - bgEventSegEl: function(seg, el) { - return this.view.resolveEventEl(seg.event, el); // will filter through eventRender - }, + // Updates values that rely on options and also relate to range + processRangeOptions: function() { + var view = this.view; + var displayEventTime; + var displayEventEnd; + this.eventTimeFormat = + view.opt('eventTimeFormat') || + view.opt('timeFormat') || // deprecated + this.computeEventTimeFormat(); - // Generates an array of classNames to be used for the default rendering of a background event. - // Called by the fill system. - bgEventSegClasses: function(seg) { - var event = seg.event; - var source = event.source || {}; + displayEventTime = view.opt('displayEventTime'); + if (displayEventTime == null) { + displayEventTime = this.computeDisplayEventTime(); // might be based off of range + } - return [ 'fc-bgevent' ].concat( - event.className, - source.className || [] - ); - }, + displayEventEnd = view.opt('displayEventEnd'); + if (displayEventEnd == null) { + displayEventEnd = this.computeDisplayEventEnd(); // might be based off of range + } + this.displayEventTime = displayEventTime; + this.displayEventEnd = displayEventEnd; + }, - // Generates a semicolon-separated CSS string to be used for the default rendering of a background event. - // Called by the fill system. - bgEventSegCss: function(seg) { - return { - 'background-color': this.getSegSkinCss(seg)['background-color'] - }; - }, + // Converts a span (has unzoned start/end and any other grid-specific location information) + // into an array of segments (pieces of events whose format is decided by the grid). + spanToSegs: function(span) { + // subclasses must implement + }, - // Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system. - businessHoursSegClasses: function(seg) { - return [ 'fc-nonbusiness', 'fc-bgevent' ]; - }, + // Diffs the two dates, returning a duration, based on granularity of the grid + // TODO: port isTimeScale into this system? + diffDates: function(a, b) { + if (this.largeUnit) { + return diffByUnit(a, b, this.largeUnit); + } + else { + return diffDayTime(a, b); + } + }, - /* Handlers - ------------------------------------------------------------------------------------------------------------------*/ + /* Hit Area + ------------------------------------------------------------------------------------------------------------------*/ - // Attaches event-element-related handlers to the container element and leverage bubbling - bindSegHandlers: function() { - if (FC.isTouchEnabled) { - this.bindSegHandler('touchstart', this.handleSegTouchStart); - } - else { - this.bindSegHandler('mouseenter', this.handleSegMouseover); - this.bindSegHandler('mouseleave', this.handleSegMouseout); - this.bindSegHandler('mousedown', this.handleSegMousedown); - } - - this.bindSegHandler('click', this.handleSegClick); - }, + // Called before one or more queryHit calls might happen. Should prepare any cached coordinates for queryHit + prepareHits: function() { + }, - // Executes a handler for any a user-interaction on a segment. - // Handler gets called with (seg, ev), and with the `this` context of the Grid - bindSegHandler: function(name, handler) { - var _this = this; - this.el.on(name, '.fc-event-container > *', function(ev) { - var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents + // Called when queryHit calls have subsided. Good place to clear any coordinate caches. + releaseHits: function() { + }, - // only call the handlers if there is not a drag/resize in progress - if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) { - return handler.call(_this, seg, ev); // context will be the Grid - } - }); - }, + // Given coordinates from the topleft of the document, return data about the date-related area underneath. + // Can return an object with arbitrary properties (although top/right/left/bottom are encouraged). + // Must have a `grid` property, a reference to this current grid. TODO: avoid this + // The returned object will be processed by getHitSpan and getHitEl. + queryHit: function(leftOffset, topOffset) { + }, - handleSegClick: function(seg, ev) { - return this.view.trigger('eventClick', seg.el[0], seg.event, ev); // can return `false` to cancel - }, + // Given position-level information about a date-related area within the grid, + // should return an object with at least a start/end date. Can provide other information as well. + getHitSpan: function(hit) { + }, - // Updates internal state and triggers handlers for when an event element is moused over - handleSegMouseover: function(seg, ev) { - if (!this.mousedOverSeg) { - this.mousedOverSeg = seg; - this.view.trigger('eventMouseover', seg.el[0], seg.event, ev); - } - }, - - - // Updates internal state and triggers handlers for when an event element is moused out. - // Can be given no arguments, in which case it will mouseout the segment that was previously moused over. - handleSegMouseout: function(seg, ev) { - ev = ev || {}; // if given no args, make a mock mouse event - - if (this.mousedOverSeg) { - seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment - this.mousedOverSeg = null; - this.view.trigger('eventMouseout', seg.el[0], seg.event, ev); - } - }, - - - handleSegTouchStart: function(seg, ev) { - var view = this.view; - var event = seg.event; - var isSelected = view.isEventSelected(event); - var isResizing = false; - var dragListener; - - if (isSelected) { - // only allow resizing of the event is selected - isResizing = this.startSegResize(seg, ev); - } - - if (!isResizing && view.isEventDraggable(event)) { - this.clearDragListeners(); - dragListener = this.buildSegDragListener(seg); - - dragListener._dragStart = function() { // TODO: better way of binding - // if not previously selected, will fire after a delay. then, select the event - if (!isSelected) { - view.selectEvent(event); - } - }; - - dragListener.startInteraction(ev, { - delay: isSelected ? 0 : this.view.opt('longPressDelay') // do delay if not already selected - }); - } - }, - - - handleSegMousedown: function(seg, ev) { - var isResizing = this.startSegResize(seg, ev, { distance: 5 }); - - if (!isResizing && this.view.isEventDraggable(seg.event)) { - this.clearDragListeners(); - this.buildSegDragListener(seg) - .startInteraction(ev, { - distance: 5 - }); - } - }, - - - // returns boolean whether resizing actually started or not - // `dragOptions` are optional - startSegResize: function(seg, ev, dragOptions) { - if ($(ev.target).is('.fc-resizer') && this.view.isEventResizable(seg.event)) { - this.clearDragListeners(); - this.buildSegResizeListener(seg, $(ev.target).is('.fc-start-resizer')) - .startInteraction(ev, dragOptions); - return true; - } - return false; - }, - - - - /* Event Dragging - ------------------------------------------------------------------------------------------------------------------*/ - - - // Builds a listener that will track user-dragging on an event segment. - // Generic enough to work with any type of Grid. - buildSegDragListener: function(seg) { - var _this = this; - var view = this.view; - var calendar = view.calendar; - var el = seg.el; - var event = seg.event; - var isDragging; - var mouseFollower; // A clone of the original element that will move with the mouse - var dropLocation; // zoned event date properties - - // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents - // of the view. - var dragListener = this.segDragListener = new HitDragListener(view, { - scroll: view.opt('dragScroll'), - subjectEl: el, - subjectCenter: true, - interactionStart: function(ev) { - isDragging = false; - mouseFollower = new MouseFollower(seg.el, { - additionalClass: 'fc-dragging', - parentEl: view.el, - opacity: dragListener.isTouch ? null : view.opt('dragOpacity'), - revertDuration: view.opt('dragRevertDuration'), - zIndex: 2 // one above the .fc-view - }); - mouseFollower.hide(); // don't show until we know this is a real drag - mouseFollower.start(ev); - }, - dragStart: function(ev) { - isDragging = true; - _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported - _this.segDragStart(seg, ev); - view.hideEvent(event); // hide all event segments. our mouseFollower will take over - }, - hitOver: function(hit, isOrig, origHit) { - var dragHelperEls; - - // starting hit could be forced (DayGrid.limit) - if (seg.hit) { - origHit = seg.hit; - } - - // since we are querying the parent view, might not belong to this grid - dropLocation = _this.computeEventDrop( - origHit.component.getHitSpan(origHit), - hit.component.getHitSpan(hit), - event - ); - - if (dropLocation && !calendar.isEventSpanAllowed(_this.eventToSpan(dropLocation), event)) { - disableCursor(); - dropLocation = null; - } - - // if a valid drop location, have the subclass render a visual indication - if (dropLocation && (dragHelperEls = view.renderDrag(dropLocation, seg))) { - - dragHelperEls.addClass('fc-dragging'); - if (!dragListener.isTouch) { - _this.applyDragOpacity(dragHelperEls); - } - - mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own - } - else { - mouseFollower.show(); // otherwise, have the helper follow the mouse (no snapping) - } - - if (isOrig) { - dropLocation = null; // needs to have moved hits to be a valid drop - } - }, - hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits - view.unrenderDrag(); // unrender whatever was done in renderDrag - mouseFollower.show(); // show in case we are moving out of all hits - dropLocation = null; - }, - hitDone: function() { // Called after a hitOut OR before a dragEnd - enableCursor(); - }, - interactionEnd: function(ev) { - // do revert animation if hasn't changed. calls a callback when finished (whether animation or not) - mouseFollower.stop(!dropLocation, function() { - if (isDragging) { - view.unrenderDrag(); - view.showEvent(event); - _this.segDragStop(seg, ev); - } - if (dropLocation) { - view.reportEventDrop(event, dropLocation, this.largeUnit, el, ev); - } - }); - _this.segDragListener = null; - } - }); - - return dragListener; - }, - - - // Called before event segment dragging starts - segDragStart: function(seg, ev) { - this.isDraggingSeg = true; - this.view.trigger('eventDragStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy - }, - - - // Called after event segment dragging stops - segDragStop: function(seg, ev) { - this.isDraggingSeg = false; - this.view.trigger('eventDragStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy - }, - - - // Given the spans an event drag began, and the span event was dropped, calculates the new zoned start/end/allDay - // values for the event. Subclasses may override and set additional properties to be used by renderDrag. - // A falsy returned value indicates an invalid drop. - // DOES NOT consider overlap/constraint. - computeEventDrop: function(startSpan, endSpan, event) { - var calendar = this.view.calendar; - var dragStart = startSpan.start; - var dragEnd = endSpan.start; - var delta; - var dropLocation; // zoned event date properties - - if (dragStart.hasTime() === dragEnd.hasTime()) { - delta = this.diffDates(dragEnd, dragStart); - - // if an all-day event was in a timed area and it was dragged to a different time, - // guarantee an end and adjust start/end to have times - if (event.allDay && durationHasTime(delta)) { - dropLocation = { - start: event.start.clone(), - end: calendar.getEventEnd(event), // will be an ambig day - allDay: false // for normalizeEventTimes - }; - calendar.normalizeEventTimes(dropLocation); - } - // othewise, work off existing values - else { - dropLocation = { - start: event.start.clone(), - end: event.end ? event.end.clone() : null, - allDay: event.allDay // keep it the same - }; - } - - dropLocation.start.add(delta); - if (dropLocation.end) { - dropLocation.end.add(delta); - } - } - else { - // if switching from day <-> timed, start should be reset to the dropped date, and the end cleared - dropLocation = { - start: dragEnd.clone(), - end: null, // end should be cleared - allDay: !dragEnd.hasTime() - }; - } - - return dropLocation; - }, - - - // Utility for apply dragOpacity to a jQuery set - applyDragOpacity: function(els) { - var opacity = this.view.opt('dragOpacity'); - - if (opacity != null) { - els.each(function(i, node) { - // Don't use jQuery (will set an IE filter), do it the old fashioned way. - // In IE8, a helper element will disappears if there's a filter. - node.style.opacity = opacity; - }); - } - }, - - - /* External Element Dragging - ------------------------------------------------------------------------------------------------------------------*/ - - - // Called when a jQuery UI drag is initiated anywhere in the DOM - externalDragStart: function(ev, ui) { - var view = this.view; - var el; - var accept; - - if (view.opt('droppable')) { // only listen if this setting is on - el = $((ui ? ui.item : null) || ev.target); - - // Test that the dragged element passes the dropAccept selector or filter function. - // FYI, the default is "*" (matches all) - accept = view.opt('dropAccept'); - if ($.isFunction(accept) ? accept.call(el[0], el) : el.is(accept)) { - if (!this.isDraggingExternal) { // prevent double-listening if fired twice - this.listenToExternalDrag(el, ev, ui); - } - } - } - }, - - - // Called when a jQuery UI drag starts and it needs to be monitored for dropping - listenToExternalDrag: function(el, ev, ui) { - var _this = this; - var calendar = this.view.calendar; - var meta = getDraggedElMeta(el); // extra data about event drop, including possible event to create - var dropLocation; // a null value signals an unsuccessful drag - - // listener that tracks mouse movement over date-associated pixel regions - var dragListener = _this.externalDragListener = new HitDragListener(this, { - interactionStart: function() { - _this.isDraggingExternal = true; - }, - hitOver: function(hit) { - dropLocation = _this.computeExternalDrop( - hit.component.getHitSpan(hit), // since we are querying the parent view, might not belong to this grid - meta - ); - - if ( // invalid hit? - dropLocation && - !calendar.isExternalSpanAllowed(_this.eventToSpan(dropLocation), dropLocation, meta.eventProps) - ) { - disableCursor(); - dropLocation = null; - } - - if (dropLocation) { - _this.renderDrag(dropLocation); // called without a seg parameter - } - }, - hitOut: function() { - dropLocation = null; // signal unsuccessful - }, - hitDone: function() { // Called after a hitOut OR before a dragEnd - enableCursor(); - _this.unrenderDrag(); - }, - interactionEnd: function(ev) { - if (dropLocation) { // element was dropped on a valid hit - _this.view.reportExternalDrop(meta, dropLocation, el, ev, ui); - } - _this.isDraggingExternal = false; - _this.externalDragListener = null; - } - }); - - dragListener.startDrag(ev); // start listening immediately - }, - - - // Given a hit to be dropped upon, and misc data associated with the jqui drag (guaranteed to be a plain object), - // returns the zoned start/end dates for the event that would result from the hypothetical drop. end might be null. - // Returning a null value signals an invalid drop hit. - // DOES NOT consider overlap/constraint. - computeExternalDrop: function(span, meta) { - var calendar = this.view.calendar; - var dropLocation = { - start: calendar.applyTimezone(span.start), // simulate a zoned event start date - end: null - }; - - // if dropped on an all-day span, and element's metadata specified a time, set it - if (meta.startTime && !dropLocation.start.hasTime()) { - dropLocation.start.time(meta.startTime); - } - - if (meta.duration) { - dropLocation.end = dropLocation.start.clone().add(meta.duration); - } - - return dropLocation; - }, - - - - /* Drag Rendering (for both events and an external elements) - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event or external element being dragged. - // `dropLocation` contains hypothetical start/end/allDay values the event would have if dropped. end can be null. - // `seg` is the internal segment object that is being dragged. If dragging an external element, `seg` is null. - // A truthy returned value indicates this method has rendered a helper element. - // Must return elements used for any mock events. - renderDrag: function(dropLocation, seg) { - // subclasses must implement - }, - - - // Unrenders a visual indication of an event or external element being dragged - unrenderDrag: function() { - // subclasses must implement - }, - - - /* Resizing - ------------------------------------------------------------------------------------------------------------------*/ - - - // Creates a listener that tracks the user as they resize an event segment. - // Generic enough to work with any type of Grid. - buildSegResizeListener: function(seg, isStart) { - var _this = this; - var view = this.view; - var calendar = view.calendar; - var el = seg.el; - var event = seg.event; - var eventEnd = calendar.getEventEnd(event); - var isDragging; - var resizeLocation; // zoned event date properties. falsy if invalid resize - - // Tracks mouse movement over the *grid's* coordinate map - var dragListener = this.segResizeListener = new HitDragListener(this, { - scroll: view.opt('dragScroll'), - subjectEl: el, - interactionStart: function() { - isDragging = false; - }, - dragStart: function(ev) { - isDragging = true; - _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported - _this.segResizeStart(seg, ev); - }, - hitOver: function(hit, isOrig, origHit) { - var origHitSpan = _this.getHitSpan(origHit); - var hitSpan = _this.getHitSpan(hit); - - resizeLocation = isStart ? - _this.computeEventStartResize(origHitSpan, hitSpan, event) : - _this.computeEventEndResize(origHitSpan, hitSpan, event); - - if (resizeLocation) { - if (!calendar.isEventSpanAllowed(_this.eventToSpan(resizeLocation), event)) { - disableCursor(); - resizeLocation = null; - } - // no change? (TODO: how does this work with timezones?) - else if (resizeLocation.start.isSame(event.start) && resizeLocation.end.isSame(eventEnd)) { - resizeLocation = null; - } - } - - if (resizeLocation) { - view.hideEvent(event); - _this.renderEventResize(resizeLocation, seg); - } - }, - hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits - resizeLocation = null; - }, - hitDone: function() { // resets the rendering to show the original event - _this.unrenderEventResize(); - view.showEvent(event); - enableCursor(); - }, - interactionEnd: function(ev) { - if (isDragging) { - _this.segResizeStop(seg, ev); - } - if (resizeLocation) { // valid date to resize to? - view.reportEventResize(event, resizeLocation, this.largeUnit, el, ev); - } - _this.segResizeListener = null; - } - }); - - return dragListener; - }, - - - // Called before event segment resizing starts - segResizeStart: function(seg, ev) { - this.isResizingSeg = true; - this.view.trigger('eventResizeStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy - }, - - - // Called after event segment resizing stops - segResizeStop: function(seg, ev) { - this.isResizingSeg = false; - this.view.trigger('eventResizeStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy - }, - - - // Returns new date-information for an event segment being resized from its start - computeEventStartResize: function(startSpan, endSpan, event) { - return this.computeEventResize('start', startSpan, endSpan, event); - }, - - - // Returns new date-information for an event segment being resized from its end - computeEventEndResize: function(startSpan, endSpan, event) { - return this.computeEventResize('end', startSpan, endSpan, event); - }, - - - // Returns new zoned date information for an event segment being resized from its start OR end - // `type` is either 'start' or 'end'. - // DOES NOT consider overlap/constraint. - computeEventResize: function(type, startSpan, endSpan, event) { - var calendar = this.view.calendar; - var delta = this.diffDates(endSpan[type], startSpan[type]); - var resizeLocation; // zoned event date properties - var defaultDuration; - - // build original values to work from, guaranteeing a start and end - resizeLocation = { - start: event.start.clone(), - end: calendar.getEventEnd(event), - allDay: event.allDay - }; - - // if an all-day event was in a timed area and was resized to a time, adjust start/end to have times - if (resizeLocation.allDay && durationHasTime(delta)) { - resizeLocation.allDay = false; - calendar.normalizeEventTimes(resizeLocation); - } - - resizeLocation[type].add(delta); // apply delta to start or end - - // if the event was compressed too small, find a new reasonable duration for it - if (!resizeLocation.start.isBefore(resizeLocation.end)) { - - defaultDuration = - this.minResizeDuration || // TODO: hack - (event.allDay ? - calendar.defaultAllDayEventDuration : - calendar.defaultTimedEventDuration); - - if (type == 'start') { // resizing the start? - resizeLocation.start = resizeLocation.end.clone().subtract(defaultDuration); - } - else { // resizing the end? - resizeLocation.end = resizeLocation.start.clone().add(defaultDuration); - } - } - - return resizeLocation; - }, - - - // Renders a visual indication of an event being resized. - // `range` has the updated dates of the event. `seg` is the original segment object involved in the drag. - // Must return elements used for any mock events. - renderEventResize: function(range, seg) { - // subclasses must implement - }, - - - // Unrenders a visual indication of an event being resized. - unrenderEventResize: function() { - // subclasses must implement - }, - - - /* Rendering Utils - ------------------------------------------------------------------------------------------------------------------*/ - - - // Compute the text that should be displayed on an event's element. - // `range` can be the Event object itself, or something range-like, with at least a `start`. - // If event times are disabled, or the event has no time, will return a blank string. - // If not specified, formatStr will default to the eventTimeFormat setting, - // and displayEnd will default to the displayEventEnd setting. - getEventTimeText: function(range, formatStr, displayEnd) { - - if (formatStr == null) { - formatStr = this.eventTimeFormat; - } - - if (displayEnd == null) { - displayEnd = this.displayEventEnd; - } - - if (this.displayEventTime && range.start.hasTime()) { - if (displayEnd && range.end) { - return this.view.formatRange(range, formatStr); - } - else { - return range.start.format(formatStr); - } - } - - return ''; - }, - - - // Generic utility for generating the HTML classNames for an event segment's element - getSegClasses: function(seg, isDraggable, isResizable) { - var view = this.view; - var event = seg.event; - var classes = [ - 'fc-event', - seg.isStart ? 'fc-start' : 'fc-not-start', - seg.isEnd ? 'fc-end' : 'fc-not-end' - ].concat( - event.className, - event.source ? event.source.className : [] - ); - - if (isDraggable) { - classes.push('fc-draggable'); - } - if (isResizable) { - classes.push('fc-resizable'); - } - - // event is currently selected? attach a className. - if (view.isEventSelected(event)) { - classes.push('fc-selected'); - } - - return classes; - }, - - - // Utility for generating event skin-related CSS properties - getSegSkinCss: function(seg) { - var event = seg.event; - var view = this.view; - var source = event.source || {}; - var eventColor = event.color; - var sourceColor = source.color; - var optionColor = view.opt('eventColor'); - - return { - 'background-color': - event.backgroundColor || - eventColor || - source.backgroundColor || - sourceColor || - view.opt('eventBackgroundColor') || - optionColor, - 'border-color': - event.borderColor || - eventColor || - source.borderColor || - sourceColor || - view.opt('eventBorderColor') || - optionColor, - color: - event.textColor || - source.textColor || - view.opt('eventTextColor') - }; - }, - - - /* Converting events -> eventRange -> eventSpan -> eventSegs - ------------------------------------------------------------------------------------------------------------------*/ - - - // Generates an array of segments for the given single event - // Can accept an event "location" as well (which only has start/end and no allDay) - eventToSegs: function(event) { - return this.eventsToSegs([ event ]); - }, - - - eventToSpan: function(event) { - return this.eventToSpans(event)[0]; - }, - - - // Generates spans (always unzoned) for the given event. - // Does not do any inverting for inverse-background events. - // Can accept an event "location" as well (which only has start/end and no allDay) - eventToSpans: function(event) { - var range = this.eventToRange(event); - return this.eventRangeToSpans(range, event); - }, - - - - // Converts an array of event objects into an array of event segment objects. - // A custom `segSliceFunc` may be given for arbitrarily slicing up events. - // Doesn't guarantee an order for the resulting array. - eventsToSegs: function(allEvents, segSliceFunc) { - var _this = this; - var eventsById = groupEventsById(allEvents); - var segs = []; - - $.each(eventsById, function(id, events) { - var ranges = []; - var i; - - for (i = 0; i < events.length; i++) { - ranges.push(_this.eventToRange(events[i])); - } - - // inverse-background events (utilize only the first event in calculations) - if (isInverseBgEvent(events[0])) { - ranges = _this.invertRanges(ranges); - - for (i = 0; i < ranges.length; i++) { - segs.push.apply(segs, // append to - _this.eventRangeToSegs(ranges[i], events[0], segSliceFunc)); - } - } - // normal event ranges - else { - for (i = 0; i < ranges.length; i++) { - segs.push.apply(segs, // append to - _this.eventRangeToSegs(ranges[i], events[i], segSliceFunc)); - } - } - }); - - return segs; - }, - - - // Generates the unzoned start/end dates an event appears to occupy - // Can accept an event "location" as well (which only has start/end and no allDay) - eventToRange: function(event) { - return { - start: event.start.clone().stripZone(), - end: ( - event.end ? - event.end.clone() : - // derive the end from the start and allDay. compute allDay if necessary - this.view.calendar.getDefaultEventEnd( - event.allDay != null ? - event.allDay : - !event.start.hasTime(), - event.start - ) - ).stripZone() - }; - }, - - - // Given an event's range (unzoned start/end), and the event itself, - // slice into segments (using the segSliceFunc function if specified) - eventRangeToSegs: function(range, event, segSliceFunc) { - var spans = this.eventRangeToSpans(range, event); - var segs = []; - var i; - - for (i = 0; i < spans.length; i++) { - segs.push.apply(segs, // append to - this.eventSpanToSegs(spans[i], event, segSliceFunc)); - } - - return segs; - }, - - - // Given an event's unzoned date range, return an array of "span" objects. - // Subclasses can override. - eventRangeToSpans: function(range, event) { - return [ $.extend({}, range) ]; // copy into a single-item array - }, - - - // Given an event's span (unzoned start/end and other misc data), and the event itself, - // slices into segments and attaches event-derived properties to them. - eventSpanToSegs: function(span, event, segSliceFunc) { - var segs = segSliceFunc ? segSliceFunc(span) : this.spanToSegs(span); - var i, seg; - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - seg.event = event; - seg.eventStartMS = +span.start; // TODO: not the best name after making spans unzoned - seg.eventDurationMS = span.end - span.start; - } - - return segs; - }, - - - // Produces a new array of range objects that will cover all the time NOT covered by the given ranges. - // SIDE EFFECT: will mutate the given array and will use its date references. - invertRanges: function(ranges) { - var view = this.view; - var viewStart = view.start.clone(); // need a copy - var viewEnd = view.end.clone(); // need a copy - var inverseRanges = []; - var start = viewStart; // the end of the previous range. the start of the new range - var i, range; - - // ranges need to be in order. required for our date-walking algorithm - ranges.sort(compareRanges); - - for (i = 0; i < ranges.length; i++) { - range = ranges[i]; - - // add the span of time before the event (if there is any) - if (range.start > start) { // compare millisecond time (skip any ambig logic) - inverseRanges.push({ - start: start, - end: range.start - }); - } - - start = range.end; - } - - // add the span of time after the last event (if there is any) - if (start < viewEnd) { // compare millisecond time (skip any ambig logic) - inverseRanges.push({ - start: start, - end: viewEnd - }); - } - - return inverseRanges; - }, - - - sortEventSegs: function(segs) { - segs.sort(proxy(this, 'compareEventSegs')); - }, - - - // A cmp function for determining which segments should take visual priority - compareEventSegs: function(seg1, seg2) { - return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first - seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first - seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1) - compareByFieldSpecs(seg1.event, seg2.event, this.view.eventOrderSpecs); - } -}); + // Given position-level information about a date-related area within the grid, + // should return a jQuery element that best represents it. passed to dayClick callback. + getHitEl: function(hit) { + }, -/* Utilities -----------------------------------------------------------------------------------------------------------------------*/ + /* Rendering + ------------------------------------------------------------------------------------------------------------------*/ -function isBgEvent(event) { // returns true if background OR inverse-background - var rendering = getEventRendering(event); - return rendering === 'background' || rendering === 'inverse-background'; -} -FC.isBgEvent = isBgEvent; // export + // Sets the container element that the grid should render inside of. + // Does other DOM-related initializations. + setElement: function(el) { + this.el = el; + preventSelection(el); + if (this.view.calendar.isTouch) { + this.bindDayHandler('touchstart', this.dayTouchStart); + } + else { + this.bindDayHandler('mousedown', this.dayMousedown); + } -function isInverseBgEvent(event) { - return getEventRendering(event) === 'inverse-background'; -} + // attach event-element-related handlers. in Grid.events + // same garbage collection note as above. + this.bindSegHandlers(); + this.bindGlobalHandlers(); + }, -function getEventRendering(event) { - return firstDefined((event.source || {}).rendering, event.rendering); -} + bindDayHandler: function(name, handler) { + var _this = this; -function groupEventsById(events) { - var eventsById = {}; - var i, event; + // attach a handler to the grid's root element. + // jQuery will take care of unregistering them when removeElement gets called. + this.el.on(name, 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) + ) { + return handler.call(_this, ev); + } + }); + }, - for (i = 0; i < events.length; i++) { - event = events[i]; - (eventsById[event._id] || (eventsById[event._id] = [])).push(event); - } - return eventsById; -} - - -// A cmp function for determining which non-inverted "ranges" (see above) happen earlier -function compareRanges(range1, range2) { - return range1.start - range2.start; // earlier ranges go first -} - - -/* External-Dragging-Element Data -----------------------------------------------------------------------------------------------------------------------*/ - -// Require all HTML5 data-* attributes used by FullCalendar to have this prefix. -// A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event. -FC.dataAttrPrefix = ''; - -// Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure -// to be used for Event Object creation. -// A defined `.eventProps`, even when empty, indicates that an event should be created. -function getDraggedElMeta(el) { - var prefix = FC.dataAttrPrefix; - var eventProps; // properties for creating the event, not related to date/time - var startTime; // a Duration - var duration; - var stick; - - if (prefix) { prefix += '-'; } - eventProps = el.data(prefix + 'event') || null; - - if (eventProps) { - if (typeof eventProps === 'object') { - eventProps = $.extend({}, eventProps); // make a copy - } - else { // something like 1 or true. still signal event creation - eventProps = {}; - } - - // pluck special-cased date/time properties - startTime = eventProps.start; - if (startTime == null) { startTime = eventProps.time; } // accept 'time' as well - duration = eventProps.duration; - stick = eventProps.stick; - delete eventProps.start; - delete eventProps.time; - delete eventProps.duration; - delete eventProps.stick; - } - - // fallback to standalone attribute values for each of the date/time properties - if (startTime == null) { startTime = el.data(prefix + 'start'); } - if (startTime == null) { startTime = el.data(prefix + 'time'); } // accept 'time' as well - if (duration == null) { duration = el.data(prefix + 'duration'); } - if (stick == null) { stick = el.data(prefix + 'stick'); } - - // massage into correct data types - startTime = startTime != null ? moment.duration(startTime) : null; - duration = duration != null ? moment.duration(duration) : null; - stick = Boolean(stick); - - return { eventProps: eventProps, startTime: startTime, duration: duration, stick: stick }; -} - - -;; - -/* -A set of rendering and date-related methods for a visual component comprised of one or more rows of day columns. -Prerequisite: the object being mixed into needs to be a *Grid* -*/ -var DayTableMixin = FC.DayTableMixin = { - - breakOnWeeks: false, // should create a new row for each week? - dayDates: null, // whole-day dates for each column. left to right - dayIndices: null, // for each day from start, the offset - daysPerRow: null, - rowCnt: null, - colCnt: null, - colHeadFormat: null, - - - // Populates internal variables used for date calculation and rendering - updateDayTable: function() { - var view = this.view; - var date = this.start.clone(); - var dayIndex = -1; - var dayIndices = []; - var dayDates = []; - var daysPerRow; - var firstDay; - var rowCnt; - - while (date.isBefore(this.end)) { // loop each day from start to end - if (view.isHiddenDay(date)) { - dayIndices.push(dayIndex + 0.5); // mark that it's between indices - } - else { - dayIndex++; - dayIndices.push(dayIndex); - dayDates.push(date.clone()); - } - date.add(1, 'days'); - } - - if (this.breakOnWeeks) { - // count columns until the day-of-week repeats - firstDay = dayDates[0].day(); - for (daysPerRow = 1; daysPerRow < dayDates.length; daysPerRow++) { - if (dayDates[daysPerRow].day() == firstDay) { - break; - } - } - rowCnt = Math.ceil(dayDates.length / daysPerRow); - } - else { - rowCnt = 1; - daysPerRow = dayDates.length; - } - - this.dayDates = dayDates; - this.dayIndices = dayIndices; - this.daysPerRow = daysPerRow; - this.rowCnt = rowCnt; - - this.updateDayTableCols(); - }, - - - // Computes and assigned the colCnt property and updates any options that may be computed from it - updateDayTableCols: function() { - this.colCnt = this.computeColCnt(); - this.colHeadFormat = this.view.opt('columnFormat') || this.computeColHeadFormat(); - }, - - - // Determines how many columns there should be in the table - computeColCnt: function() { - return this.daysPerRow; - }, - - - // Computes the ambiguously-timed moment for the given cell - getCellDate: function(row, col) { - return this.dayDates[ - this.getCellDayIndex(row, col) - ].clone(); - }, - - - // Computes the ambiguously-timed date range for the given cell - getCellRange: function(row, col) { - var start = this.getCellDate(row, col); - var end = start.clone().add(1, 'days'); - - return { start: start, end: end }; - }, - - - // Returns the number of day cells, chronologically, from the first of the grid (0-based) - getCellDayIndex: function(row, col) { - return row * this.daysPerRow + this.getColDayIndex(col); - }, - - - // Returns the numner of day cells, chronologically, from the first cell in *any given row* - getColDayIndex: function(col) { - if (this.isRTL) { - return this.colCnt - 1 - col; - } - else { - return col; - } - }, - - - // Given a date, returns its chronolocial cell-index from the first cell of the grid. - // If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets. - // If before the first offset, returns a negative number. - // If after the last offset, returns an offset past the last cell offset. - // Only works for *start* dates of cells. Will not work for exclusive end dates for cells. - getDateDayIndex: function(date) { - var dayIndices = this.dayIndices; - var dayOffset = date.diff(this.start, 'days'); - - if (dayOffset < 0) { - return dayIndices[0] - 1; - } - else if (dayOffset >= dayIndices.length) { - return dayIndices[dayIndices.length - 1] + 1; - } - else { - return dayIndices[dayOffset]; - } - }, - - - /* Options - ------------------------------------------------------------------------------------------------------------------*/ - - - // Computes a default column header formatting string if `colFormat` is not explicitly defined - computeColHeadFormat: function() { - // if more than one week row, or if there are a lot of columns with not much space, - // put just the day numbers will be in each cell - if (this.rowCnt > 1 || this.colCnt > 10) { - return 'ddd'; // "Sat" - } - // multiple days, so full single date string WON'T be in title text - else if (this.colCnt > 1) { - return this.view.opt('dayOfMonthFormat'); // "Sat 12/10" - } - // single day, so full single date string will probably be in title text - else { - return 'dddd'; // "Saturday" - } - }, - - - /* Slicing - ------------------------------------------------------------------------------------------------------------------*/ - - - // Slices up a date range into a segment for every week-row it intersects with - sliceRangeByRow: function(range) { - var daysPerRow = this.daysPerRow; - var normalRange = this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold - var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index - var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index - var segs = []; - var row; - var rowFirst, rowLast; // inclusive day-index range for current row - var segFirst, segLast; // inclusive day-index range for segment - - for (row = 0; row < this.rowCnt; row++) { - rowFirst = row * daysPerRow; - rowLast = rowFirst + daysPerRow - 1; - - // intersect segment's offset range with the row's - segFirst = Math.max(rangeFirst, rowFirst); - segLast = Math.min(rangeLast, rowLast); - - // deal with in-between indices - segFirst = Math.ceil(segFirst); // in-between starts round to next cell - segLast = Math.floor(segLast); // in-between ends round to prev cell - - if (segFirst <= segLast) { // was there any intersection with the current row? - segs.push({ - row: row, - - // normalize to start of row - firstRowDayIndex: segFirst - rowFirst, - lastRowDayIndex: segLast - rowFirst, - - // must be matching integers to be the segment's start/end - isStart: segFirst === rangeFirst, - isEnd: segLast === rangeLast - }); - } - } - - return segs; - }, - - - // Slices up a date range into a segment for every day-cell it intersects with. - // TODO: make more DRY with sliceRangeByRow somehow. - sliceRangeByDay: function(range) { - var daysPerRow = this.daysPerRow; - var normalRange = this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold - var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index - var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index - var segs = []; - var row; - var rowFirst, rowLast; // inclusive day-index range for current row - var i; - var segFirst, segLast; // inclusive day-index range for segment - - for (row = 0; row < this.rowCnt; row++) { - rowFirst = row * daysPerRow; - rowLast = rowFirst + daysPerRow - 1; - - for (i = rowFirst; i <= rowLast; i++) { - - // intersect segment's offset range with the row's - segFirst = Math.max(rangeFirst, i); - segLast = Math.min(rangeLast, i); - - // deal with in-between indices - segFirst = Math.ceil(segFirst); // in-between starts round to next cell - segLast = Math.floor(segLast); // in-between ends round to prev cell - - if (segFirst <= segLast) { // was there any intersection with the current row? - segs.push({ - row: row, - - // normalize to start of row - firstRowDayIndex: segFirst - rowFirst, - lastRowDayIndex: segLast - rowFirst, - - // must be matching integers to be the segment's start/end - isStart: segFirst === rangeFirst, - isEnd: segLast === rangeLast - }); - } - } - } - - return segs; - }, - - - /* Header Rendering - ------------------------------------------------------------------------------------------------------------------*/ + // Removes the grid's container element from the DOM. Undoes any other DOM-related attachments. + // DOES NOT remove any content beforehand (doesn't clear events or call unrenderDates), unlike View + removeElement: function() { + this.unbindGlobalHandlers(); + this.clearDragListeners(); + this.el.remove(); + + // NOTE: we don't null-out this.el for the same reasons we don't do it within View::removeElement + }, + + + // Renders the basic structure of grid view before any content is rendered + renderSkeleton: function() { + // subclasses should implement + }, + + + // Renders the grid's date-related content (like areas that represent days/times). + // Assumes setRange has already been called and the skeleton has already been rendered. + renderDates: function() { + // subclasses should implement + }, + + + // Unrenders the grid's date-related content + unrenderDates: function() { + // subclasses should implement + }, + + + /* Handlers + ------------------------------------------------------------------------------------------------------------------*/ + + + // Binds DOM handlers to elements that reside outside the grid, such as the document + bindGlobalHandlers: function() { + this.listenTo($(document), { + dragstart: this.externalDragStart, // jqui + sortstart: this.externalDragStart // jqui + }); + }, + + + // Unbinds DOM handlers from elements that reside outside the grid + unbindGlobalHandlers: function() { + this.stopListeningTo($(document)); + }, + + + // Process a mousedown on an element that represents a day. For day clicking and selecting. + dayMousedown: function(ev) { + this.clearDragListeners(); + this.buildDayDragListener().startInteraction(ev, { + //distance: 5, // needs more work if we want dayClick to fire correctly + }); + }, + + + dayTouchStart: function(ev) { + this.clearDragListeners(); + this.buildDayDragListener().startInteraction(ev, { + delay: this.view.opt('longPressDelay') + }); + }, + + + // Creates a listener that tracks the user's drag across day elements. + // For day clicking and selecting. + buildDayDragListener: function() { + var _this = this; + var view = this.view; + var isSelectable = view.opt('selectable'); + var dayClickHit; // null if invalid dayClick + var selectionSpan; // 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 = this.dayDragListener = new HitDragListener(this, { + scroll: view.opt('dragScroll'), + interactionStart: function() { + dayClickHit = dragListener.origHit; + }, + dragStart: function() { + view.unselect(); // since we could be rendering a new selection, we want to clear any old one + }, + hitOver: function(hit, isOrig, origHit) { + if (origHit) { // click needs to have started on a hit + + // if user dragged to another cell at any point, it can no longer be a dayClick + if (!isOrig) { + dayClickHit = null; + } + + if (isSelectable) { + selectionSpan = _this.computeSelection( + _this.getHitSpan(origHit), + _this.getHitSpan(hit) + ); + if (selectionSpan) { + _this.renderSelection(selectionSpan); + } + else if (selectionSpan === false) { + disableCursor(); + } + } + } + }, + hitOut: function() { + dayClickHit = null; + selectionSpan = null; + _this.unrenderSelection(); + enableCursor(); + }, + interactionEnd: function(ev) { + if (dayClickHit) { + view.triggerDayClick( + _this.getHitSpan(dayClickHit), + _this.getHitEl(dayClickHit), + ev + ); + } + if (selectionSpan) { + // the selection will already have been rendered. just report it + view.reportSelection(selectionSpan, ev); + } + enableCursor(); + _this.dayDragListener = null; + } + }); + + return dragListener; + }, + + + // Kills all in-progress dragging. + // Useful for when public API methods that result in re-rendering are invoked during a drag. + // Also useful for when touch devices misbehave and don't fire their touchend. + clearDragListeners: function() { + if (this.dayDragListener) { + this.dayDragListener.endInteraction(); // will clear this.dayDragListener + } + if (this.segDragListener) { + this.segDragListener.endInteraction(); // will clear this.segDragListener + } + if (this.segResizeListener) { + this.segResizeListener.endInteraction(); // will clear this.segResizeListener + } + if (this.externalDragListener) { + this.externalDragListener.endInteraction(); // will clear this.externalDragListener + } + }, + + + /* Event Helper + ------------------------------------------------------------------------------------------------------------------*/ + // TODO: should probably move this to Grid.events, like we did event dragging / resizing + + + // Renders a mock event at the given event location, which contains zoned start/end properties. + // Returns all mock event elements. + renderEventLocationHelper: function(eventLocation, sourceSeg) { + var fakeEvent = this.fabricateHelperEvent(eventLocation, sourceSeg); + + return this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering + }, + + + // Builds a fake event given zoned event date properties and a segment is should be inspired from. + // 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. + fabricateHelperEvent: function(eventLocation, sourceSeg) { + var fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible + + fakeEvent.start = eventLocation.start.clone(); + fakeEvent.end = eventLocation.end ? eventLocation.end.clone() : null; + fakeEvent.allDay = null; // force it to be freshly computed by normalizeEventDates + this.view.calendar.normalizeEventDates(fakeEvent); + + // this extra className will be useful for differentiating real events from mock events in CSS + fakeEvent.className = (fakeEvent.className || []).concat('fc-helper'); - renderHeadHtml: function() { - var view = this.view; - - return '' + - '
' + - '' + - '' + - this.renderHeadTrHtml() + - '' + - '
' + - '
'; - }, + // if something external is being dragged in, don't render a resizer + if (!sourceSeg) { + fakeEvent.editable = false; + } + return fakeEvent; + }, - renderHeadIntroHtml: function() { - return this.renderIntroHtml(); // fall back to generic - }, + // Renders a mock event. Given zoned event date properties. + // Must return all mock event elements. + renderHelper: function(eventLocation, sourceSeg) { + // subclasses must implement + }, - renderHeadTrHtml: function() { - return '' + - '' + - (this.isRTL ? '' : this.renderHeadIntroHtml()) + - this.renderHeadDateCellsHtml() + - (this.isRTL ? this.renderHeadIntroHtml() : '') + - ''; - }, + // Unrenders a mock event + unrenderHelper: function() { + // subclasses must implement + }, - renderHeadDateCellsHtml: function() { - var htmls = []; - var col, date; - for (col = 0; col < this.colCnt; col++) { - date = this.getCellDate(0, col); - htmls.push(this.renderHeadDateCellHtml(date)); - } + /* Selection + ------------------------------------------------------------------------------------------------------------------*/ - return htmls.join(''); - }, + // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses. + // Given a span (unzoned start/end and other misc data) + renderSelection: function(span) { + this.renderHighlight(span); + }, - // TODO: when internalApiVersion, accept an object for HTML attributes - // (colspan should be no different) - renderHeadDateCellHtml: function(date, colspan, otherAttrs) { - var view = this.view; - return '' + - ' 1 ? - ' colspan="' + colspan + '"' : - '') + - (otherAttrs ? - ' ' + otherAttrs : - '') + - '>' + - htmlEscape(date.format(this.colHeadFormat)) + - ''; - }, + // Unrenders any visual indications of a selection. Will unrender a highlight by default. + unrenderSelection: function() { + this.unrenderHighlight(); + }, - /* Background Rendering - ------------------------------------------------------------------------------------------------------------------*/ + // Given the first and last date-spans of a selection, returns another date-span object. + // Subclasses can override and provide additional data in the span object. Will be passed to renderSelection(). + // Will return false if the selection is invalid and this should be indicated to the user. + // Will return null/undefined if a selection invalid but no error should be reported. + computeSelection: function(span0, span1) { + var span = this.computeSelectionSpan(span0, span1); + if (span && !this.view.calendar.isSelectionSpanAllowed(span)) { + return false; + } - renderBgTrHtml: function(row) { - return '' + - '' + - (this.isRTL ? '' : this.renderBgIntroHtml(row)) + - this.renderBgCellsHtml(row) + - (this.isRTL ? this.renderBgIntroHtml(row) : '') + - ''; - }, + return span; + }, - renderBgIntroHtml: function(row) { - return this.renderIntroHtml(); // fall back to generic - }, + // Given two spans, must return the combination of the two. + // TODO: do this separation of concerns (combining VS validation) for event dnd/resize too. + computeSelectionSpan: function(span0, span1) { + var dates = [ span0.start, span0.end, span1.start, span1.end ]; + dates.sort(compareNumbers); // sorts chronologically. works with Moments - renderBgCellsHtml: function(row) { - var htmls = []; - var col, date; + return { start: dates[0].clone(), end: dates[3].clone() }; + }, - for (col = 0; col < this.colCnt; col++) { - date = this.getCellDate(row, col); - htmls.push(this.renderBgCellHtml(date)); - } - return htmls.join(''); - }, + /* Highlight + ------------------------------------------------------------------------------------------------------------------*/ - renderBgCellHtml: function(date, otherAttrs) { - var view = this.view; - var classes = this.getDayClasses(date); + // Renders an emphasis on the given date range. Given a span (unzoned start/end and other misc data) + renderHighlight: function(span) { + this.renderFill('highlight', this.spanToSegs(span)); + }, - classes.unshift('fc-day', view.widgetContentClass); - return ''; - }, + // Unrenders the emphasis on a date range + unrenderHighlight: function() { + this.unrenderFill('highlight'); + }, - /* Generic - ------------------------------------------------------------------------------------------------------------------*/ + // Generates an array of classNames for rendering the highlight. Used by the fill system. + highlightSegClasses: function() { + return [ 'fc-highlight' ]; + }, - // Generates the default HTML intro for any row. User classes should override - renderIntroHtml: function() { - }, + /* Business Hours + ------------------------------------------------------------------------------------------------------------------*/ - // TODO: a generic method for dealing with , RTL, intro - // when increment internalApiVersion - // wrapTr (scheduler) + renderBusinessHours: function() { + }, - /* Utils - ------------------------------------------------------------------------------------------------------------------*/ + unrenderBusinessHours: function() { + }, - // Applies the generic "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. - bookendCells: function(trEl) { - var introHtml = this.renderIntroHtml(); + /* Now Indicator + ------------------------------------------------------------------------------------------------------------------*/ - if (introHtml) { - if (this.isRTL) { - trEl.append(introHtml); - } - else { - trEl.prepend(introHtml); - } - } - } -}; - -;; - -/* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week. -----------------------------------------------------------------------------------------------------------------------*/ - -var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, { - - numbersVisible: false, // should render a row for day/week numbers? set by outside view. TODO: make internal - bottomCoordPadding: 0, // hack for extending the hit area for the last row of the coordinate grid - - rowEls: null, // set of fake row elements - cellEls: null, // set of whole-day elements comprising the row's background - helperEls: null, // set of cell skeleton elements for rendering the mock event "helper" - - rowCoordCache: null, - colCoordCache: null, - - - // Renders the rows and columns into the component's `this.el`, which should already be assigned. - // isRigid determins whether the individual rows should ignore the contents and be a constant height. - // Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient. - renderDates: function(isRigid) { - var view = this.view; - var rowCnt = this.rowCnt; - var colCnt = this.colCnt; - var html = ''; - var row; - var col; - - for (row = 0; row < rowCnt; row++) { - html += this.renderDayRowHtml(row, isRigid); - } - this.el.html(html); - - this.rowEls = this.el.find('.fc-row'); - this.cellEls = this.el.find('.fc-day'); - - this.rowCoordCache = new CoordCache({ - els: this.rowEls, - isVertical: true - }); - this.colCoordCache = new CoordCache({ - els: this.cellEls.slice(0, this.colCnt), // only the first row - isHorizontal: true - }); - - // trigger dayRender with each cell's element - for (row = 0; row < rowCnt; row++) { - for (col = 0; col < colCnt; col++) { - view.trigger( - 'dayRender', - null, - this.getCellDate(row, col), - this.getCellEl(row, col) - ); - } - } - }, + getNowIndicatorUnit: function() { + }, - unrenderDates: function() { - this.removeSegPopover(); - }, + renderNowIndicator: function(date) { + }, - renderBusinessHours: function() { - var events = this.view.calendar.getBusinessHoursEvents(true); // wholeDay=true - var segs = this.eventsToSegs(events); + unrenderNowIndicator: function() { + }, - this.renderFill('businessHours', segs, 'bgevent'); - }, + /* Fill System (highlight, background events, business hours) + -------------------------------------------------------------------------------------------------------------------- + TODO: remove this system. like we did in TimeGrid + */ - // Generates the HTML for a single row, which is a div that wraps a table. - // `row` is the row number. - renderDayRowHtml: function(row, isRigid) { - var view = this.view; - var classes = [ 'fc-row', 'fc-week', view.widgetContentClass ]; - if (isRigid) { - classes.push('fc-rigid'); - } + // Renders a set of rectangles over the given segments of time. + // MUST RETURN a subset of segs, the segs that were actually rendered. + // Responsible for populating this.elsByFill. TODO: better API for expressing this requirement + renderFill: function(type, segs) { + // subclasses must implement + }, - return '' + - '
' + - '
' + - '' + - this.renderBgTrHtml(row) + - '
' + - '
' + - '
' + - '' + - (this.numbersVisible ? - '' + - this.renderNumberTrHtml(row) + - '' : - '' - ) + - '
' + - '
' + - '
'; - }, + // Unrenders a specific type of fill that is currently rendered on the grid + unrenderFill: function(type) { + var el = this.elsByFill[type]; - /* Grid Number Rendering - ------------------------------------------------------------------------------------------------------------------*/ + if (el) { + el.remove(); + delete this.elsByFill[type]; + } + }, - renderNumberTrHtml: function(row) { - return '' + - '' + - (this.isRTL ? '' : this.renderNumberIntroHtml(row)) + - this.renderNumberCellsHtml(row) + - (this.isRTL ? this.renderNumberIntroHtml(row) : '') + - ''; - }, + // Renders and assigns an `el` property for each fill segment. Generic enough to work with different types. + // Only returns segments that successfully rendered. + // To be harnessed by renderFill (implemented by subclasses). + // Analagous to renderFgSegEls. + renderFillSegEls: function(type, segs) { + var _this = this; + var segElMethod = this[type + 'SegEl']; + var html = ''; + var renderedSegs = []; + var i; + if (segs.length) { - renderNumberIntroHtml: function(row) { - return this.renderIntroHtml(); - }, + // build a large concatenation of segment HTML + for (i = 0; i < segs.length; i++) { + html += this.fillSegHtml(type, segs[i]); + } + // Grab individual elements from the combined HTML string. Use each as the default rendering. + // Then, compute the 'el' for each segment. + $(html).each(function(i, node) { + var seg = segs[i]; + var el = $(node); - renderNumberCellsHtml: function(row) { - var htmls = []; - var col, date; + // allow custom filter methods per-type + if (segElMethod) { + el = segElMethod.call(_this, seg, el); + } - for (col = 0; col < this.colCnt; col++) { - date = this.getCellDate(row, col); - htmls.push(this.renderNumberCellHtml(date)); - } + if (el) { // custom filters did not cancel the render + el = $(el); // allow custom filter to return raw DOM node - return htmls.join(''); - }, + // correct element type? (would be bad if a non-TD were inserted into a table for example) + if (el.is(_this.fillSegTag)) { + seg.el = el; + renderedSegs.push(seg); + } + } + }); + } + return renderedSegs; + }, - // Generates the HTML for the s of the "number" row in the DayGrid's content skeleton. - // The number row will only exist if either day numbers or week numbers are turned on. - renderNumberCellHtml: function(date) { - var classes; - if (!this.view.dayNumbersVisible) { // if there are week numbers but not day numbers - return ''; // will create an empty space above events :( - } + fillSegTag: 'div', // subclasses can override - classes = this.getDayClasses(date); - classes.unshift('fc-day-number'); - return '' + - '' + - date.date() + - ''; - }, + // Builds the HTML needed for one fill segment. Generic enought o work with different types. + fillSegHtml: function(type, seg) { + // custom hooks per-type + var classesMethod = this[type + 'SegClasses']; + var cssMethod = this[type + 'SegCss']; - /* Options - ------------------------------------------------------------------------------------------------------------------*/ + var classes = classesMethod ? classesMethod.call(this, seg) : []; + var css = cssToStr(cssMethod ? cssMethod.call(this, seg) : {}); + return '<' + this.fillSegTag + + (classes.length ? ' class="' + classes.join(' ') + '"' : '') + + (css ? ' style="' + css + '"' : '') + + ' />'; + }, - // Computes a default event time formatting string if `timeFormat` is not explicitly defined - computeEventTimeFormat: function() { - return this.view.opt('extraSmallTimeFormat'); // like "6p" or "6:30p" - }, - // Computes a default `displayEventEnd` value if one is not expliclty defined - computeDisplayEventEnd: function() { - return this.colCnt == 1; // we'll likely have space if there's only one day - }, + /* Generic rendering utilities for subclasses + ------------------------------------------------------------------------------------------------------------------*/ - /* Dates - ------------------------------------------------------------------------------------------------------------------*/ + // Computes HTML classNames for a single-day element + getDayClasses: function(date) { + var view = this.view; + var today = view.calendar.getNow(); + var classes = [ 'fc-' + dayIDs[date.day()] ]; + if ( + view.intervalDuration.as('months') == 1 && + date.month() != view.intervalStart.month() + ) { + classes.push('fc-other-month'); + } - rangeUpdated: function() { - this.updateDayTable(); - }, + if (date.isSame(today, 'day')) { + classes.push( + 'fc-today', + view.highlightStateClass + ); + } + else if (date < today) { + classes.push('fc-past'); + } + else { + classes.push('fc-future'); + } + return classes; + } - // Slices up the given span (unzoned start/end with other misc data) into an array of segments - spanToSegs: function(span) { - var segs = this.sliceRangeByRow(span); - var i, seg; + }); - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - if (this.isRTL) { - seg.leftCol = this.daysPerRow - 1 - seg.lastRowDayIndex; - seg.rightCol = this.daysPerRow - 1 - seg.firstRowDayIndex; - } - else { - seg.leftCol = seg.firstRowDayIndex; - seg.rightCol = seg.lastRowDayIndex; - } - } + ;; - return segs; - }, + /* Event-rendering and event-interaction methods for the abstract Grid class + ----------------------------------------------------------------------------------------------------------------------*/ + Grid.mixin({ - /* Hit System - ------------------------------------------------------------------------------------------------------------------*/ + mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing + isDraggingSeg: false, // is a segment being dragged? boolean + isResizingSeg: false, // is a segment being resized? boolean + isDraggingExternal: false, // jqui-dragging an external element? boolean + segs: null, // the *event* segments currently rendered in the grid. TODO: rename to `eventSegs` - prepareHits: function() { - this.colCoordCache.build(); - this.rowCoordCache.build(); - this.rowCoordCache.bottoms[this.rowCnt - 1] += this.bottomCoordPadding; // hack - }, + // Renders the given events onto the grid + renderEvents: function(events) { + var bgEvents = []; + var fgEvents = []; + var i; + for (i = 0; i < events.length; i++) { + (isBgEvent(events[i]) ? bgEvents : fgEvents).push(events[i]); + } - releaseHits: function() { - this.colCoordCache.clear(); - this.rowCoordCache.clear(); - }, + this.segs = [].concat( // record all segs + this.renderBgEvents(bgEvents), + this.renderFgEvents(fgEvents) + ); + }, - queryHit: function(leftOffset, topOffset) { - var col = this.colCoordCache.getHorizontalIndex(leftOffset); - var row = this.rowCoordCache.getVerticalIndex(topOffset); + renderBgEvents: function(events) { + var segs = this.eventsToSegs(events); - if (row != null && col != null) { - return this.getCellHit(row, col); - } - }, + // renderBgSegs might return a subset of segs, segs that were actually rendered + return this.renderBgSegs(segs) || segs; + }, - getHitSpan: function(hit) { - return this.getCellRange(hit.row, hit.col); - }, + renderFgEvents: function(events) { + var segs = this.eventsToSegs(events); + // renderFgSegs might return a subset of segs, segs that were actually rendered + return this.renderFgSegs(segs) || segs; + }, - getHitEl: function(hit) { - return this.getCellEl(hit.row, hit.col); - }, + // Unrenders all events currently rendered on the grid + unrenderEvents: function() { + this.handleSegMouseout(); // trigger an eventMouseout if user's mouse is over an event + this.clearDragListeners(); - /* Cell System - ------------------------------------------------------------------------------------------------------------------*/ - // FYI: the first column is the leftmost column, regardless of date + this.unrenderFgSegs(); + this.unrenderBgSegs(); + this.segs = null; + }, - getCellHit: function(row, col) { - return { - row: row, - col: col, - component: this, // needed unfortunately :( - left: this.colCoordCache.getLeftOffset(col), - right: this.colCoordCache.getRightOffset(col), - top: this.rowCoordCache.getTopOffset(row), - bottom: this.rowCoordCache.getBottomOffset(row) - }; - }, + // Retrieves all rendered segment objects currently rendered on the grid + getEventSegs: function() { + return this.segs || []; + }, - getCellEl: function(row, col) { - return this.cellEls.eq(row * this.colCnt + col); - }, + /* Foreground Segment Rendering + ------------------------------------------------------------------------------------------------------------------*/ - /* Event Drag Visualization - ------------------------------------------------------------------------------------------------------------------*/ - // TODO: move to DayGrid.event, similar to what we did with Grid's drag methods + // Renders foreground event segments onto the grid. May return a subset of segs that were rendered. + renderFgSegs: function(segs) { + // subclasses must implement + }, - // Renders a visual indication of an event or external element being dragged. - // `eventLocation` has zoned start and end (optional) - renderDrag: function(eventLocation, seg) { - // always render a highlight underneath - this.renderHighlight(this.eventToSpan(eventLocation)); + // Unrenders all currently rendered foreground segments + unrenderFgSegs: function() { + // subclasses must implement + }, - // if a segment from the same calendar but another component is being dragged, render a helper event - if (seg && !seg.el.closest(this.el).length) { - return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements - } - }, + // Renders and assigns an `el` property for each foreground event segment. + // Only returns segments that successfully rendered. + // A utility that subclasses may use. + renderFgSegEls: function(segs, disableResizing) { + var view = this.view; + var html = ''; + var renderedSegs = []; + var i; + if (segs.length) { // don't build an empty html string - // Unrenders any visual indication of a hovering event - unrenderDrag: function() { - this.unrenderHighlight(); - this.unrenderHelper(); - }, + // build a large concatenation of event segment HTML + for (i = 0; i < segs.length; i++) { + html += this.fgSegHtml(segs[i], disableResizing); + } + // Grab individual elements from the combined HTML string. Use each as the default rendering. + // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false. + $(html).each(function(i, node) { + var seg = segs[i]; + var el = view.resolveEventEl(seg.event, $(node)); - /* Event Resize Visualization - ------------------------------------------------------------------------------------------------------------------*/ + if (el) { + el.data('fc-seg', seg); // used by handlers + seg.el = el; + renderedSegs.push(seg); + } + }); + } + return renderedSegs; + }, - // Renders a visual indication of an event being resized - renderEventResize: function(eventLocation, seg) { - this.renderHighlight(this.eventToSpan(eventLocation)); - return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements - }, + // Generates the HTML for the default rendering of a foreground event segment. Used by renderFgSegEls() + fgSegHtml: function(seg, disableResizing) { + // subclasses should implement + }, - // Unrenders a visual indication of an event being resized - unrenderEventResize: function() { - this.unrenderHighlight(); - this.unrenderHelper(); - }, + /* Background Segment Rendering + ------------------------------------------------------------------------------------------------------------------*/ - /* Event Helper - ------------------------------------------------------------------------------------------------------------------*/ + // Renders the given background event segments onto the grid. + // Returns a subset of the segs that were actually rendered. + renderBgSegs: function(segs) { + return this.renderFill('bgEvent', segs); + }, - // Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null. - renderHelper: function(event, sourceSeg) { - var helperNodes = []; - var segs = this.eventToSegs(event); - var rowStructs; - segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered - rowStructs = this.renderSegRows(segs); + // Unrenders all the currently rendered background event segments + unrenderBgSegs: function() { + this.unrenderFill('bgEvent'); + }, - // inject each new event skeleton into each associated row - this.rowEls.each(function(row, rowNode) { - var rowEl = $(rowNode); // the .fc-row - var skeletonEl = $('
'); // will be absolutely positioned - var skeletonTop; - // If there is an original segment, match the top position. Otherwise, put it at the row's top level - if (sourceSeg && sourceSeg.row === row) { - skeletonTop = sourceSeg.el.position().top; - } - else { - skeletonTop = rowEl.find('.fc-content-skeleton tbody').position().top; - } + // Renders a background event element, given the default rendering. Called by the fill system. + bgEventSegEl: function(seg, el) { + return this.view.resolveEventEl(seg.event, el); // will filter through eventRender + }, - skeletonEl.css('top', skeletonTop) - .find('table') - .append(rowStructs[row].tbodyEl); - rowEl.append(skeletonEl); - helperNodes.push(skeletonEl[0]); - }); + // Generates an array of classNames to be used for the default rendering of a background event. + // Called by the fill system. + bgEventSegClasses: function(seg) { + var event = seg.event; + var source = event.source || {}; - return ( // must return the elements rendered - this.helperEls = $(helperNodes) // array -> jQuery set - ); - }, + return [ 'fc-bgevent' ].concat( + event.className, + source.className || [] + ); + }, - // Unrenders any visual indication of a mock helper event - unrenderHelper: function() { - if (this.helperEls) { - this.helperEls.remove(); - this.helperEls = null; - } - }, + // Generates a semicolon-separated CSS string to be used for the default rendering of a background event. + // Called by the fill system. + bgEventSegCss: function(seg) { + return { + 'background-color': this.getSegSkinCss(seg)['background-color'] + }; + }, - /* Fill System (highlight, background events, business hours) - ------------------------------------------------------------------------------------------------------------------*/ + // Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system. + businessHoursSegClasses: function(seg) { + return [ 'fc-nonbusiness', 'fc-bgevent' ]; + }, - fillSegTag: 'td', // override the default tag name + /* Handlers + ------------------------------------------------------------------------------------------------------------------*/ - // Renders a set of rectangles over the given segments of days. - // Only returns segments that successfully rendered. - renderFill: function(type, segs, className) { - var nodes = []; - var i, seg; - var skeletonEl; + // Attaches event-element-related handlers to the container element and leverage bubbling + bindSegHandlers: function() { + if (this.view.calendar.isTouch) { + this.bindSegHandler('touchstart', this.handleSegTouchStart); + } + else { + this.bindSegHandler('mouseenter', this.handleSegMouseover); + this.bindSegHandler('mouseleave', this.handleSegMouseout); + this.bindSegHandler('mousedown', this.handleSegMousedown); + } - segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs + this.bindSegHandler('click', this.handleSegClick); + }, - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - skeletonEl = this.renderFillRow(type, seg, className); - this.rowEls.eq(seg.row).append(skeletonEl); - nodes.push(skeletonEl[0]); - } - this.elsByFill[type] = $(nodes); + // Executes a handler for any a user-interaction on a segment. + // Handler gets called with (seg, ev), and with the `this` context of the Grid + bindSegHandler: function(name, handler) { + var _this = this; + + this.el.on(name, '.fc-event-container > *', function(ev) { + var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents + + // only call the handlers if there is not a drag/resize in progress + if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) { + return handler.call(_this, seg, ev); // context will be the Grid + } + }); + }, - return segs; - }, + handleSegClick: function(seg, ev) { + return this.view.trigger('eventClick', seg.el[0], seg.event, ev); // can return `false` to cancel + }, - // Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered. - renderFillRow: function(type, seg, className) { - var colCnt = this.colCnt; - var startCol = seg.leftCol; - var endCol = seg.rightCol + 1; - var skeletonEl; - var trEl; - className = className || type.toLowerCase(); + // Updates internal state and triggers handlers for when an event element is moused over + handleSegMouseover: function(seg, ev) { + if (!this.mousedOverSeg) { + this.mousedOverSeg = seg; + this.view.trigger('eventMouseover', seg.el[0], seg.event, ev); + } + }, + + + // Updates internal state and triggers handlers for when an event element is moused out. + // Can be given no arguments, in which case it will mouseout the segment that was previously moused over. + handleSegMouseout: function(seg, ev) { + ev = ev || {}; // if given no args, make a mock mouse event + + if (this.mousedOverSeg) { + seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment + this.mousedOverSeg = null; + this.view.trigger('eventMouseout', seg.el[0], seg.event, ev); + } + }, + + + handleSegTouchStart: function(seg, ev) { + var view = this.view; + var event = seg.event; + var isSelected = view.isEventSelected(event); + var isDraggable = view.isEventDraggable(event); + var isResizable = view.isEventResizable(event); + var isResizing = false; + var dragListener; + + if (isSelected && isResizable) { + // only allow resizing of the event is selected + isResizing = this.startSegResize(seg, ev); + } + + if (!isResizing && (isDraggable || isResizable)) { // allowed to be selected? + this.clearDragListeners(); + + dragListener = isDraggable ? + this.buildSegDragListener(seg) : + new DragListener(); // seg isn't draggable, but let's use a generic DragListener + // simply for the delay, so it can be selected. + + dragListener._dragStart = function() { // TODO: better way of binding + // if not previously selected, will fire after a delay. then, select the event + if (!isSelected) { + view.selectEvent(event); + } + }; + + dragListener.startInteraction(ev, { + delay: isSelected ? 0 : this.view.opt('longPressDelay') // do delay if not already selected + }); + } + }, + + + handleSegMousedown: function(seg, ev) { + var isResizing = this.startSegResize(seg, ev, { distance: 5 }); + + if (!isResizing && this.view.isEventDraggable(seg.event)) { + this.clearDragListeners(); + this.buildSegDragListener(seg) + .startInteraction(ev, { + distance: 5 + }); + } + }, + + + // returns boolean whether resizing actually started or not. + // assumes the seg allows resizing. + // `dragOptions` are optional. + startSegResize: function(seg, ev, dragOptions) { + if ($(ev.target).is('.fc-resizer')) { + this.clearDragListeners(); + this.buildSegResizeListener(seg, $(ev.target).is('.fc-start-resizer')) + .startInteraction(ev, dragOptions); + return true; + } + return false; + }, + + + + /* Event Dragging + ------------------------------------------------------------------------------------------------------------------*/ + + + // Builds a listener that will track user-dragging on an event segment. + // Generic enough to work with any type of Grid. + buildSegDragListener: function(seg) { + var _this = this; + var view = this.view; + var calendar = view.calendar; + var el = seg.el; + var event = seg.event; + var isDragging; + var mouseFollower; // A clone of the original element that will move with the mouse + var dropLocation; // zoned event date properties + + // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents + // of the view. + var dragListener = this.segDragListener = new HitDragListener(view, { + scroll: view.opt('dragScroll'), + subjectEl: el, + subjectCenter: true, + interactionStart: function(ev) { + isDragging = false; + mouseFollower = new MouseFollower(seg.el, { + additionalClass: 'fc-dragging', + parentEl: view.el, + opacity: dragListener.isTouch ? null : view.opt('dragOpacity'), + revertDuration: view.opt('dragRevertDuration'), + zIndex: 2 // one above the .fc-view + }); + mouseFollower.hide(); // don't show until we know this is a real drag + mouseFollower.start(ev); + }, + dragStart: function(ev) { + isDragging = true; + _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported + _this.segDragStart(seg, ev); + view.hideEvent(event); // hide all event segments. our mouseFollower will take over + }, + hitOver: function(hit, isOrig, origHit) { + var dragHelperEls; + + // starting hit could be forced (DayGrid.limit) + if (seg.hit) { + origHit = seg.hit; + } + + // since we are querying the parent view, might not belong to this grid + dropLocation = _this.computeEventDrop( + origHit.component.getHitSpan(origHit), + hit.component.getHitSpan(hit), + event + ); + + if (dropLocation && !calendar.isEventSpanAllowed(_this.eventToSpan(dropLocation), event)) { + disableCursor(); + dropLocation = null; + } + + // if a valid drop location, have the subclass render a visual indication + if (dropLocation && (dragHelperEls = view.renderDrag(dropLocation, seg))) { + + dragHelperEls.addClass('fc-dragging'); + if (!dragListener.isTouch) { + _this.applyDragOpacity(dragHelperEls); + } + + mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own + } + else { + mouseFollower.show(); // otherwise, have the helper follow the mouse (no snapping) + } + + if (isOrig) { + dropLocation = null; // needs to have moved hits to be a valid drop + } + }, + hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits + view.unrenderDrag(); // unrender whatever was done in renderDrag + mouseFollower.show(); // show in case we are moving out of all hits + dropLocation = null; + }, + hitDone: function() { // Called after a hitOut OR before a dragEnd + enableCursor(); + }, + interactionEnd: function(ev) { + // do revert animation if hasn't changed. calls a callback when finished (whether animation or not) + mouseFollower.stop(!dropLocation, function() { + if (isDragging) { + view.unrenderDrag(); + view.showEvent(event); + _this.segDragStop(seg, ev); + } + if (dropLocation) { + view.reportEventDrop(event, dropLocation, this.largeUnit, el, ev); + } + }); + _this.segDragListener = null; + } + }); + + return dragListener; + }, + + + // Called before event segment dragging starts + segDragStart: function(seg, ev) { + this.isDraggingSeg = true; + this.view.trigger('eventDragStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy + }, + + + // Called after event segment dragging stops + segDragStop: function(seg, ev) { + this.isDraggingSeg = false; + this.view.trigger('eventDragStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy + }, + + + // Given the spans an event drag began, and the span event was dropped, calculates the new zoned start/end/allDay + // values for the event. Subclasses may override and set additional properties to be used by renderDrag. + // A falsy returned value indicates an invalid drop. + // DOES NOT consider overlap/constraint. + computeEventDrop: function(startSpan, endSpan, event) { + var calendar = this.view.calendar; + var dragStart = startSpan.start; + var dragEnd = endSpan.start; + var delta; + var dropLocation; // zoned event date properties + + if (dragStart.hasTime() === dragEnd.hasTime()) { + delta = this.diffDates(dragEnd, dragStart); + + // if an all-day event was in a timed area and it was dragged to a different time, + // guarantee an end and adjust start/end to have times + if (event.allDay && durationHasTime(delta)) { + dropLocation = { + start: event.start.clone(), + end: calendar.getEventEnd(event), // will be an ambig day + allDay: false // for normalizeEventTimes + }; + calendar.normalizeEventTimes(dropLocation); + } + // othewise, work off existing values + else { + dropLocation = { + start: event.start.clone(), + end: event.end ? event.end.clone() : null, + allDay: event.allDay // keep it the same + }; + } + + dropLocation.start.add(delta); + if (dropLocation.end) { + dropLocation.end.add(delta); + } + } + else { + // if switching from day <-> timed, start should be reset to the dropped date, and the end cleared + dropLocation = { + start: dragEnd.clone(), + end: null, // end should be cleared + allDay: !dragEnd.hasTime() + }; + } + + return dropLocation; + }, + + + // Utility for apply dragOpacity to a jQuery set + applyDragOpacity: function(els) { + var opacity = this.view.opt('dragOpacity'); + + if (opacity != null) { + els.each(function(i, node) { + // Don't use jQuery (will set an IE filter), do it the old fashioned way. + // In IE8, a helper element will disappears if there's a filter. + node.style.opacity = opacity; + }); + } + }, + + + /* External Element Dragging + ------------------------------------------------------------------------------------------------------------------*/ + + + // Called when a jQuery UI drag is initiated anywhere in the DOM + externalDragStart: function(ev, ui) { + var view = this.view; + var el; + var accept; + + if (view.opt('droppable')) { // only listen if this setting is on + el = $((ui ? ui.item : null) || ev.target); + + // Test that the dragged element passes the dropAccept selector or filter function. + // FYI, the default is "*" (matches all) + accept = view.opt('dropAccept'); + if ($.isFunction(accept) ? accept.call(el[0], el) : el.is(accept)) { + if (!this.isDraggingExternal) { // prevent double-listening if fired twice + this.listenToExternalDrag(el, ev, ui); + } + } + } + }, + + + // Called when a jQuery UI drag starts and it needs to be monitored for dropping + listenToExternalDrag: function(el, ev, ui) { + var _this = this; + var calendar = this.view.calendar; + var meta = getDraggedElMeta(el); // extra data about event drop, including possible event to create + var dropLocation; // a null value signals an unsuccessful drag + + // listener that tracks mouse movement over date-associated pixel regions + var dragListener = _this.externalDragListener = new HitDragListener(this, { + interactionStart: function() { + _this.isDraggingExternal = true; + }, + hitOver: function(hit) { + dropLocation = _this.computeExternalDrop( + hit.component.getHitSpan(hit), // since we are querying the parent view, might not belong to this grid + meta + ); + + if ( // invalid hit? + dropLocation && + !calendar.isExternalSpanAllowed(_this.eventToSpan(dropLocation), dropLocation, meta.eventProps) + ) { + disableCursor(); + dropLocation = null; + } + + if (dropLocation) { + _this.renderDrag(dropLocation); // called without a seg parameter + } + }, + hitOut: function() { + dropLocation = null; // signal unsuccessful + }, + hitDone: function() { // Called after a hitOut OR before a dragEnd + enableCursor(); + _this.unrenderDrag(); + }, + interactionEnd: function(ev) { + if (dropLocation) { // element was dropped on a valid hit + _this.view.reportExternalDrop(meta, dropLocation, el, ev, ui); + } + _this.isDraggingExternal = false; + _this.externalDragListener = null; + } + }); + + dragListener.startDrag(ev); // start listening immediately + }, + + + // Given a hit to be dropped upon, and misc data associated with the jqui drag (guaranteed to be a plain object), + // returns the zoned start/end dates for the event that would result from the hypothetical drop. end might be null. + // Returning a null value signals an invalid drop hit. + // DOES NOT consider overlap/constraint. + computeExternalDrop: function(span, meta) { + var calendar = this.view.calendar; + var dropLocation = { + start: calendar.applyTimezone(span.start), // simulate a zoned event start date + end: null + }; + + // if dropped on an all-day span, and element's metadata specified a time, set it + if (meta.startTime && !dropLocation.start.hasTime()) { + dropLocation.start.time(meta.startTime); + } + + if (meta.duration) { + dropLocation.end = dropLocation.start.clone().add(meta.duration); + } + + return dropLocation; + }, + + + + /* Drag Rendering (for both events and an external elements) + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of an event or external element being dragged. + // `dropLocation` contains hypothetical start/end/allDay values the event would have if dropped. end can be null. + // `seg` is the internal segment object that is being dragged. If dragging an external element, `seg` is null. + // A truthy returned value indicates this method has rendered a helper element. + // Must return elements used for any mock events. + renderDrag: function(dropLocation, seg) { + // subclasses must implement + }, + + + // Unrenders a visual indication of an event or external element being dragged + unrenderDrag: function() { + // subclasses must implement + }, + + + /* Resizing + ------------------------------------------------------------------------------------------------------------------*/ + + + // Creates a listener that tracks the user as they resize an event segment. + // Generic enough to work with any type of Grid. + buildSegResizeListener: function(seg, isStart) { + var _this = this; + var view = this.view; + var calendar = view.calendar; + var el = seg.el; + var event = seg.event; + var eventEnd = calendar.getEventEnd(event); + var isDragging; + var resizeLocation; // zoned event date properties. falsy if invalid resize + + // Tracks mouse movement over the *grid's* coordinate map + var dragListener = this.segResizeListener = new HitDragListener(this, { + scroll: view.opt('dragScroll'), + subjectEl: el, + interactionStart: function() { + isDragging = false; + }, + dragStart: function(ev) { + isDragging = true; + _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported + _this.segResizeStart(seg, ev); + }, + hitOver: function(hit, isOrig, origHit) { + var origHitSpan = _this.getHitSpan(origHit); + var hitSpan = _this.getHitSpan(hit); + + resizeLocation = isStart ? + _this.computeEventStartResize(origHitSpan, hitSpan, event) : + _this.computeEventEndResize(origHitSpan, hitSpan, event); + + if (resizeLocation) { + if (!calendar.isEventSpanAllowed(_this.eventToSpan(resizeLocation), event)) { + disableCursor(); + resizeLocation = null; + } + // no change? (TODO: how does this work with timezones?) + else if (resizeLocation.start.isSame(event.start) && resizeLocation.end.isSame(eventEnd)) { + resizeLocation = null; + } + } + + if (resizeLocation) { + view.hideEvent(event); + _this.renderEventResize(resizeLocation, seg); + } + }, + hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits + resizeLocation = null; + }, + hitDone: function() { // resets the rendering to show the original event + _this.unrenderEventResize(); + view.showEvent(event); + enableCursor(); + }, + interactionEnd: function(ev) { + if (isDragging) { + _this.segResizeStop(seg, ev); + } + if (resizeLocation) { // valid date to resize to? + view.reportEventResize(event, resizeLocation, this.largeUnit, el, ev); + } + _this.segResizeListener = null; + } + }); + + return dragListener; + }, + + + // Called before event segment resizing starts + segResizeStart: function(seg, ev) { + this.isResizingSeg = true; + this.view.trigger('eventResizeStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy + }, + + + // Called after event segment resizing stops + segResizeStop: function(seg, ev) { + this.isResizingSeg = false; + this.view.trigger('eventResizeStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy + }, + + + // Returns new date-information for an event segment being resized from its start + computeEventStartResize: function(startSpan, endSpan, event) { + return this.computeEventResize('start', startSpan, endSpan, event); + }, + + + // Returns new date-information for an event segment being resized from its end + computeEventEndResize: function(startSpan, endSpan, event) { + return this.computeEventResize('end', startSpan, endSpan, event); + }, + + + // Returns new zoned date information for an event segment being resized from its start OR end + // `type` is either 'start' or 'end'. + // DOES NOT consider overlap/constraint. + computeEventResize: function(type, startSpan, endSpan, event) { + var calendar = this.view.calendar; + var delta = this.diffDates(endSpan[type], startSpan[type]); + var resizeLocation; // zoned event date properties + var defaultDuration; + + // build original values to work from, guaranteeing a start and end + resizeLocation = { + start: event.start.clone(), + end: calendar.getEventEnd(event), + allDay: event.allDay + }; + + // if an all-day event was in a timed area and was resized to a time, adjust start/end to have times + if (resizeLocation.allDay && durationHasTime(delta)) { + resizeLocation.allDay = false; + calendar.normalizeEventTimes(resizeLocation); + } + + resizeLocation[type].add(delta); // apply delta to start or end + + // if the event was compressed too small, find a new reasonable duration for it + if (!resizeLocation.start.isBefore(resizeLocation.end)) { + + defaultDuration = + this.minResizeDuration || // TODO: hack + (event.allDay ? + calendar.defaultAllDayEventDuration : + calendar.defaultTimedEventDuration); + + if (type == 'start') { // resizing the start? + resizeLocation.start = resizeLocation.end.clone().subtract(defaultDuration); + } + else { // resizing the end? + resizeLocation.end = resizeLocation.start.clone().add(defaultDuration); + } + } + + return resizeLocation; + }, + + + // Renders a visual indication of an event being resized. + // `range` has the updated dates of the event. `seg` is the original segment object involved in the drag. + // Must return elements used for any mock events. + renderEventResize: function(range, seg) { + // subclasses must implement + }, + + + // Unrenders a visual indication of an event being resized. + unrenderEventResize: function() { + // subclasses must implement + }, + + + /* Rendering Utils + ------------------------------------------------------------------------------------------------------------------*/ + + + // Compute the text that should be displayed on an event's element. + // `range` can be the Event object itself, or something range-like, with at least a `start`. + // If event times are disabled, or the event has no time, will return a blank string. + // If not specified, formatStr will default to the eventTimeFormat setting, + // and displayEnd will default to the displayEventEnd setting. + getEventTimeText: function(range, formatStr, displayEnd) { + + if (formatStr == null) { + formatStr = this.eventTimeFormat; + } + + if (displayEnd == null) { + displayEnd = this.displayEventEnd; + } + + if (this.displayEventTime && range.start.hasTime()) { + if (displayEnd && range.end) { + return this.view.formatRange(range, formatStr); + } + else { + return range.start.format(formatStr); + } + } + + return ''; + }, + + + // Generic utility for generating the HTML classNames for an event segment's element + getSegClasses: function(seg, isDraggable, isResizable) { + var view = this.view; + var event = seg.event; + var classes = [ + 'fc-event', + seg.isStart ? 'fc-start' : 'fc-not-start', + seg.isEnd ? 'fc-end' : 'fc-not-end' + ].concat( + event.className, + event.source ? event.source.className : [] + ); + + if (isDraggable) { + classes.push('fc-draggable'); + } + if (isResizable) { + classes.push('fc-resizable'); + } + + // event is currently selected? attach a className. + if (view.isEventSelected(event)) { + classes.push('fc-selected'); + } + + return classes; + }, + + + // Utility for generating event skin-related CSS properties + getSegSkinCss: function(seg) { + var event = seg.event; + var view = this.view; + var source = event.source || {}; + var eventColor = event.color; + var sourceColor = source.color; + var optionColor = view.opt('eventColor'); + + return { + 'background-color': + event.backgroundColor || + eventColor || + source.backgroundColor || + sourceColor || + view.opt('eventBackgroundColor') || + optionColor, + 'border-color': + event.borderColor || + eventColor || + source.borderColor || + sourceColor || + view.opt('eventBorderColor') || + optionColor, + color: + event.textColor || + source.textColor || + view.opt('eventTextColor') + }; + }, + + + /* Converting events -> eventRange -> eventSpan -> eventSegs + ------------------------------------------------------------------------------------------------------------------*/ + + + // Generates an array of segments for the given single event + // Can accept an event "location" as well (which only has start/end and no allDay) + eventToSegs: function(event) { + return this.eventsToSegs([ event ]); + }, + + + eventToSpan: function(event) { + return this.eventToSpans(event)[0]; + }, + + + // Generates spans (always unzoned) for the given event. + // Does not do any inverting for inverse-background events. + // Can accept an event "location" as well (which only has start/end and no allDay) + eventToSpans: function(event) { + var range = this.eventToRange(event); + return this.eventRangeToSpans(range, event); + }, + + + + // Converts an array of event objects into an array of event segment objects. + // A custom `segSliceFunc` may be given for arbitrarily slicing up events. + // Doesn't guarantee an order for the resulting array. + eventsToSegs: function(allEvents, segSliceFunc) { + var _this = this; + var eventsById = groupEventsById(allEvents); + var segs = []; + + $.each(eventsById, function(id, events) { + var ranges = []; + var i; + + for (i = 0; i < events.length; i++) { + ranges.push(_this.eventToRange(events[i])); + } + + // inverse-background events (utilize only the first event in calculations) + if (isInverseBgEvent(events[0])) { + ranges = _this.invertRanges(ranges); + + for (i = 0; i < ranges.length; i++) { + segs.push.apply(segs, // append to + _this.eventRangeToSegs(ranges[i], events[0], segSliceFunc)); + } + } + // normal event ranges + else { + for (i = 0; i < ranges.length; i++) { + segs.push.apply(segs, // append to + _this.eventRangeToSegs(ranges[i], events[i], segSliceFunc)); + } + } + }); + + return segs; + }, + + + // Generates the unzoned start/end dates an event appears to occupy + // Can accept an event "location" as well (which only has start/end and no allDay) + eventToRange: function(event) { + return { + start: event.start.clone().stripZone(), + end: ( + event.end ? + event.end.clone() : + // derive the end from the start and allDay. compute allDay if necessary + this.view.calendar.getDefaultEventEnd( + event.allDay != null ? + event.allDay : + !event.start.hasTime(), + event.start + ) + ).stripZone() + }; + }, + + + // Given an event's range (unzoned start/end), and the event itself, + // slice into segments (using the segSliceFunc function if specified) + eventRangeToSegs: function(range, event, segSliceFunc) { + var spans = this.eventRangeToSpans(range, event); + var segs = []; + var i; + + for (i = 0; i < spans.length; i++) { + segs.push.apply(segs, // append to + this.eventSpanToSegs(spans[i], event, segSliceFunc)); + } + + return segs; + }, - skeletonEl = $( - '
' + - '
' + - '
' - ); - trEl = skeletonEl.find('tr'); - if (startCol > 0) { - trEl.append(''); - } + // Given an event's unzoned date range, return an array of "span" objects. + // Subclasses can override. + eventRangeToSpans: function(range, event) { + return [ $.extend({}, range) ]; // copy into a single-item array + }, + + + // Given an event's span (unzoned start/end and other misc data), and the event itself, + // slices into segments and attaches event-derived properties to them. + eventSpanToSegs: function(span, event, segSliceFunc) { + var segs = segSliceFunc ? segSliceFunc(span) : this.spanToSegs(span); + var i, seg; + + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + seg.event = event; + seg.eventStartMS = +span.start; // TODO: not the best name after making spans unzoned + seg.eventDurationMS = span.end - span.start; + } + + return segs; + }, - trEl.append( - seg.el.attr('colspan', endCol - startCol) - ); - if (endCol < colCnt) { - trEl.append(''); - } + // Produces a new array of range objects that will cover all the time NOT covered by the given ranges. + // SIDE EFFECT: will mutate the given array and will use its date references. + invertRanges: function(ranges) { + var view = this.view; + var viewStart = view.start.clone(); // need a copy + var viewEnd = view.end.clone(); // need a copy + var inverseRanges = []; + var start = viewStart; // the end of the previous range. the start of the new range + var i, range; + + // ranges need to be in order. required for our date-walking algorithm + ranges.sort(compareRanges); + + for (i = 0; i < ranges.length; i++) { + range = ranges[i]; + + // add the span of time before the event (if there is any) + if (range.start > start) { // compare millisecond time (skip any ambig logic) + inverseRanges.push({ + start: start, + end: range.start + }); + } + + start = range.end; + } + + // add the span of time after the last event (if there is any) + if (start < viewEnd) { // compare millisecond time (skip any ambig logic) + inverseRanges.push({ + start: start, + end: viewEnd + }); + } + + return inverseRanges; + }, + + + sortEventSegs: function(segs) { + segs.sort(proxy(this, 'compareEventSegs')); + }, + + + // A cmp function for determining which segments should take visual priority + compareEventSegs: function(seg1, seg2) { + return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first + seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first + seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1) + compareByFieldSpecs(seg1.event, seg2.event, this.view.eventOrderSpecs); + } - this.bookendCells(trEl); + }); - return skeletonEl; - } -}); + /* Utilities + ----------------------------------------------------------------------------------------------------------------------*/ -;; -/* Event-rendering methods for the DayGrid class -----------------------------------------------------------------------------------------------------------------------*/ + function isBgEvent(event) { // returns true if background OR inverse-background + var rendering = getEventRendering(event); + return rendering === 'background' || rendering === 'inverse-background'; + } + FC.isBgEvent = isBgEvent; // export -DayGrid.mixin({ - rowStructs: null, // an array of objects, each holding information about a row's foreground event-rendering + function isInverseBgEvent(event) { + return getEventRendering(event) === 'inverse-background'; + } - // Unrenders all events currently rendered on the grid - unrenderEvents: function() { - this.removeSegPopover(); // removes the "more.." events popover - Grid.prototype.unrenderEvents.apply(this, arguments); // calls the super-method - }, + function getEventRendering(event) { + return firstDefined((event.source || {}).rendering, event.rendering); + } + + + function groupEventsById(events) { + var eventsById = {}; + var i, event; + + for (i = 0; i < events.length; i++) { + event = events[i]; + (eventsById[event._id] || (eventsById[event._id] = [])).push(event); + } + + return eventsById; + } + + + // A cmp function for determining which non-inverted "ranges" (see above) happen earlier + function compareRanges(range1, range2) { + return range1.start - range2.start; // earlier ranges go first + } + + + /* External-Dragging-Element Data + ----------------------------------------------------------------------------------------------------------------------*/ + + // Require all HTML5 data-* attributes used by FullCalendar to have this prefix. + // A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event. + FC.dataAttrPrefix = ''; + + // Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure + // to be used for Event Object creation. + // A defined `.eventProps`, even when empty, indicates that an event should be created. + function getDraggedElMeta(el) { + var prefix = FC.dataAttrPrefix; + var eventProps; // properties for creating the event, not related to date/time + var startTime; // a Duration + var duration; + var stick; + + if (prefix) { prefix += '-'; } + eventProps = el.data(prefix + 'event') || null; + + if (eventProps) { + if (typeof eventProps === 'object') { + eventProps = $.extend({}, eventProps); // make a copy + } + else { // something like 1 or true. still signal event creation + eventProps = {}; + } + + // pluck special-cased date/time properties + startTime = eventProps.start; + if (startTime == null) { startTime = eventProps.time; } // accept 'time' as well + duration = eventProps.duration; + stick = eventProps.stick; + delete eventProps.start; + delete eventProps.time; + delete eventProps.duration; + delete eventProps.stick; + } + + // fallback to standalone attribute values for each of the date/time properties + if (startTime == null) { startTime = el.data(prefix + 'start'); } + if (startTime == null) { startTime = el.data(prefix + 'time'); } // accept 'time' as well + if (duration == null) { duration = el.data(prefix + 'duration'); } + if (stick == null) { stick = el.data(prefix + 'stick'); } + + // massage into correct data types + startTime = startTime != null ? moment.duration(startTime) : null; + duration = duration != null ? moment.duration(duration) : null; + stick = Boolean(stick); + + return { eventProps: eventProps, startTime: startTime, duration: duration, stick: stick }; + } + + + ;; + + /* + A set of rendering and date-related methods for a visual component comprised of one or more rows of day columns. + Prerequisite: the object being mixed into needs to be a *Grid* + */ + var DayTableMixin = FC.DayTableMixin = { + + breakOnWeeks: false, // should create a new row for each week? + dayDates: null, // whole-day dates for each column. left to right + dayIndices: null, // for each day from start, the offset + daysPerRow: null, + rowCnt: null, + colCnt: null, + colHeadFormat: null, + + + // Populates internal variables used for date calculation and rendering + updateDayTable: function() { + var view = this.view; + var date = this.start.clone(); + var dayIndex = -1; + var dayIndices = []; + var dayDates = []; + var daysPerRow; + var firstDay; + var rowCnt; + + while (date.isBefore(this.end)) { // loop each day from start to end + if (view.isHiddenDay(date)) { + dayIndices.push(dayIndex + 0.5); // mark that it's between indices + } + else { + dayIndex++; + dayIndices.push(dayIndex); + dayDates.push(date.clone()); + } + date.add(1, 'days'); + } + + if (this.breakOnWeeks) { + // count columns until the day-of-week repeats + firstDay = dayDates[0].day(); + for (daysPerRow = 1; daysPerRow < dayDates.length; daysPerRow++) { + if (dayDates[daysPerRow].day() == firstDay) { + break; + } + } + rowCnt = Math.ceil(dayDates.length / daysPerRow); + } + else { + rowCnt = 1; + daysPerRow = dayDates.length; + } + + this.dayDates = dayDates; + this.dayIndices = dayIndices; + this.daysPerRow = daysPerRow; + this.rowCnt = rowCnt; + + this.updateDayTableCols(); + }, + + + // Computes and assigned the colCnt property and updates any options that may be computed from it + updateDayTableCols: function() { + this.colCnt = this.computeColCnt(); + this.colHeadFormat = this.view.opt('columnFormat') || this.computeColHeadFormat(); + }, + + + // Determines how many columns there should be in the table + computeColCnt: function() { + return this.daysPerRow; + }, + + + // Computes the ambiguously-timed moment for the given cell + getCellDate: function(row, col) { + return this.dayDates[ + this.getCellDayIndex(row, col) + ].clone(); + }, + + + // Computes the ambiguously-timed date range for the given cell + getCellRange: function(row, col) { + var start = this.getCellDate(row, col); + var end = start.clone().add(1, 'days'); + + return { start: start, end: end }; + }, + + + // Returns the number of day cells, chronologically, from the first of the grid (0-based) + getCellDayIndex: function(row, col) { + return row * this.daysPerRow + this.getColDayIndex(col); + }, + + + // Returns the numner of day cells, chronologically, from the first cell in *any given row* + getColDayIndex: function(col) { + if (this.isRTL) { + return this.colCnt - 1 - col; + } + else { + return col; + } + }, + + + // Given a date, returns its chronolocial cell-index from the first cell of the grid. + // If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets. + // If before the first offset, returns a negative number. + // If after the last offset, returns an offset past the last cell offset. + // Only works for *start* dates of cells. Will not work for exclusive end dates for cells. + getDateDayIndex: function(date) { + var dayIndices = this.dayIndices; + var dayOffset = date.diff(this.start, 'days'); + + if (dayOffset < 0) { + return dayIndices[0] - 1; + } + else if (dayOffset >= dayIndices.length) { + return dayIndices[dayIndices.length - 1] + 1; + } + else { + return dayIndices[dayOffset]; + } + }, + + + /* Options + ------------------------------------------------------------------------------------------------------------------*/ + + + // Computes a default column header formatting string if `colFormat` is not explicitly defined + computeColHeadFormat: function() { + // if more than one week row, or if there are a lot of columns with not much space, + // put just the day numbers will be in each cell + if (this.rowCnt > 1 || this.colCnt > 10) { + return 'ddd'; // "Sat" + } + // multiple days, so full single date string WON'T be in title text + else if (this.colCnt > 1) { + return this.view.opt('dayOfMonthFormat'); // "Sat 12/10" + } + // single day, so full single date string will probably be in title text + else { + return 'dddd'; // "Saturday" + } + }, + + + /* Slicing + ------------------------------------------------------------------------------------------------------------------*/ + + + // Slices up a date range into a segment for every week-row it intersects with + sliceRangeByRow: function(range) { + var daysPerRow = this.daysPerRow; + var normalRange = this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold + var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index + var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index + var segs = []; + var row; + var rowFirst, rowLast; // inclusive day-index range for current row + var segFirst, segLast; // inclusive day-index range for segment + + for (row = 0; row < this.rowCnt; row++) { + rowFirst = row * daysPerRow; + rowLast = rowFirst + daysPerRow - 1; + + // intersect segment's offset range with the row's + segFirst = Math.max(rangeFirst, rowFirst); + segLast = Math.min(rangeLast, rowLast); + + // deal with in-between indices + segFirst = Math.ceil(segFirst); // in-between starts round to next cell + segLast = Math.floor(segLast); // in-between ends round to prev cell + + if (segFirst <= segLast) { // was there any intersection with the current row? + segs.push({ + row: row, + + // normalize to start of row + firstRowDayIndex: segFirst - rowFirst, + lastRowDayIndex: segLast - rowFirst, + + // must be matching integers to be the segment's start/end + isStart: segFirst === rangeFirst, + isEnd: segLast === rangeLast + }); + } + } + + return segs; + }, + + + // Slices up a date range into a segment for every day-cell it intersects with. + // TODO: make more DRY with sliceRangeByRow somehow. + sliceRangeByDay: function(range) { + var daysPerRow = this.daysPerRow; + var normalRange = this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold + var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index + var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index + var segs = []; + var row; + var rowFirst, rowLast; // inclusive day-index range for current row + var i; + var segFirst, segLast; // inclusive day-index range for segment + + for (row = 0; row < this.rowCnt; row++) { + rowFirst = row * daysPerRow; + rowLast = rowFirst + daysPerRow - 1; + + for (i = rowFirst; i <= rowLast; i++) { + + // intersect segment's offset range with the row's + segFirst = Math.max(rangeFirst, i); + segLast = Math.min(rangeLast, i); + + // deal with in-between indices + segFirst = Math.ceil(segFirst); // in-between starts round to next cell + segLast = Math.floor(segLast); // in-between ends round to prev cell + + if (segFirst <= segLast) { // was there any intersection with the current row? + segs.push({ + row: row, + + // normalize to start of row + firstRowDayIndex: segFirst - rowFirst, + lastRowDayIndex: segLast - rowFirst, + + // must be matching integers to be the segment's start/end + isStart: segFirst === rangeFirst, + isEnd: segLast === rangeLast + }); + } + } + } + + return segs; + }, + + + /* Header Rendering + ------------------------------------------------------------------------------------------------------------------*/ + + renderHeadHtml: function() { + var view = this.view; + + return '' + + '
' + + '' + + '' + + this.renderHeadTrHtml() + + '' + + '
' + + '
'; + }, - // Retrieves all rendered segment objects currently rendered on the grid - getEventSegs: function() { - return Grid.prototype.getEventSegs.call(this) // get the segments from the super-method - .concat(this.popoverSegs || []); // append the segments from the "more..." popover - }, + renderHeadIntroHtml: function() { + return this.renderIntroHtml(); // fall back to generic + }, - // Renders the given background event segments onto the grid - renderBgSegs: function(segs) { - // don't render timed background events - var allDaySegs = $.grep(segs, function(seg) { - return seg.event.allDay; - }); + renderHeadTrHtml: function() { + return '' + + '' + + (this.isRTL ? '' : this.renderHeadIntroHtml()) + + this.renderHeadDateCellsHtml() + + (this.isRTL ? this.renderHeadIntroHtml() : '') + + ''; + }, - return Grid.prototype.renderBgSegs.call(this, allDaySegs); // call the super-method - }, - - - // Renders the given foreground event segments onto the grid - renderFgSegs: function(segs) { - var rowStructs; - - // render an `.el` on each seg - // returns a subset of the segs. segs that were actually rendered - segs = this.renderFgSegEls(segs); - - rowStructs = this.rowStructs = this.renderSegRows(segs); - - // append to each row's content skeleton - this.rowEls.each(function(i, rowNode) { - $(rowNode).find('.fc-content-skeleton > table').append( - rowStructs[i].tbodyEl - ); - }); - - return segs; // return only the segs that were actually rendered - }, - - - // Unrenders all currently rendered foreground event segments - unrenderFgSegs: function() { - var rowStructs = this.rowStructs || []; - var rowStruct; - - while ((rowStruct = rowStructs.pop())) { - rowStruct.tbodyEl.remove(); - } - - this.rowStructs = null; - }, - - - // Uses the given events array to generate elements that should be appended to each row's content skeleton. - // Returns an array of rowStruct objects (see the bottom of `renderSegRow`). - // PRECONDITION: each segment shoud already have a rendered and assigned `.el` - renderSegRows: function(segs) { - var rowStructs = []; - var segRows; - var row; - - segRows = this.groupSegRows(segs); // group into nested arrays - - // iterate each row of segment groupings - for (row = 0; row < segRows.length; row++) { - rowStructs.push( - this.renderSegRow(row, segRows[row]) - ); - } - - return rowStructs; - }, - - - // Builds the HTML to be used for the default element for an individual segment - fgSegHtml: function(seg, disableResizing) { - var view = this.view; - var event = seg.event; - var isDraggable = view.isEventDraggable(event); - var isResizableFromStart = !disableResizing && event.allDay && - seg.isStart && view.isEventResizableFromStart(event); - var isResizableFromEnd = !disableResizing && event.allDay && - seg.isEnd && view.isEventResizableFromEnd(event); - var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd); - var skinCss = cssToStr(this.getSegSkinCss(seg)); - var timeHtml = ''; - var timeText; - var titleHtml; - - classes.unshift('fc-day-grid-event', 'fc-h-event'); - - // Only display a timed events time if it is the starting segment - if (seg.isStart) { - timeText = this.getEventTimeText(event); - if (timeText) { - timeHtml = '' + htmlEscape(timeText) + ''; - } - } - - titleHtml = - '' + - (htmlEscape(event.title || '') || ' ') + // we always want one line of height - ''; - - return '
' + - '
' + - (this.isRTL ? - titleHtml + ' ' + timeHtml : // put a natural space in between - timeHtml + ' ' + titleHtml // - ) + - '
' + - (isResizableFromStart ? - '
' : - '' - ) + - (isResizableFromEnd ? - '
' : - '' - ) + - ''; - }, - - - // Given a row # and an array of segments all in the same row, render a element, a skeleton that contains - // the segments. Returns object with a bunch of internal data about how the render was calculated. - // NOTE: modifies rowSegs - renderSegRow: function(row, rowSegs) { - var colCnt = this.colCnt; - var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels - var levelCnt = Math.max(1, segLevels.length); // ensure at least one level - var tbody = $(''); - var segMatrix = []; // lookup for which segments are rendered into which level+col cells - var cellMatrix = []; // lookup for all elements of the level+col matrix - var loneCellMatrix = []; // lookup for elements that only take up a single column - var i, levelSegs; - var col; - var tr; - var j, seg; - var td; - - // populates empty cells from the current column (`col`) to `endCol` - function emptyCellsUntil(endCol) { - while (col < endCol) { - // try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell - td = (loneCellMatrix[i - 1] || [])[col]; - if (td) { - td.attr( - 'rowspan', - parseInt(td.attr('rowspan') || 1, 10) + 1 - ); - } - else { - td = $(''); - tr.append(td); - } - cellMatrix[i][col] = td; - loneCellMatrix[i][col] = td; - col++; - } - } - - for (i = 0; i < levelCnt; i++) { // iterate through all levels - levelSegs = segLevels[i]; - col = 0; - tr = $(''); - - segMatrix.push([]); - cellMatrix.push([]); - loneCellMatrix.push([]); - - // levelCnt might be 1 even though there are no actual levels. protect against this. - // this single empty row is useful for styling. - if (levelSegs) { - for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level - seg = levelSegs[j]; - - emptyCellsUntil(seg.leftCol); - - // create a container that occupies or more columns. append the event element. - td = $('').append(seg.el); - if (seg.leftCol != seg.rightCol) { - td.attr('colspan', seg.rightCol - seg.leftCol + 1); - } - else { // a single-column segment - loneCellMatrix[i][col] = td; - } - - while (col <= seg.rightCol) { - cellMatrix[i][col] = td; - segMatrix[i][col] = seg; - col++; - } - - tr.append(td); - } - } - - emptyCellsUntil(colCnt); // finish off the row - this.bookendCells(tr); - tbody.append(tr); - } - - return { // a "rowStruct" - row: row, // the row number - tbodyEl: tbody, - cellMatrix: cellMatrix, - segMatrix: segMatrix, - segLevels: segLevels, - segs: rowSegs - }; - }, - - - // Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels. - // NOTE: modifies segs - buildSegLevels: function(segs) { - var levels = []; - var i, seg; - var j; - - // Give preference to elements with certain criteria, so they have - // a chance to be closer to the top. - this.sortEventSegs(segs); - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - - // loop through levels, starting with the topmost, until the segment doesn't collide with other segments - for (j = 0; j < levels.length; j++) { - if (!isDaySegCollision(seg, levels[j])) { - break; - } - } - // `j` now holds the desired subrow index - seg.level = j; - - // create new level array if needed and append segment - (levels[j] || (levels[j] = [])).push(seg); - } - - // order segments left-to-right. very important if calendar is RTL - for (j = 0; j < levels.length; j++) { - levels[j].sort(compareDaySegCols); - } - - return levels; - }, - - - // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row - groupSegRows: function(segs) { - var segRows = []; - var i; - - for (i = 0; i < this.rowCnt; i++) { - segRows.push([]); - } - - for (i = 0; i < segs.length; i++) { - segRows[segs[i].row].push(segs[i]); - } - - return segRows; - } -}); + renderHeadDateCellsHtml: function() { + var htmls = []; + var col, date; + for (col = 0; col < this.colCnt; col++) { + date = this.getCellDate(0, col); + htmls.push(this.renderHeadDateCellHtml(date)); + } -// Computes whether two segments' columns collide. They are assumed to be in the same row. -function isDaySegCollision(seg, otherSegs) { - var i, otherSeg; + return htmls.join(''); + }, - for (i = 0; i < otherSegs.length; i++) { - otherSeg = otherSegs[i]; - if ( - otherSeg.leftCol <= seg.rightCol && - otherSeg.rightCol >= seg.leftCol - ) { - return true; - } - } + // TODO: when internalApiVersion, accept an object for HTML attributes + // (colspan should be no different) + renderHeadDateCellHtml: function(date, colspan, otherAttrs) { + var view = this.view; - return false; -} - - -// A cmp function for determining the leftmost event -function compareDaySegCols(a, b) { - return a.leftCol - b.leftCol; -} - -;; - -/* Methods relate to limiting the number events for a given day on a DayGrid -----------------------------------------------------------------------------------------------------------------------*/ -// NOTE: all the segs being passed around in here are foreground segs - -DayGrid.mixin({ - - segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible - popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible - - - removeSegPopover: function() { - if (this.segPopover) { - this.segPopover.hide(); // in handler, will call segPopover's removeElement - } - }, - - - // Limits the number of "levels" (vertically stacking layers of events) for each row of the grid. - // `levelLimit` can be false (don't limit), a number, or true (should be computed). - limitRows: function(levelLimit) { - var rowStructs = this.rowStructs || []; - var row; // row # - var rowLevelLimit; - - for (row = 0; row < rowStructs.length; row++) { - this.unlimitRow(row); - - if (!levelLimit) { - rowLevelLimit = false; - } - else if (typeof levelLimit === 'number') { - rowLevelLimit = levelLimit; - } - else { - rowLevelLimit = this.computeRowLevelLimit(row); - } - - if (rowLevelLimit !== false) { - this.limitRow(row, rowLevelLimit); - } - } - }, - - - // Computes the number of levels a row will accomodate without going outside its bounds. - // Assumes the row is "rigid" (maintains a constant height regardless of what is inside). - // `row` is the row number. - computeRowLevelLimit: function(row) { - var rowEl = this.rowEls.eq(row); // the containing "fake" row div - var rowHeight = rowEl.height(); // TODO: cache somehow? - var trEls = this.rowStructs[row].tbodyEl.children(); - var i, trEl; - var trHeight; - - function iterInnerHeights(i, childNode) { - trHeight = Math.max(trHeight, $(childNode).outerHeight()); - } - - // Reveal one level at a time and stop when we find one out of bounds - for (i = 0; i < trEls.length; i++) { - trEl = trEls.eq(i).removeClass('fc-limited'); // reset to original state (reveal) - - // with rowspans>1 and IE8, trEl.outerHeight() would return the height of the largest cell, - // so instead, find the tallest inner content element. - trHeight = 0; - trEl.find('> td > :first-child').each(iterInnerHeights); - - if (trEl.position().top + trHeight > rowHeight) { - return i; - } - } - - return false; // should not limit at all - }, - - - // Limits the given grid row to the maximum number of levels and injects "more" links if necessary. - // `row` is the row number. - // `levelLimit` is a number for the maximum (inclusive) number of levels allowed. - limitRow: function(row, levelLimit) { - var _this = this; - var rowStruct = this.rowStructs[row]; - var moreNodes = []; // array of "more" links and DOM nodes - var col = 0; // col #, left-to-right (not chronologically) - var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right - var cellMatrix; // a matrix (by level, then column) of all jQuery elements in the row - var limitedNodes; // array of temporarily hidden level and segment DOM nodes - var i, seg; - var segsBelow; // array of segment objects below `seg` in the current `col` - var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies - var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column) - var td, rowspan; - var segMoreNodes; // array of "more" cells that will stand-in for the current seg's cell - var j; - var moreTd, moreWrap, moreLink; - - // Iterates through empty level cells and places "more" links inside if need be - function emptyCellsUntil(endCol) { // goes from current `col` to `endCol` - while (col < endCol) { - segsBelow = _this.getCellSegs(row, col, levelLimit); - if (segsBelow.length) { - td = cellMatrix[levelLimit - 1][col]; - moreLink = _this.renderMoreLink(row, col, segsBelow); - moreWrap = $('
').append(moreLink); - td.append(moreWrap); - moreNodes.push(moreWrap[0]); - } - col++; - } - } - - if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit? - levelSegs = rowStruct.segLevels[levelLimit - 1]; - cellMatrix = rowStruct.cellMatrix; - - limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level elements past the limit - .addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array - - // iterate though segments in the last allowable level - for (i = 0; i < levelSegs.length; i++) { - seg = levelSegs[i]; - emptyCellsUntil(seg.leftCol); // process empty cells before the segment - - // determine *all* segments below `seg` that occupy the same columns - colSegsBelow = []; - totalSegsBelow = 0; - while (col <= seg.rightCol) { - segsBelow = this.getCellSegs(row, col, levelLimit); - colSegsBelow.push(segsBelow); - totalSegsBelow += segsBelow.length; - col++; - } - - if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links? - td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell - rowspan = td.attr('rowspan') || 1; - segMoreNodes = []; - - // make a replacement for each column the segment occupies. will be one for each colspan - for (j = 0; j < colSegsBelow.length; j++) { - moreTd = $('').attr('rowspan', rowspan); - segsBelow = colSegsBelow[j]; - moreLink = this.renderMoreLink( - row, - seg.leftCol + j, - [ seg ].concat(segsBelow) // count seg as hidden too - ); - moreWrap = $('
').append(moreLink); - moreTd.append(moreWrap); - segMoreNodes.push(moreTd[0]); - moreNodes.push(moreTd[0]); - } - - td.addClass('fc-limited').after($(segMoreNodes)); // hide original and inject replacements - limitedNodes.push(td[0]); - } - } - - emptyCellsUntil(this.colCnt); // finish off the level - rowStruct.moreEls = $(moreNodes); // for easy undoing later - rowStruct.limitedEls = $(limitedNodes); // for easy undoing later - } - }, - - - // Reveals all levels and removes all "more"-related elements for a grid's row. - // `row` is a row number. - unlimitRow: function(row) { - var rowStruct = this.rowStructs[row]; - - if (rowStruct.moreEls) { - rowStruct.moreEls.remove(); - rowStruct.moreEls = null; - } - - if (rowStruct.limitedEls) { - rowStruct.limitedEls.removeClass('fc-limited'); - rowStruct.limitedEls = null; - } - }, - - - // Renders an element that represents hidden event element for a cell. - // Responsible for attaching click handler as well. - renderMoreLink: function(row, col, hiddenSegs) { - var _this = this; - var view = this.view; - - return $('') - .text( - this.getMoreLinkText(hiddenSegs.length) - ) - .on('click', function(ev) { - var clickOption = view.opt('eventLimitClick'); - var date = _this.getCellDate(row, col); - var moreEl = $(this); - var dayEl = _this.getCellEl(row, col); - var allSegs = _this.getCellSegs(row, col); - - // rescope the segments to be within the cell's date - var reslicedAllSegs = _this.resliceDaySegs(allSegs, date); - var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date); - - if (typeof clickOption === 'function') { - // the returned value can be an atomic option - clickOption = view.trigger('eventLimitClick', null, { - date: date, - dayEl: dayEl, - moreEl: moreEl, - segs: reslicedAllSegs, - hiddenSegs: reslicedHiddenSegs - }, ev); - } - - if (clickOption === 'popover') { - _this.showSegPopover(row, col, moreEl, reslicedAllSegs); - } - else if (typeof clickOption === 'string') { // a view name - view.calendar.zoomTo(date, clickOption); - } - }); - }, - - - // Reveals the popover that displays all events within a cell - showSegPopover: function(row, col, moreLink, segs) { - var _this = this; - var view = this.view; - var moreWrap = moreLink.parent(); // the
wrapper around the - var topEl; // the element we want to match the top coordinate of - var options; - - if (this.rowCnt == 1) { - topEl = view.el; // will cause the popover to cover any sort of header - } - else { - topEl = this.rowEls.eq(row); // will align with top of row - } - - options = { - className: 'fc-more-popover', - content: this.renderSegPopoverContent(row, col, segs), - parentEl: this.el, - top: topEl.offset().top, - autoHide: true, // when the user clicks elsewhere, hide the popover - viewportConstrain: view.opt('popoverViewportConstrain'), - hide: function() { - // kill everything when the popover is hidden - _this.segPopover.removeElement(); - _this.segPopover = null; - _this.popoverSegs = null; - } - }; - - // Determine horizontal coordinate. - // We use the moreWrap instead of the to avoid border confusion. - if (this.isRTL) { - options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border - } - else { - options.left = moreWrap.offset().left - 1; // -1 to be over cell border - } - - this.segPopover = new Popover(options); - this.segPopover.show(); - }, - - - // Builds the inner DOM contents of the segment popover - renderSegPopoverContent: function(row, col, segs) { - var view = this.view; - var isTheme = view.opt('theme'); - var title = this.getCellDate(row, col).format(view.opt('dayPopoverFormat')); - var content = $( - '
' + - '' + - '' + - htmlEscape(title) + - '' + - '
' + - '
' + - '
' + - '
' + - '
' - ); - var segContainer = content.find('.fc-event-container'); - var i; - - // render each seg's `el` and only return the visible segs - segs = this.renderFgSegEls(segs, true); // disableResizing=true - this.popoverSegs = segs; - - for (i = 0; i < segs.length; i++) { - - // because segments in the popover are not part of a grid coordinate system, provide a hint to any - // grids that want to do drag-n-drop about which cell it came from - this.prepareHits(); - segs[i].hit = this.getCellHit(row, col); - this.releaseHits(); - - segContainer.append(segs[i].el); - } - - return content; - }, - - - // Given the events within an array of segment objects, reslice them to be in a single day - resliceDaySegs: function(segs, dayDate) { - - // build an array of the original events - var events = $.map(segs, function(seg) { - return seg.event; - }); - - var dayStart = dayDate.clone(); - var dayEnd = dayStart.clone().add(1, 'days'); - var dayRange = { start: dayStart, end: dayEnd }; - - // slice the events with a custom slicing function - segs = this.eventsToSegs( - events, - function(range) { - var seg = intersectRanges(range, dayRange); // undefind if no intersection - return seg ? [ seg ] : []; // must return an array of segments - } - ); - - // force an order because eventsToSegs doesn't guarantee one - this.sortEventSegs(segs); - - return segs; - }, - - - // Generates the text that should be inside a "more" link, given the number of events it represents - getMoreLinkText: function(num) { - var opt = this.view.opt('eventLimitText'); - - if (typeof opt === 'function') { - return opt(num); - } - else { - return '+' + num + ' ' + opt; - } - }, - - - // Returns segments within a given cell. - // If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs. - getCellSegs: function(row, col, startLevel) { - var segMatrix = this.rowStructs[row].segMatrix; - var level = startLevel || 0; - var segs = []; - var seg; - - while (level < segMatrix.length) { - seg = segMatrix[level][col]; - if (seg) { - segs.push(seg); - } - level++; - } - - return segs; - } + return '' + + ' 1 ? + ' colspan="' + colspan + '"' : + '') + + (otherAttrs ? + ' ' + otherAttrs : + '') + + '>' + + htmlEscape(date.format(this.colHeadFormat)) + + ''; + }, -}); -;; - -/* A component that renders one or more columns of vertical time slots -----------------------------------------------------------------------------------------------------------------------*/ -// We mixin DayTable, even though there is only a single row of days - -var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, { - - slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines - snapDuration: null, // granularity of time for dragging and selecting - snapsPerSlot: null, - minTime: null, // Duration object that denotes the first visible time of any given day - maxTime: null, // Duration object that denotes the exclusive visible end time of any given day - labelFormat: null, // formatting string for times running along vertical axis - labelInterval: null, // duration of how often a label should be displayed for a slot - - colEls: null, // cells elements in the day-row background - slatContainerEl: null, // div that wraps all the slat rows - slatEls: null, // elements running horizontally across all columns - nowIndicatorEls: null, - - colCoordCache: null, - slatCoordCache: null, - - - constructor: function() { - Grid.apply(this, arguments); // call the super-constructor - - this.processOptions(); - }, - - - // Renders the time grid into `this.el`, which should already be assigned. - // Relies on the view's colCnt. In the future, this component should probably be self-sufficient. - renderDates: function() { - this.el.html(this.renderHtml()); - this.colEls = this.el.find('.fc-day'); - this.slatContainerEl = this.el.find('.fc-slats'); - this.slatEls = this.slatContainerEl.find('tr'); - - this.colCoordCache = new CoordCache({ - els: this.colEls, - isHorizontal: true - }); - this.slatCoordCache = new CoordCache({ - els: this.slatEls, - isVertical: true - }); - - this.renderContentSkeleton(); - }, - - - // Renders the basic HTML skeleton for the grid - renderHtml: function() { - return '' + - '
' + - '' + - this.renderBgTrHtml(0) + // row=0 - '
' + - '
' + - '
' + - '' + - this.renderSlatRowHtml() + - '
' + - '
'; - }, - - - // Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL. - renderSlatRowHtml: function() { - var view = this.view; - var isRTL = this.isRTL; - var html = ''; - var slotTime = moment.duration(+this.minTime); // wish there was .clone() for durations - var slotDate; // will be on the view's first day, but we only care about its time - var isLabeled; - var axisHtml; - - // Calculate the time for each slot - while (slotTime < this.maxTime) { - slotDate = this.start.clone().time(slotTime); - isLabeled = isInt(divideDurationByDuration(slotTime, this.labelInterval)); - - axisHtml = - '' + - (isLabeled ? - '' + // for matchCellWidths - htmlEscape(slotDate.format(this.labelFormat)) + - '' : - '' - ) + - ''; - - html += - '' + - (!isRTL ? axisHtml : '') + - '' + - (isRTL ? axisHtml : '') + - ""; - - slotTime.add(this.slotDuration); - } - - return html; - }, - - - /* Options - ------------------------------------------------------------------------------------------------------------------*/ - - - // Parses various options into properties of this object - processOptions: function() { - var view = this.view; - var slotDuration = view.opt('slotDuration'); - var snapDuration = view.opt('snapDuration'); - var input; - - slotDuration = moment.duration(slotDuration); - snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration; - - this.slotDuration = slotDuration; - this.snapDuration = snapDuration; - this.snapsPerSlot = slotDuration / snapDuration; // TODO: ensure an integer multiple? - - this.minResizeDuration = snapDuration; // hack - - this.minTime = moment.duration(view.opt('minTime')); - this.maxTime = moment.duration(view.opt('maxTime')); - - // might be an array value (for TimelineView). - // if so, getting the most granular entry (the last one probably). - input = view.opt('slotLabelFormat'); - if ($.isArray(input)) { - input = input[input.length - 1]; - } - - this.labelFormat = - input || - view.opt('axisFormat') || // deprecated - view.opt('smallTimeFormat'); // the computed default - - input = view.opt('slotLabelInterval'); - this.labelInterval = input ? - moment.duration(input) : - this.computeLabelInterval(slotDuration); - }, - - - // Computes an automatic value for slotLabelInterval - computeLabelInterval: function(slotDuration) { - var i; - var labelInterval; - var slotsPerLabel; - - // find the smallest stock label interval that results in more than one slots-per-label - for (i = AGENDA_STOCK_SUB_DURATIONS.length - 1; i >= 0; i--) { - labelInterval = moment.duration(AGENDA_STOCK_SUB_DURATIONS[i]); - slotsPerLabel = divideDurationByDuration(labelInterval, slotDuration); - if (isInt(slotsPerLabel) && slotsPerLabel > 1) { - return labelInterval; - } - } + /* Background Rendering + ------------------------------------------------------------------------------------------------------------------*/ - return moment.duration(slotDuration); // fall back. clone - }, - - - // Computes a default event time formatting string if `timeFormat` is not explicitly defined - computeEventTimeFormat: function() { - return this.view.opt('noMeridiemTimeFormat'); // like "6:30" (no AM/PM) - }, + renderBgTrHtml: function(row) { + return '' + + '' + + (this.isRTL ? '' : this.renderBgIntroHtml(row)) + + this.renderBgCellsHtml(row) + + (this.isRTL ? this.renderBgIntroHtml(row) : '') + + ''; + }, - // Computes a default `displayEventEnd` value if one is not expliclty defined - computeDisplayEventEnd: function() { - return true; - }, + renderBgIntroHtml: function(row) { + return this.renderIntroHtml(); // fall back to generic + }, - /* Hit System - ------------------------------------------------------------------------------------------------------------------*/ + renderBgCellsHtml: function(row) { + var htmls = []; + var col, date; - prepareHits: function() { - this.colCoordCache.build(); - this.slatCoordCache.build(); - }, + for (col = 0; col < this.colCnt; col++) { + date = this.getCellDate(row, col); + htmls.push(this.renderBgCellHtml(date)); + } + return htmls.join(''); + }, - releaseHits: function() { - this.colCoordCache.clear(); - // NOTE: don't clear slatCoordCache because we rely on it for computeTimeTop - }, + renderBgCellHtml: function(date, otherAttrs) { + var view = this.view; + var classes = this.getDayClasses(date); - queryHit: function(leftOffset, topOffset) { - var snapsPerSlot = this.snapsPerSlot; - var colCoordCache = this.colCoordCache; - var slatCoordCache = this.slatCoordCache; - var colIndex = colCoordCache.getHorizontalIndex(leftOffset); - var slatIndex = slatCoordCache.getVerticalIndex(topOffset); + classes.unshift('fc-day', view.widgetContentClass); - if (colIndex != null && slatIndex != null) { - var slatTop = slatCoordCache.getTopOffset(slatIndex); - var slatHeight = slatCoordCache.getHeight(slatIndex); - var partial = (topOffset - slatTop) / slatHeight; // floating point number between 0 and 1 - var localSnapIndex = Math.floor(partial * snapsPerSlot); // the snap # relative to start of slat - var snapIndex = slatIndex * snapsPerSlot + localSnapIndex; - var snapTop = slatTop + (localSnapIndex / snapsPerSlot) * slatHeight; - var snapBottom = slatTop + ((localSnapIndex + 1) / snapsPerSlot) * slatHeight; + return ''; + }, - return { - col: colIndex, - snap: snapIndex, - component: this, // needed unfortunately :( - left: colCoordCache.getLeftOffset(colIndex), - right: colCoordCache.getRightOffset(colIndex), - top: snapTop, - bottom: snapBottom - }; - } - }, + /* Generic + ------------------------------------------------------------------------------------------------------------------*/ - getHitSpan: function(hit) { - var start = this.getCellDate(0, hit.col); // row=0 - var time = this.computeSnapTime(hit.snap); // pass in the snap-index - var end; - start.time(time); - end = start.clone().add(this.snapDuration); + // Generates the default HTML intro for any row. User classes should override + renderIntroHtml: function() { + }, - return { start: start, end: end }; - }, + // TODO: a generic method for dealing with , RTL, intro + // when increment internalApiVersion + // wrapTr (scheduler) - getHitEl: function(hit) { - return this.colEls.eq(hit.col); - }, + /* Utils + ------------------------------------------------------------------------------------------------------------------*/ - /* Dates - ------------------------------------------------------------------------------------------------------------------*/ + // Applies the generic "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. + bookendCells: function(trEl) { + var introHtml = this.renderIntroHtml(); - rangeUpdated: function() { - this.updateDayTable(); - }, + if (introHtml) { + if (this.isRTL) { + trEl.append(introHtml); + } + else { + trEl.prepend(introHtml); + } + } + } + + }; + + ;; + + /* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week. + ----------------------------------------------------------------------------------------------------------------------*/ + + var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, { + numbersVisible: false, // should render a row for day/week numbers? set by outside view. TODO: make internal + bottomCoordPadding: 0, // hack for extending the hit area for the last row of the coordinate grid - // Given a row number of the grid, representing a "snap", returns a time (Duration) from its start-of-day - computeSnapTime: function(snapIndex) { - return moment.duration(this.minTime + this.snapDuration * snapIndex); - }, + rowEls: null, // set of fake row elements + cellEls: null, // set of whole-day elements comprising the row's background + helperEls: null, // set of cell skeleton elements for rendering the mock event "helper" + rowCoordCache: null, + colCoordCache: null, - // Slices up the given span (unzoned start/end with other misc data) into an array of segments - spanToSegs: function(span) { - var segs = this.sliceRangeByTimes(span); - var i; - for (i = 0; i < segs.length; i++) { - if (this.isRTL) { - segs[i].col = this.daysPerRow - 1 - segs[i].dayIndex; - } - else { - segs[i].col = segs[i].dayIndex; - } - } + // Renders the rows and columns into the component's `this.el`, which should already be assigned. + // isRigid determins whether the individual rows should ignore the contents and be a constant height. + // Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient. + renderDates: function(isRigid) { + var view = this.view; + var rowCnt = this.rowCnt; + var colCnt = this.colCnt; + var html = ''; + var row; + var col; - return segs; - }, + for (row = 0; row < rowCnt; row++) { + html += this.renderDayRowHtml(row, isRigid); + } + this.el.html(html); + this.rowEls = this.el.find('.fc-row'); + this.cellEls = this.el.find('.fc-day'); - sliceRangeByTimes: function(range) { - var segs = []; - var seg; - var dayIndex; - var dayDate; - var dayRange; + this.rowCoordCache = new CoordCache({ + els: this.rowEls, + isVertical: true + }); + this.colCoordCache = new CoordCache({ + els: this.cellEls.slice(0, this.colCnt), // only the first row + isHorizontal: true + }); - for (dayIndex = 0; dayIndex < this.daysPerRow; dayIndex++) { - dayDate = this.dayDates[dayIndex].clone(); // TODO: better API for this? - dayRange = { - start: dayDate.clone().time(this.minTime), - end: dayDate.clone().time(this.maxTime) - }; - seg = intersectRanges(range, dayRange); // both will be ambig timezone - if (seg) { - seg.dayIndex = dayIndex; - segs.push(seg); - } - } + // trigger dayRender with each cell's element + for (row = 0; row < rowCnt; row++) { + for (col = 0; col < colCnt; col++) { + view.trigger( + 'dayRender', + null, + this.getCellDate(row, col), + this.getCellEl(row, col) + ); + } + } + }, - return segs; - }, + unrenderDates: function() { + this.removeSegPopover(); + }, - /* Coordinates - ------------------------------------------------------------------------------------------------------------------*/ + renderBusinessHours: function() { + var events = this.view.calendar.getBusinessHoursEvents(true); // wholeDay=true + var segs = this.eventsToSegs(events); - updateSize: function(isResize) { // NOT a standard Grid method - this.slatCoordCache.build(); + this.renderFill('businessHours', segs, 'bgevent'); + }, - if (isResize) { - this.updateSegVerticals( - [].concat(this.fgSegs || [], this.bgSegs || [], this.businessSegs || []) - ); - } - }, + // Generates the HTML for a single row, which is a div that wraps a table. + // `row` is the row number. + renderDayRowHtml: function(row, isRigid) { + var view = this.view; + var classes = [ 'fc-row', 'fc-week', view.widgetContentClass ]; - getTotalSlatHeight: function() { - return this.slatContainerEl.outerHeight(); - }, + if (isRigid) { + classes.push('fc-rigid'); + } + return '' + + '
' + + '
' + + '' + + this.renderBgTrHtml(row) + + '
' + + '
' + + '
' + + '' + + (this.numbersVisible ? + '' + + this.renderNumberTrHtml(row) + + '' : + '' + ) + + '
' + + '
' + + '
'; + }, - // Computes the top coordinate, relative to the bounds of the grid, of the given date. - // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight. - computeDateTop: function(date, startOfDayDate) { - return this.computeTimeTop( - moment.duration( - date - startOfDayDate.clone().stripTime() - ) - ); - }, + /* Grid Number Rendering + ------------------------------------------------------------------------------------------------------------------*/ - // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration). - computeTimeTop: function(time) { - var len = this.slatEls.length; - var slatCoverage = (time - this.minTime) / this.slotDuration; // floating-point value of # of slots covered - var slatIndex; - var slatRemainder; - // compute a floating-point number for how many slats should be progressed through. - // from 0 to number of slats (inclusive) - // constrained because minTime/maxTime might be customized. - slatCoverage = Math.max(0, slatCoverage); - slatCoverage = Math.min(len, slatCoverage); + renderNumberTrHtml: function(row) { + return '' + + '' + + (this.isRTL ? '' : this.renderNumberIntroHtml(row)) + + this.renderNumberCellsHtml(row) + + (this.isRTL ? this.renderNumberIntroHtml(row) : '') + + ''; + }, - // an integer index of the furthest whole slat - // from 0 to number slats (*exclusive*, so len-1) - slatIndex = Math.floor(slatCoverage); - slatIndex = Math.min(slatIndex, len - 1); - // how much further through the slatIndex slat (from 0.0-1.0) must be covered in addition. - // could be 1.0 if slatCoverage is covering *all* the slots - slatRemainder = slatCoverage - slatIndex; + renderNumberIntroHtml: function(row) { + return this.renderIntroHtml(); + }, - return this.slatCoordCache.getTopPosition(slatIndex) + - this.slatCoordCache.getHeight(slatIndex) * slatRemainder; - }, + renderNumberCellsHtml: function(row) { + var htmls = []; + var col, date; + for (col = 0; col < this.colCnt; col++) { + date = this.getCellDate(row, col); + htmls.push(this.renderNumberCellHtml(date)); + } - /* Event Drag Visualization - ------------------------------------------------------------------------------------------------------------------*/ + return htmls.join(''); + }, - // Renders a visual indication of an event being dragged over the specified date(s). - // A returned value of `true` signals that a mock "helper" event has been rendered. - renderDrag: function(eventLocation, seg) { + // Generates the HTML for the s of the "number" row in the DayGrid's content skeleton. + // The number row will only exist if either day numbers or week numbers are turned on. + renderNumberCellHtml: function(date) { + var classes; - if (seg) { // if there is event information for this drag, render a helper event + if (!this.view.dayNumbersVisible) { // if there are week numbers but not day numbers + return ''; // will create an empty space above events :( + } - // returns mock event elements - // signal that a helper has been rendered - return this.renderEventLocationHelper(eventLocation, seg); - } - else { - // otherwise, just render a highlight - this.renderHighlight(this.eventToSpan(eventLocation)); - } - }, + classes = this.getDayClasses(date); + classes.unshift('fc-day-number'); + return '' + + '' + + date.date() + + ''; + }, - // Unrenders any visual indication of an event being dragged - unrenderDrag: function() { - this.unrenderHelper(); - this.unrenderHighlight(); - }, + /* Options + ------------------------------------------------------------------------------------------------------------------*/ - /* Event Resize Visualization - ------------------------------------------------------------------------------------------------------------------*/ + // Computes a default event time formatting string if `timeFormat` is not explicitly defined + computeEventTimeFormat: function() { + return this.view.opt('extraSmallTimeFormat'); // like "6p" or "6:30p" + }, - // Renders a visual indication of an event being resized - renderEventResize: function(eventLocation, seg) { - return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements - }, + // Computes a default `displayEventEnd` value if one is not expliclty defined + computeDisplayEventEnd: function() { + return this.colCnt == 1; // we'll likely have space if there's only one day + }, - // Unrenders any visual indication of an event being resized - unrenderEventResize: function() { - this.unrenderHelper(); - }, + /* Dates + ------------------------------------------------------------------------------------------------------------------*/ - /* Event Helper - ------------------------------------------------------------------------------------------------------------------*/ + rangeUpdated: function() { + this.updateDayTable(); + }, - // Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag) - renderHelper: function(event, sourceSeg) { - return this.renderHelperSegs(this.eventToSegs(event), sourceSeg); // returns mock event elements - }, + // Slices up the given span (unzoned start/end with other misc data) into an array of segments + spanToSegs: function(span) { + var segs = this.sliceRangeByRow(span); + var i, seg; - // Unrenders any mock helper event - unrenderHelper: function() { - this.unrenderHelperSegs(); - }, + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + if (this.isRTL) { + seg.leftCol = this.daysPerRow - 1 - seg.lastRowDayIndex; + seg.rightCol = this.daysPerRow - 1 - seg.firstRowDayIndex; + } + else { + seg.leftCol = seg.firstRowDayIndex; + seg.rightCol = seg.lastRowDayIndex; + } + } + return segs; + }, - /* Business Hours - ------------------------------------------------------------------------------------------------------------------*/ + /* Hit System + ------------------------------------------------------------------------------------------------------------------*/ - renderBusinessHours: function() { - var events = this.view.calendar.getBusinessHoursEvents(); - var segs = this.eventsToSegs(events); - this.renderBusinessSegs(segs); - }, + prepareHits: function() { + this.colCoordCache.build(); + this.rowCoordCache.build(); + this.rowCoordCache.bottoms[this.rowCnt - 1] += this.bottomCoordPadding; // hack + }, - unrenderBusinessHours: function() { - this.unrenderBusinessSegs(); - }, + releaseHits: function() { + this.colCoordCache.clear(); + this.rowCoordCache.clear(); + }, - /* Now Indicator - ------------------------------------------------------------------------------------------------------------------*/ + queryHit: function(leftOffset, topOffset) { + var col = this.colCoordCache.getHorizontalIndex(leftOffset); + var row = this.rowCoordCache.getVerticalIndex(topOffset); + if (row != null && col != null) { + return this.getCellHit(row, col); + } + }, - getNowIndicatorUnit: function() { - return 'minute'; // will refresh on the minute - }, + getHitSpan: function(hit) { + return this.getCellRange(hit.row, hit.col); + }, - renderNowIndicator: function(date) { - // seg system might be overkill, but it handles scenario where line needs to be rendered - // more than once because of columns with the same date (resources columns for example) - var segs = this.spanToSegs({ start: date, end: date }); - var top = this.computeDateTop(date, date); - var nodes = []; - var i; - // render lines within the columns - for (i = 0; i < segs.length; i++) { - nodes.push($('
') - .css('top', top) - .appendTo(this.colContainerEls.eq(segs[i].col))[0]); - } + getHitEl: function(hit) { + return this.getCellEl(hit.row, hit.col); + }, - // render an arrow over the axis - if (segs.length > 0) { // is the current time in view? - nodes.push($('
') - .css('top', top) - .appendTo(this.el.find('.fc-content-skeleton'))[0]); - } - this.nowIndicatorEls = $(nodes); - }, + /* Cell System + ------------------------------------------------------------------------------------------------------------------*/ + // FYI: the first column is the leftmost column, regardless of date - unrenderNowIndicator: function() { - if (this.nowIndicatorEls) { - this.nowIndicatorEls.remove(); - this.nowIndicatorEls = null; - } - }, + getCellHit: function(row, col) { + return { + row: row, + col: col, + component: this, // needed unfortunately :( + left: this.colCoordCache.getLeftOffset(col), + right: this.colCoordCache.getRightOffset(col), + top: this.rowCoordCache.getTopOffset(row), + bottom: this.rowCoordCache.getBottomOffset(row) + }; + }, - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ + getCellEl: function(row, col) { + return this.cellEls.eq(row * this.colCnt + col); + }, - // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight. - renderSelection: function(span) { - if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered + /* Event Drag Visualization + ------------------------------------------------------------------------------------------------------------------*/ + // TODO: move to DayGrid.event, similar to what we did with Grid's drag methods - // normally acceps an eventLocation, span has a start/end, which is good enough - this.renderEventLocationHelper(span); - } - else { - this.renderHighlight(span); - } - }, + // Renders a visual indication of an event or external element being dragged. + // `eventLocation` has zoned start and end (optional) + renderDrag: function(eventLocation, seg) { - // Unrenders any visual indication of a selection - unrenderSelection: function() { - this.unrenderHelper(); - this.unrenderHighlight(); - }, + // always render a highlight underneath + this.renderHighlight(this.eventToSpan(eventLocation)); + // if a segment from the same calendar but another component is being dragged, render a helper event + if (seg && !seg.el.closest(this.el).length) { - /* Highlight - ------------------------------------------------------------------------------------------------------------------*/ + return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements + } + }, - renderHighlight: function(span) { - this.renderHighlightSegs(this.spanToSegs(span)); - }, + // Unrenders any visual indication of a hovering event + unrenderDrag: function() { + this.unrenderHighlight(); + this.unrenderHelper(); + }, - unrenderHighlight: function() { - this.unrenderHighlightSegs(); - } + /* Event Resize Visualization + ------------------------------------------------------------------------------------------------------------------*/ -}); -;; - -/* Methods for rendering SEGMENTS, pieces of content that live on the view - ( this file is no longer just for events ) -----------------------------------------------------------------------------------------------------------------------*/ - -TimeGrid.mixin({ - - colContainerEls: null, // containers for each column - - // inner-containers for each column where different types of segs live - fgContainerEls: null, - bgContainerEls: null, - helperContainerEls: null, - highlightContainerEls: null, - businessContainerEls: null, + // Renders a visual indication of an event being resized + renderEventResize: function(eventLocation, seg) { + this.renderHighlight(this.eventToSpan(eventLocation)); + return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements + }, - // arrays of different types of displayed segments - fgSegs: null, - bgSegs: null, - helperSegs: null, - highlightSegs: null, - businessSegs: null, + // Unrenders a visual indication of an event being resized + unrenderEventResize: function() { + this.unrenderHighlight(); + this.unrenderHelper(); + }, - // Renders the DOM that the view's content will live in - renderContentSkeleton: function() { - var cellHtml = ''; - var i; - var skeletonEl; - for (i = 0; i < this.colCnt; i++) { - cellHtml += - '' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - ''; - } + /* Event Helper + ------------------------------------------------------------------------------------------------------------------*/ - skeletonEl = $( - '
' + - '' + - '' + cellHtml + '' + - '
' + - '
' - ); - this.colContainerEls = skeletonEl.find('.fc-content-col'); - this.helperContainerEls = skeletonEl.find('.fc-helper-container'); - this.fgContainerEls = skeletonEl.find('.fc-event-container:not(.fc-helper-container)'); - this.bgContainerEls = skeletonEl.find('.fc-bgevent-container'); - this.highlightContainerEls = skeletonEl.find('.fc-highlight-container'); - this.businessContainerEls = skeletonEl.find('.fc-business-container'); + // Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null. + renderHelper: function(event, sourceSeg) { + var helperNodes = []; + var segs = this.eventToSegs(event); + var rowStructs; - this.bookendCells(skeletonEl.find('tr')); // TODO: do this on string level - this.el.append(skeletonEl); - }, + segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered + rowStructs = this.renderSegRows(segs); + // inject each new event skeleton into each associated row + this.rowEls.each(function(row, rowNode) { + var rowEl = $(rowNode); // the .fc-row + var skeletonEl = $('
'); // will be absolutely positioned + var skeletonTop; - /* Foreground Events - ------------------------------------------------------------------------------------------------------------------*/ + // If there is an original segment, match the top position. Otherwise, put it at the row's top level + if (sourceSeg && sourceSeg.row === row) { + skeletonTop = sourceSeg.el.position().top; + } + else { + skeletonTop = rowEl.find('.fc-content-skeleton tbody').position().top; + } + skeletonEl.css('top', skeletonTop) + .find('table') + .append(rowStructs[row].tbodyEl); - renderFgSegs: function(segs) { - segs = this.renderFgSegsIntoContainers(segs, this.fgContainerEls); - this.fgSegs = segs; - return segs; // needed for Grid::renderEvents - }, + rowEl.append(skeletonEl); + helperNodes.push(skeletonEl[0]); + }); + return ( // must return the elements rendered + this.helperEls = $(helperNodes) // array -> jQuery set + ); + }, - unrenderFgSegs: function() { - this.unrenderNamedSegs('fgSegs'); - }, + // Unrenders any visual indication of a mock helper event + unrenderHelper: function() { + if (this.helperEls) { + this.helperEls.remove(); + this.helperEls = null; + } + }, - /* Foreground Helper Events - ------------------------------------------------------------------------------------------------------------------*/ + /* Fill System (highlight, background events, business hours) + ------------------------------------------------------------------------------------------------------------------*/ - renderHelperSegs: function(segs, sourceSeg) { - var helperEls = []; - var i, seg; - var sourceEl; - segs = this.renderFgSegsIntoContainers(segs, this.helperContainerEls); + fillSegTag: 'td', // override the default tag name - // Try to make the segment that is in the same row as sourceSeg look the same - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - if (sourceSeg && sourceSeg.col === seg.col) { - sourceEl = sourceSeg.el; - seg.el.css({ - left: sourceEl.css('left'), - right: sourceEl.css('right'), - 'margin-left': sourceEl.css('margin-left'), - 'margin-right': sourceEl.css('margin-right') - }); - } - helperEls.push(seg.el[0]); - } - this.helperSegs = segs; + // Renders a set of rectangles over the given segments of days. + // Only returns segments that successfully rendered. + renderFill: function(type, segs, className) { + var nodes = []; + var i, seg; + var skeletonEl; - return $(helperEls); // must return rendered helpers - }, + segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + skeletonEl = this.renderFillRow(type, seg, className); + this.rowEls.eq(seg.row).append(skeletonEl); + nodes.push(skeletonEl[0]); + } - unrenderHelperSegs: function() { - this.unrenderNamedSegs('helperSegs'); - }, + this.elsByFill[type] = $(nodes); + return segs; + }, - /* Background Events - ------------------------------------------------------------------------------------------------------------------*/ + // Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered. + renderFillRow: function(type, seg, className) { + var colCnt = this.colCnt; + var startCol = seg.leftCol; + var endCol = seg.rightCol + 1; + var skeletonEl; + var trEl; - renderBgSegs: function(segs) { - segs = this.renderFillSegEls('bgEvent', segs); // TODO: old fill system - this.updateSegVerticals(segs); - this.attachSegsByCol(this.groupSegsByCol(segs), this.bgContainerEls); - this.bgSegs = segs; - return segs; // needed for Grid::renderEvents - }, + className = className || type.toLowerCase(); + skeletonEl = $( + '
' + + '
' + + '
' + ); + trEl = skeletonEl.find('tr'); - unrenderBgSegs: function() { - this.unrenderNamedSegs('bgSegs'); - }, + if (startCol > 0) { + trEl.append(''); + } + trEl.append( + seg.el.attr('colspan', endCol - startCol) + ); - /* Highlight - ------------------------------------------------------------------------------------------------------------------*/ + if (endCol < colCnt) { + trEl.append(''); + } + this.bookendCells(trEl); - renderHighlightSegs: function(segs) { - segs = this.renderFillSegEls('highlight', segs); // TODO: old fill system - this.updateSegVerticals(segs); - this.attachSegsByCol(this.groupSegsByCol(segs), this.highlightContainerEls); - this.highlightSegs = segs; - }, + return skeletonEl; + } + }); - unrenderHighlightSegs: function() { - this.unrenderNamedSegs('highlightSegs'); - }, + ;; + /* Event-rendering methods for the DayGrid class + ----------------------------------------------------------------------------------------------------------------------*/ - /* Business Hours - ------------------------------------------------------------------------------------------------------------------*/ + DayGrid.mixin({ + rowStructs: null, // an array of objects, each holding information about a row's foreground event-rendering - renderBusinessSegs: function(segs) { - segs = this.renderFillSegEls('businessHours', segs); // TODO: old fill system - this.updateSegVerticals(segs); - this.attachSegsByCol(this.groupSegsByCol(segs), this.businessContainerEls); - this.businessSegs = segs; - }, + // Unrenders all events currently rendered on the grid + unrenderEvents: function() { + this.removeSegPopover(); // removes the "more.." events popover + Grid.prototype.unrenderEvents.apply(this, arguments); // calls the super-method + }, - unrenderBusinessSegs: function() { - this.unrenderNamedSegs('businessSegs'); - }, + // Retrieves all rendered segment objects currently rendered on the grid + getEventSegs: function() { + return Grid.prototype.getEventSegs.call(this) // get the segments from the super-method + .concat(this.popoverSegs || []); // append the segments from the "more..." popover + }, - /* Seg Rendering Utils - ------------------------------------------------------------------------------------------------------------------*/ + // Renders the given background event segments onto the grid + renderBgSegs: function(segs) { - // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col - groupSegsByCol: function(segs) { - var segsByCol = []; - var i; + // don't render timed background events + var allDaySegs = $.grep(segs, function(seg) { + return seg.event.allDay; + }); - for (i = 0; i < this.colCnt; i++) { - segsByCol.push([]); - } + return Grid.prototype.renderBgSegs.call(this, allDaySegs); // call the super-method + }, - for (i = 0; i < segs.length; i++) { - segsByCol[segs[i].col].push(segs[i]); - } - return segsByCol; - }, + // Renders the given foreground event segments onto the grid + renderFgSegs: function(segs) { + var rowStructs; + + // render an `.el` on each seg + // returns a subset of the segs. segs that were actually rendered + segs = this.renderFgSegEls(segs); + + rowStructs = this.rowStructs = this.renderSegRows(segs); + + // append to each row's content skeleton + this.rowEls.each(function(i, rowNode) { + $(rowNode).find('.fc-content-skeleton > table').append( + rowStructs[i].tbodyEl + ); + }); + + return segs; // return only the segs that were actually rendered + }, + + + // Unrenders all currently rendered foreground event segments + unrenderFgSegs: function() { + var rowStructs = this.rowStructs || []; + var rowStruct; + + while ((rowStruct = rowStructs.pop())) { + rowStruct.tbodyEl.remove(); + } + + this.rowStructs = null; + }, + + + // Uses the given events array to generate elements that should be appended to each row's content skeleton. + // Returns an array of rowStruct objects (see the bottom of `renderSegRow`). + // PRECONDITION: each segment shoud already have a rendered and assigned `.el` + renderSegRows: function(segs) { + var rowStructs = []; + var segRows; + var row; + + segRows = this.groupSegRows(segs); // group into nested arrays + + // iterate each row of segment groupings + for (row = 0; row < segRows.length; row++) { + rowStructs.push( + this.renderSegRow(row, segRows[row]) + ); + } + + return rowStructs; + }, + + + // Builds the HTML to be used for the default element for an individual segment + fgSegHtml: function(seg, disableResizing) { + var view = this.view; + var event = seg.event; + var isDraggable = view.isEventDraggable(event); + var isResizableFromStart = !disableResizing && event.allDay && + seg.isStart && view.isEventResizableFromStart(event); + var isResizableFromEnd = !disableResizing && event.allDay && + seg.isEnd && view.isEventResizableFromEnd(event); + var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd); + var skinCss = cssToStr(this.getSegSkinCss(seg)); + var timeHtml = ''; + var timeText; + var titleHtml; + + classes.unshift('fc-day-grid-event', 'fc-h-event'); + + // Only display a timed events time if it is the starting segment + if (seg.isStart) { + timeText = this.getEventTimeText(event); + if (timeText) { + timeHtml = '' + htmlEscape(timeText) + ''; + } + } + + titleHtml = + '' + + (htmlEscape(event.title || '') || ' ') + // we always want one line of height + ''; + + return '
' + + '
' + + (this.isRTL ? + titleHtml + ' ' + timeHtml : // put a natural space in between + timeHtml + ' ' + titleHtml // + ) + + '
' + + (isResizableFromStart ? + '
' : + '' + ) + + (isResizableFromEnd ? + '
' : + '' + ) + + ''; + }, + + + // Given a row # and an array of segments all in the same row, render a element, a skeleton that contains + // the segments. Returns object with a bunch of internal data about how the render was calculated. + // NOTE: modifies rowSegs + renderSegRow: function(row, rowSegs) { + var colCnt = this.colCnt; + var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels + var levelCnt = Math.max(1, segLevels.length); // ensure at least one level + var tbody = $(''); + var segMatrix = []; // lookup for which segments are rendered into which level+col cells + var cellMatrix = []; // lookup for all elements of the level+col matrix + var loneCellMatrix = []; // lookup for elements that only take up a single column + var i, levelSegs; + var col; + var tr; + var j, seg; + var td; + + // populates empty cells from the current column (`col`) to `endCol` + function emptyCellsUntil(endCol) { + while (col < endCol) { + // try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell + td = (loneCellMatrix[i - 1] || [])[col]; + if (td) { + td.attr( + 'rowspan', + parseInt(td.attr('rowspan') || 1, 10) + 1 + ); + } + else { + td = $(''); + tr.append(td); + } + cellMatrix[i][col] = td; + loneCellMatrix[i][col] = td; + col++; + } + } + + for (i = 0; i < levelCnt; i++) { // iterate through all levels + levelSegs = segLevels[i]; + col = 0; + tr = $(''); + + segMatrix.push([]); + cellMatrix.push([]); + loneCellMatrix.push([]); + + // levelCnt might be 1 even though there are no actual levels. protect against this. + // this single empty row is useful for styling. + if (levelSegs) { + for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level + seg = levelSegs[j]; + + emptyCellsUntil(seg.leftCol); + + // create a container that occupies or more columns. append the event element. + td = $('').append(seg.el); + if (seg.leftCol != seg.rightCol) { + td.attr('colspan', seg.rightCol - seg.leftCol + 1); + } + else { // a single-column segment + loneCellMatrix[i][col] = td; + } + + while (col <= seg.rightCol) { + cellMatrix[i][col] = td; + segMatrix[i][col] = seg; + col++; + } + + tr.append(td); + } + } + + emptyCellsUntil(colCnt); // finish off the row + this.bookendCells(tr); + tbody.append(tr); + } + + return { // a "rowStruct" + row: row, // the row number + tbodyEl: tbody, + cellMatrix: cellMatrix, + segMatrix: segMatrix, + segLevels: segLevels, + segs: rowSegs + }; + }, + + + // Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels. + // NOTE: modifies segs + buildSegLevels: function(segs) { + var levels = []; + var i, seg; + var j; + + // Give preference to elements with certain criteria, so they have + // a chance to be closer to the top. + this.sortEventSegs(segs); + + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + + // loop through levels, starting with the topmost, until the segment doesn't collide with other segments + for (j = 0; j < levels.length; j++) { + if (!isDaySegCollision(seg, levels[j])) { + break; + } + } + // `j` now holds the desired subrow index + seg.level = j; + + // create new level array if needed and append segment + (levels[j] || (levels[j] = [])).push(seg); + } + + // order segments left-to-right. very important if calendar is RTL + for (j = 0; j < levels.length; j++) { + levels[j].sort(compareDaySegCols); + } + + return levels; + }, + + + // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row + groupSegRows: function(segs) { + var segRows = []; + var i; + + for (i = 0; i < this.rowCnt; i++) { + segRows.push([]); + } + + for (i = 0; i < segs.length; i++) { + segRows[segs[i].row].push(segs[i]); + } + + return segRows; + } + + }); + + + // Computes whether two segments' columns collide. They are assumed to be in the same row. + function isDaySegCollision(seg, otherSegs) { + var i, otherSeg; + + for (i = 0; i < otherSegs.length; i++) { + otherSeg = otherSegs[i]; + + if ( + otherSeg.leftCol <= seg.rightCol && + otherSeg.rightCol >= seg.leftCol + ) { + return true; + } + } + + return false; + } + + + // A cmp function for determining the leftmost event + function compareDaySegCols(a, b) { + return a.leftCol - b.leftCol; + } + + ;; + + /* Methods relate to limiting the number events for a given day on a DayGrid + ----------------------------------------------------------------------------------------------------------------------*/ + // NOTE: all the segs being passed around in here are foreground segs + + DayGrid.mixin({ + + segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible + popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible + + + removeSegPopover: function() { + if (this.segPopover) { + this.segPopover.hide(); // in handler, will call segPopover's removeElement + } + }, + + + // Limits the number of "levels" (vertically stacking layers of events) for each row of the grid. + // `levelLimit` can be false (don't limit), a number, or true (should be computed). + limitRows: function(levelLimit) { + var rowStructs = this.rowStructs || []; + var row; // row # + var rowLevelLimit; + + for (row = 0; row < rowStructs.length; row++) { + this.unlimitRow(row); + + if (!levelLimit) { + rowLevelLimit = false; + } + else if (typeof levelLimit === 'number') { + rowLevelLimit = levelLimit; + } + else { + rowLevelLimit = this.computeRowLevelLimit(row); + } + + if (rowLevelLimit !== false) { + this.limitRow(row, rowLevelLimit); + } + } + }, + + + // Computes the number of levels a row will accomodate without going outside its bounds. + // Assumes the row is "rigid" (maintains a constant height regardless of what is inside). + // `row` is the row number. + computeRowLevelLimit: function(row) { + var rowEl = this.rowEls.eq(row); // the containing "fake" row div + var rowHeight = rowEl.height(); // TODO: cache somehow? + var trEls = this.rowStructs[row].tbodyEl.children(); + var i, trEl; + var trHeight; + + function iterInnerHeights(i, childNode) { + trHeight = Math.max(trHeight, $(childNode).outerHeight()); + } + + // Reveal one level at a time and stop when we find one out of bounds + for (i = 0; i < trEls.length; i++) { + trEl = trEls.eq(i).removeClass('fc-limited'); // reset to original state (reveal) + + // with rowspans>1 and IE8, trEl.outerHeight() would return the height of the largest cell, + // so instead, find the tallest inner content element. + trHeight = 0; + trEl.find('> td > :first-child').each(iterInnerHeights); + + if (trEl.position().top + trHeight > rowHeight) { + return i; + } + } + + return false; // should not limit at all + }, + + + // Limits the given grid row to the maximum number of levels and injects "more" links if necessary. + // `row` is the row number. + // `levelLimit` is a number for the maximum (inclusive) number of levels allowed. + limitRow: function(row, levelLimit) { + var _this = this; + var rowStruct = this.rowStructs[row]; + var moreNodes = []; // array of "more" links and DOM nodes + var col = 0; // col #, left-to-right (not chronologically) + var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right + var cellMatrix; // a matrix (by level, then column) of all jQuery elements in the row + var limitedNodes; // array of temporarily hidden level and segment DOM nodes + var i, seg; + var segsBelow; // array of segment objects below `seg` in the current `col` + var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies + var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column) + var td, rowspan; + var segMoreNodes; // array of "more" cells that will stand-in for the current seg's cell + var j; + var moreTd, moreWrap, moreLink; + + // Iterates through empty level cells and places "more" links inside if need be + function emptyCellsUntil(endCol) { // goes from current `col` to `endCol` + while (col < endCol) { + segsBelow = _this.getCellSegs(row, col, levelLimit); + if (segsBelow.length) { + td = cellMatrix[levelLimit - 1][col]; + moreLink = _this.renderMoreLink(row, col, segsBelow); + moreWrap = $('
').append(moreLink); + td.append(moreWrap); + moreNodes.push(moreWrap[0]); + } + col++; + } + } + + if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit? + levelSegs = rowStruct.segLevels[levelLimit - 1]; + cellMatrix = rowStruct.cellMatrix; + + limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level elements past the limit + .addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array + + // iterate though segments in the last allowable level + for (i = 0; i < levelSegs.length; i++) { + seg = levelSegs[i]; + emptyCellsUntil(seg.leftCol); // process empty cells before the segment + + // determine *all* segments below `seg` that occupy the same columns + colSegsBelow = []; + totalSegsBelow = 0; + while (col <= seg.rightCol) { + segsBelow = this.getCellSegs(row, col, levelLimit); + colSegsBelow.push(segsBelow); + totalSegsBelow += segsBelow.length; + col++; + } + + if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links? + td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell + rowspan = td.attr('rowspan') || 1; + segMoreNodes = []; + + // make a replacement for each column the segment occupies. will be one for each colspan + for (j = 0; j < colSegsBelow.length; j++) { + moreTd = $('').attr('rowspan', rowspan); + segsBelow = colSegsBelow[j]; + moreLink = this.renderMoreLink( + row, + seg.leftCol + j, + [ seg ].concat(segsBelow) // count seg as hidden too + ); + moreWrap = $('
').append(moreLink); + moreTd.append(moreWrap); + segMoreNodes.push(moreTd[0]); + moreNodes.push(moreTd[0]); + } + + td.addClass('fc-limited').after($(segMoreNodes)); // hide original and inject replacements + limitedNodes.push(td[0]); + } + } + + emptyCellsUntil(this.colCnt); // finish off the level + rowStruct.moreEls = $(moreNodes); // for easy undoing later + rowStruct.limitedEls = $(limitedNodes); // for easy undoing later + } + }, + + + // Reveals all levels and removes all "more"-related elements for a grid's row. + // `row` is a row number. + unlimitRow: function(row) { + var rowStruct = this.rowStructs[row]; + + if (rowStruct.moreEls) { + rowStruct.moreEls.remove(); + rowStruct.moreEls = null; + } + + if (rowStruct.limitedEls) { + rowStruct.limitedEls.removeClass('fc-limited'); + rowStruct.limitedEls = null; + } + }, + + + // Renders an element that represents hidden event element for a cell. + // Responsible for attaching click handler as well. + renderMoreLink: function(row, col, hiddenSegs) { + var _this = this; + var view = this.view; + + return $('') + .text( + this.getMoreLinkText(hiddenSegs.length) + ) + .on('click', function(ev) { + var clickOption = view.opt('eventLimitClick'); + var date = _this.getCellDate(row, col); + var moreEl = $(this); + var dayEl = _this.getCellEl(row, col); + var allSegs = _this.getCellSegs(row, col); + + // rescope the segments to be within the cell's date + var reslicedAllSegs = _this.resliceDaySegs(allSegs, date); + var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date); + + if (typeof clickOption === 'function') { + // the returned value can be an atomic option + clickOption = view.trigger('eventLimitClick', null, { + date: date, + dayEl: dayEl, + moreEl: moreEl, + segs: reslicedAllSegs, + hiddenSegs: reslicedHiddenSegs + }, ev); + } + + if (clickOption === 'popover') { + _this.showSegPopover(row, col, moreEl, reslicedAllSegs); + } + else if (typeof clickOption === 'string') { // a view name + view.calendar.zoomTo(date, clickOption); + } + }); + }, + + + // Reveals the popover that displays all events within a cell + showSegPopover: function(row, col, moreLink, segs) { + var _this = this; + var view = this.view; + var moreWrap = moreLink.parent(); // the
wrapper around the + var topEl; // the element we want to match the top coordinate of + var options; + + if (this.rowCnt == 1) { + topEl = view.el; // will cause the popover to cover any sort of header + } + else { + topEl = this.rowEls.eq(row); // will align with top of row + } + + options = { + className: 'fc-more-popover', + content: this.renderSegPopoverContent(row, col, segs), + parentEl: this.el, + top: topEl.offset().top, + autoHide: true, // when the user clicks elsewhere, hide the popover + viewportConstrain: view.opt('popoverViewportConstrain'), + hide: function() { + // kill everything when the popover is hidden + _this.segPopover.removeElement(); + _this.segPopover = null; + _this.popoverSegs = null; + } + }; + + // Determine horizontal coordinate. + // We use the moreWrap instead of the to avoid border confusion. + if (this.isRTL) { + options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border + } + else { + options.left = moreWrap.offset().left - 1; // -1 to be over cell border + } + + this.segPopover = new Popover(options); + this.segPopover.show(); + }, + + + // Builds the inner DOM contents of the segment popover + renderSegPopoverContent: function(row, col, segs) { + var view = this.view; + var isTheme = view.opt('theme'); + var title = this.getCellDate(row, col).format(view.opt('dayPopoverFormat')); + var content = $( + '
' + + '' + + '' + + htmlEscape(title) + + '' + + '
' + + '
' + + '
' + + '
' + + '
' + ); + var segContainer = content.find('.fc-event-container'); + var i; + + // render each seg's `el` and only return the visible segs + segs = this.renderFgSegEls(segs, true); // disableResizing=true + this.popoverSegs = segs; + + for (i = 0; i < segs.length; i++) { + + // because segments in the popover are not part of a grid coordinate system, provide a hint to any + // grids that want to do drag-n-drop about which cell it came from + this.prepareHits(); + segs[i].hit = this.getCellHit(row, col); + this.releaseHits(); + + segContainer.append(segs[i].el); + } + + return content; + }, + + + // Given the events within an array of segment objects, reslice them to be in a single day + resliceDaySegs: function(segs, dayDate) { + + // build an array of the original events + var events = $.map(segs, function(seg) { + return seg.event; + }); + + var dayStart = dayDate.clone(); + var dayEnd = dayStart.clone().add(1, 'days'); + var dayRange = { start: dayStart, end: dayEnd }; + + // slice the events with a custom slicing function + segs = this.eventsToSegs( + events, + function(range) { + var seg = intersectRanges(range, dayRange); // undefind if no intersection + return seg ? [ seg ] : []; // must return an array of segments + } + ); + + // force an order because eventsToSegs doesn't guarantee one + this.sortEventSegs(segs); + + return segs; + }, + + + // Generates the text that should be inside a "more" link, given the number of events it represents + getMoreLinkText: function(num) { + var opt = this.view.opt('eventLimitText'); + + if (typeof opt === 'function') { + return opt(num); + } + else { + return '+' + num + ' ' + opt; + } + }, + + + // Returns segments within a given cell. + // If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs. + getCellSegs: function(row, col, startLevel) { + var segMatrix = this.rowStructs[row].segMatrix; + var level = startLevel || 0; + var segs = []; + var seg; + + while (level < segMatrix.length) { + seg = segMatrix[level][col]; + if (seg) { + segs.push(seg); + } + level++; + } + + return segs; + } + + }); + + ;; + + /* A component that renders one or more columns of vertical time slots + ----------------------------------------------------------------------------------------------------------------------*/ + // We mixin DayTable, even though there is only a single row of days + + var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, { + + slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines + snapDuration: null, // granularity of time for dragging and selecting + snapsPerSlot: null, + minTime: null, // Duration object that denotes the first visible time of any given day + maxTime: null, // Duration object that denotes the exclusive visible end time of any given day + labelFormat: null, // formatting string for times running along vertical axis + labelInterval: null, // duration of how often a label should be displayed for a slot + + colEls: null, // cells elements in the day-row background + slatContainerEl: null, // div that wraps all the slat rows + slatEls: null, // elements running horizontally across all columns + nowIndicatorEls: null, + + colCoordCache: null, + slatCoordCache: null, + + + constructor: function() { + Grid.apply(this, arguments); // call the super-constructor + + this.processOptions(); + }, + + + // Renders the time grid into `this.el`, which should already be assigned. + // Relies on the view's colCnt. In the future, this component should probably be self-sufficient. + renderDates: function() { + this.el.html(this.renderHtml()); + this.colEls = this.el.find('.fc-day'); + this.slatContainerEl = this.el.find('.fc-slats'); + this.slatEls = this.slatContainerEl.find('tr'); + + this.colCoordCache = new CoordCache({ + els: this.colEls, + isHorizontal: true + }); + this.slatCoordCache = new CoordCache({ + els: this.slatEls, + isVertical: true + }); + + this.renderContentSkeleton(); + }, + + + // Renders the basic HTML skeleton for the grid + renderHtml: function() { + return '' + + '
' + + '' + + this.renderBgTrHtml(0) + // row=0 + '
' + + '
' + + '
' + + '' + + this.renderSlatRowHtml() + + '
' + + '
'; + }, + + + // Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL. + renderSlatRowHtml: function() { + var view = this.view; + var isRTL = this.isRTL; + var html = ''; + var slotTime = moment.duration(+this.minTime); // wish there was .clone() for durations + var slotDate; // will be on the view's first day, but we only care about its time + var isLabeled; + var axisHtml; + + // Calculate the time for each slot + while (slotTime < this.maxTime) { + slotDate = this.start.clone().time(slotTime); + isLabeled = isInt(divideDurationByDuration(slotTime, this.labelInterval)); + + axisHtml = + '' + + (isLabeled ? + '' + // for matchCellWidths + htmlEscape(slotDate.format(this.labelFormat)) + + '' : + '' + ) + + ''; + + html += + '' + + (!isRTL ? axisHtml : '') + + '' + + (isRTL ? axisHtml : '') + + ""; + + slotTime.add(this.slotDuration); + } + + return html; + }, + + + /* Options + ------------------------------------------------------------------------------------------------------------------*/ + + + // Parses various options into properties of this object + processOptions: function() { + var view = this.view; + var slotDuration = view.opt('slotDuration'); + var snapDuration = view.opt('snapDuration'); + var input; + + slotDuration = moment.duration(slotDuration); + snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration; + + this.slotDuration = slotDuration; + this.snapDuration = snapDuration; + this.snapsPerSlot = slotDuration / snapDuration; // TODO: ensure an integer multiple? + + this.minResizeDuration = snapDuration; // hack + + this.minTime = moment.duration(view.opt('minTime')); + this.maxTime = moment.duration(view.opt('maxTime')); + + // might be an array value (for TimelineView). + // if so, getting the most granular entry (the last one probably). + input = view.opt('slotLabelFormat'); + if ($.isArray(input)) { + input = input[input.length - 1]; + } + + this.labelFormat = + input || + view.opt('axisFormat') || // deprecated + view.opt('smallTimeFormat'); // the computed default + + input = view.opt('slotLabelInterval'); + this.labelInterval = input ? + moment.duration(input) : + this.computeLabelInterval(slotDuration); + }, + + + // Computes an automatic value for slotLabelInterval + computeLabelInterval: function(slotDuration) { + var i; + var labelInterval; + var slotsPerLabel; + + // find the smallest stock label interval that results in more than one slots-per-label + for (i = AGENDA_STOCK_SUB_DURATIONS.length - 1; i >= 0; i--) { + labelInterval = moment.duration(AGENDA_STOCK_SUB_DURATIONS[i]); + slotsPerLabel = divideDurationByDuration(labelInterval, slotDuration); + if (isInt(slotsPerLabel) && slotsPerLabel > 1) { + return labelInterval; + } + } + return moment.duration(slotDuration); // fall back. clone + }, + + + // Computes a default event time formatting string if `timeFormat` is not explicitly defined + computeEventTimeFormat: function() { + return this.view.opt('noMeridiemTimeFormat'); // like "6:30" (no AM/PM) + }, - // Given segments grouped by column, insert the segments' elements into a parallel array of container - // elements, each living within a column. - attachSegsByCol: function(segsByCol, containerEls) { - var col; - var segs; - var i; - for (col = 0; col < this.colCnt; col++) { // iterate each column grouping - segs = segsByCol[col]; + // Computes a default `displayEventEnd` value if one is not expliclty defined + computeDisplayEventEnd: function() { + return true; + }, - for (i = 0; i < segs.length; i++) { - containerEls.eq(col).append(segs[i].el); - } - } - }, + /* Hit System + ------------------------------------------------------------------------------------------------------------------*/ - // Given the name of a property of `this` object, assumed to be an array of segments, - // loops through each segment and removes from DOM. Will null-out the property afterwards. - unrenderNamedSegs: function(propName) { - var segs = this[propName]; - var i; - - if (segs) { - for (i = 0; i < segs.length; i++) { - segs[i].el.remove(); - } - this[propName] = null; - } - }, - - - - /* Foreground Event Rendering Utils - ------------------------------------------------------------------------------------------------------------------*/ - - - // Given an array of foreground segments, render a DOM element for each, computes position, - // and attaches to the column inner-container elements. - renderFgSegsIntoContainers: function(segs, containerEls) { - var segsByCol; - var col; - - segs = this.renderFgSegEls(segs); // will call fgSegHtml - segsByCol = this.groupSegsByCol(segs); - - for (col = 0; col < this.colCnt; col++) { - this.updateFgSegCoords(segsByCol[col]); - } - - this.attachSegsByCol(segsByCol, containerEls); - - return segs; - }, - - - // Renders the HTML for a single event segment's default rendering - fgSegHtml: function(seg, disableResizing) { - var view = this.view; - var event = seg.event; - var isDraggable = view.isEventDraggable(event); - var isResizableFromStart = !disableResizing && seg.isStart && view.isEventResizableFromStart(event); - var isResizableFromEnd = !disableResizing && seg.isEnd && view.isEventResizableFromEnd(event); - var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd); - var skinCss = cssToStr(this.getSegSkinCss(seg)); - var timeText; - var fullTimeText; // more verbose time text. for the print stylesheet - var startTimeText; // just the start time text - - classes.unshift('fc-time-grid-event', 'fc-v-event'); - - if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day... - // Don't display time text on segments that run entirely through a day. - // That would appear as midnight-midnight and would look dumb. - // Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am) - if (seg.isStart || seg.isEnd) { - timeText = this.getEventTimeText(seg); - fullTimeText = this.getEventTimeText(seg, 'LT'); - startTimeText = this.getEventTimeText(seg, null, false); // displayEnd=false - } - } else { - // Display the normal time text for the *event's* times - timeText = this.getEventTimeText(event); - fullTimeText = this.getEventTimeText(event, 'LT'); - startTimeText = this.getEventTimeText(event, null, false); // displayEnd=false - } - - return '
' + - '
' + - (timeText ? - '
' + - '' + htmlEscape(timeText) + '' + - '
' : - '' - ) + - (event.title ? - '
' + - htmlEscape(event.title) + - '
' : - '' - ) + - '
' + - '
' + - /* TODO: write CSS for this - (isResizableFromStart ? - '
' : - '' - ) + - */ - (isResizableFromEnd ? - '
' : - '' - ) + - ''; - }, - - - /* Seg Position Utils - ------------------------------------------------------------------------------------------------------------------*/ - - - // Refreshes the CSS top/bottom coordinates for each segment element. - // Works when called after initial render, after a window resize/zoom for example. - updateSegVerticals: function(segs) { - this.computeSegVerticals(segs); - this.assignSegVerticals(segs); - }, - - - // For each segment in an array, computes and assigns its top and bottom properties - computeSegVerticals: function(segs) { - var i, seg; - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - seg.top = this.computeDateTop(seg.start, seg.start); - seg.bottom = this.computeDateTop(seg.end, seg.start); - } - }, - - - // Given segments that already have their top/bottom properties computed, applies those values to - // the segments' elements. - assignSegVerticals: function(segs) { - var i, seg; - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - seg.el.css(this.generateSegVerticalCss(seg)); - } - }, - - - // Generates an object with CSS properties for the top/bottom coordinates of a segment element - generateSegVerticalCss: function(seg) { - return { - top: seg.top, - bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container - }; - }, - - - /* Foreground Event Positioning Utils - ------------------------------------------------------------------------------------------------------------------*/ - - - // Given segments that are assumed to all live in the *same column*, - // compute their verical/horizontal coordinates and assign to their elements. - updateFgSegCoords: function(segs) { - this.computeSegVerticals(segs); // horizontals relies on this - this.computeFgSegHorizontals(segs); // compute horizontal coordinates, z-index's, and reorder the array - this.assignSegVerticals(segs); - this.assignFgSegHorizontals(segs); - }, - - - // Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each. - // NOTE: Also reorders the given array by date! - computeFgSegHorizontals: function(segs) { - var levels; - var level0; - var i; - - this.sortEventSegs(segs); // order by certain criteria - levels = buildSlotSegLevels(segs); - computeForwardSlotSegs(levels); - - if ((level0 = levels[0])) { - - for (i = 0; i < level0.length; i++) { - computeSlotSegPressures(level0[i]); - } - - for (i = 0; i < level0.length; i++) { - this.computeFgSegForwardBack(level0[i], 0, 0); - } - } - }, - - - // Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range - // from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and - // seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left. - // - // The segment might be part of a "series", which means consecutive segments with the same pressure - // who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of - // segments behind this one in the current series, and `seriesBackwardCoord` is the starting - // coordinate of the first segment in the series. - computeFgSegForwardBack: function(seg, seriesBackwardPressure, seriesBackwardCoord) { - var forwardSegs = seg.forwardSegs; - var i; - - if (seg.forwardCoord === undefined) { // not already computed - - if (!forwardSegs.length) { - - // if there are no forward segments, this segment should butt up against the edge - seg.forwardCoord = 1; - } - else { - - // sort highest pressure first - this.sortForwardSegs(forwardSegs); - - // this segment's forwardCoord will be calculated from the backwardCoord of the - // highest-pressure forward segment. - this.computeFgSegForwardBack(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord); - seg.forwardCoord = forwardSegs[0].backwardCoord; - } - - // calculate the backwardCoord from the forwardCoord. consider the series - seg.backwardCoord = seg.forwardCoord - - (seg.forwardCoord - seriesBackwardCoord) / // available width for series - (seriesBackwardPressure + 1); // # of segments in the series - - // use this segment's coordinates to computed the coordinates of the less-pressurized - // forward segments - for (i=0; i seg2.top && seg1.top < seg2.bottom; -} + sliceRangeByTimes: function(range) { + var segs = []; + var seg; + var dayIndex; + var dayDate; + var dayRange; -;; + for (dayIndex = 0; dayIndex < this.daysPerRow; dayIndex++) { + dayDate = this.dayDates[dayIndex].clone(); // TODO: better API for this? + dayRange = { + start: dayDate.clone().time(this.minTime), + end: dayDate.clone().time(this.maxTime) + }; + seg = intersectRanges(range, dayRange); // both will be ambig timezone + if (seg) { + seg.dayIndex = dayIndex; + segs.push(seg); + } + } -/* An abstract class from which other views inherit from -----------------------------------------------------------------------------------------------------------------------*/ + return segs; + }, -var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, { - type: null, // subclass' view name (string) - name: null, // deprecated. use `type` instead - title: null, // the text that will be displayed in the header's title + /* Coordinates + ------------------------------------------------------------------------------------------------------------------*/ - calendar: null, // owner Calendar object - options: null, // hash containing all options. already merged with view-specific-options - el: null, // the view's containing element. set by Calendar - displaying: null, // a promise representing the state of rendering. null if no render requested - isSkeletonRendered: false, - isEventsRendered: false, + updateSize: function(isResize) { // NOT a standard Grid method + this.slatCoordCache.build(); - // range the view is actually displaying (moments) - start: null, - end: null, // exclusive + if (isResize) { + this.updateSegVerticals( + [].concat(this.fgSegs || [], this.bgSegs || [], this.businessSegs || []) + ); + } + }, - // range the view is formally responsible for (moments) - // may be different from start/end. for example, a month view might have 1st-31st, excluding padded dates - intervalStart: null, - intervalEnd: null, // exclusive - intervalDuration: null, - intervalUnit: null, // name of largest unit being displayed, like "month" or "week" - isRTL: false, - isSelected: false, // boolean whether a range of time is user-selected or not - selectedEvent: null, + getTotalSlatHeight: function() { + return this.slatContainerEl.outerHeight(); + }, - eventOrderSpecs: null, // criteria for ordering events when they have same date/time - // classNames styled by jqui themes - widgetHeaderClass: null, - widgetContentClass: null, - highlightStateClass: null, + // Computes the top coordinate, relative to the bounds of the grid, of the given date. + // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight. + computeDateTop: function(date, startOfDayDate) { + return this.computeTimeTop( + moment.duration( + date - startOfDayDate.clone().stripTime() + ) + ); + }, - // for date utils, computed from options - nextDayThreshold: null, - isHiddenDayHash: null, - // now indicator - isNowIndicatorRendered: null, - initialNowDate: null, // result first getNow call - initialNowQueriedMs: null, // ms time the getNow was called - nowIndicatorTimeoutID: null, // for refresh timing of now indicator - nowIndicatorIntervalID: null, // " + // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration). + computeTimeTop: function(time) { + var len = this.slatEls.length; + var slatCoverage = (time - this.minTime) / this.slotDuration; // floating-point value of # of slots covered + var slatIndex; + var slatRemainder; + // compute a floating-point number for how many slats should be progressed through. + // from 0 to number of slats (inclusive) + // constrained because minTime/maxTime might be customized. + slatCoverage = Math.max(0, slatCoverage); + slatCoverage = Math.min(len, slatCoverage); - constructor: function(calendar, type, options, intervalDuration) { + // an integer index of the furthest whole slat + // from 0 to number slats (*exclusive*, so len-1) + slatIndex = Math.floor(slatCoverage); + slatIndex = Math.min(slatIndex, len - 1); - this.calendar = calendar; - this.type = this.name = type; // .name is deprecated - this.options = options; - this.intervalDuration = intervalDuration || moment.duration(1, 'day'); + // how much further through the slatIndex slat (from 0.0-1.0) must be covered in addition. + // could be 1.0 if slatCoverage is covering *all* the slots + slatRemainder = slatCoverage - slatIndex; - this.nextDayThreshold = moment.duration(this.opt('nextDayThreshold')); - this.initThemingProps(); - this.initHiddenDays(); - this.isRTL = this.opt('isRTL'); + return this.slatCoordCache.getTopPosition(slatIndex) + + this.slatCoordCache.getHeight(slatIndex) * slatRemainder; + }, - this.eventOrderSpecs = parseFieldSpecs(this.opt('eventOrder')); - this.initialize(); - }, + /* Event Drag Visualization + ------------------------------------------------------------------------------------------------------------------*/ - // A good place for subclasses to initialize member variables - initialize: function() { - // subclasses can implement - }, + // Renders a visual indication of an event being dragged over the specified date(s). + // A returned value of `true` signals that a mock "helper" event has been rendered. + renderDrag: function(eventLocation, seg) { - // Retrieves an option with the given name - opt: function(name) { - return this.options[name]; - }, + if (seg) { // if there is event information for this drag, render a helper event + // returns mock event elements + // signal that a helper has been rendered + return this.renderEventLocationHelper(eventLocation, seg); + } + else { + // otherwise, just render a highlight + this.renderHighlight(this.eventToSpan(eventLocation)); + } + }, - // Triggers handlers that are view-related. Modifies args before passing to calendar. - trigger: function(name, thisObj) { // arguments beyond thisObj are passed along - var calendar = this.calendar; - return calendar.trigger.apply( - calendar, - [name, thisObj || this].concat( - Array.prototype.slice.call(arguments, 2), // arguments beyond thisObj - [ this ] // always make the last argument a reference to the view. TODO: deprecate - ) - ); - }, + // Unrenders any visual indication of an event being dragged + unrenderDrag: function() { + this.unrenderHelper(); + this.unrenderHighlight(); + }, - /* Dates - ------------------------------------------------------------------------------------------------------------------*/ + /* Event Resize Visualization + ------------------------------------------------------------------------------------------------------------------*/ - // Updates all internal dates to center around the given current unzoned date. - setDate: function(date) { - this.setRange(this.computeRange(date)); - }, - - - // Updates all internal dates for displaying the given unzoned range. - setRange: function(range) { - $.extend(this, range); // assigns every property to this object's member variables - this.updateTitle(); - }, - - - // Given a single current unzoned date, produce information about what range to display. - // Subclasses can override. Must return all properties. - computeRange: function(date) { - var intervalUnit = computeIntervalUnit(this.intervalDuration); - var intervalStart = date.clone().startOf(intervalUnit); - var intervalEnd = intervalStart.clone().add(this.intervalDuration); - var start, end; - - // normalize the range's time-ambiguity - if (/year|month|week|day/.test(intervalUnit)) { // whole-days? - intervalStart.stripTime(); - intervalEnd.stripTime(); - } - else { // needs to have a time? - if (!intervalStart.hasTime()) { - intervalStart = this.calendar.time(0); // give 00:00 time - } - if (!intervalEnd.hasTime()) { - intervalEnd = this.calendar.time(0); // give 00:00 time - } - } - - start = intervalStart.clone(); - start = this.skipHiddenDays(start); - end = intervalEnd.clone(); - end = this.skipHiddenDays(end, -1, true); // exclusively move backwards - - return { - intervalUnit: intervalUnit, - intervalStart: intervalStart, - intervalEnd: intervalEnd, - start: start, - end: end - }; - }, - - - // Computes the new date when the user hits the prev button, given the current date - computePrevDate: function(date) { - return this.massageCurrentDate( - date.clone().startOf(this.intervalUnit).subtract(this.intervalDuration), -1 - ); - }, - - - // Computes the new date when the user hits the next button, given the current date - computeNextDate: function(date) { - return this.massageCurrentDate( - date.clone().startOf(this.intervalUnit).add(this.intervalDuration) - ); - }, - - - // Given an arbitrarily calculated current date of the calendar, returns a date that is ensured to be completely - // visible. `direction` is optional and indicates which direction the current date was being - // incremented or decremented (1 or -1). - massageCurrentDate: function(date, direction) { - if (this.intervalDuration.as('days') <= 1) { // if the view displays a single day or smaller - if (this.isHiddenDay(date)) { - date = this.skipHiddenDays(date, direction); - date.startOf('day'); - } - } - - return date; - }, - - - /* Title and Date Formatting - ------------------------------------------------------------------------------------------------------------------*/ - - - // Sets the view's title property to the most updated computed value - updateTitle: function() { - this.title = this.computeTitle(); - }, - - - // Computes what the title at the top of the calendar should be for this view - computeTitle: function() { - return this.formatRange( - { - // in case intervalStart/End has a time, make sure timezone is correct - start: this.calendar.applyTimezone(this.intervalStart), - end: this.calendar.applyTimezone(this.intervalEnd) - }, - this.opt('titleFormat') || this.computeTitleFormat(), - this.opt('titleRangeSeparator') - ); - }, - - - // Generates the format string that should be used to generate the title for the current date range. - // Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`. - computeTitleFormat: function() { - if (this.intervalUnit == 'year') { - return 'YYYY'; - } - else if (this.intervalUnit == 'month') { - return this.opt('monthYearFormat'); // like "September 2014" - } - else if (this.intervalDuration.as('days') > 1) { - return 'll'; // multi-day range. shorter, like "Sep 9 - 10 2014" - } - else { - return 'LL'; // one day. longer, like "September 9 2014" - } - }, - - - // Utility for formatting a range. Accepts a range object, formatting string, and optional separator. - // Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account. - // The timezones of the dates within `range` will be respected. - formatRange: function(range, formatStr, separator) { - var end = range.end; - - if (!end.hasTime()) { // all-day? - end = end.clone().subtract(1); // convert to inclusive. last ms of previous day - } - - return formatRange(range.start, end, formatStr, separator, this.opt('isRTL')); - }, - - - /* Rendering - ------------------------------------------------------------------------------------------------------------------*/ - - - // Sets the container element that the view should render inside of. - // Does other DOM-related initializations. - setElement: function(el) { - this.el = el; - this.bindGlobalHandlers(); - }, - - - // Removes the view's container element from the DOM, clearing any content beforehand. - // Undoes any other DOM-related attachments. - removeElement: function() { - this.clear(); // clears all content - - // clean up the skeleton - if (this.isSkeletonRendered) { - this.unrenderSkeleton(); - this.isSkeletonRendered = false; - } - - this.unbindGlobalHandlers(); - - this.el.remove(); - - // NOTE: don't null-out this.el in case the View was destroyed within an API callback. - // We don't null-out the View's other jQuery element references upon destroy, - // so we shouldn't kill this.el either. - }, - - - // Does everything necessary to display the view centered around the given unzoned date. - // Does every type of rendering EXCEPT rendering events. - // Is asychronous and returns a promise. - display: function(date) { - var _this = this; - var scrollState = null; - - if (this.displaying) { - scrollState = this.queryScroll(); - } - - this.calendar.freezeContentHeight(); - - return this.clear().then(function() { // clear the content first (async) - return ( - _this.displaying = - $.when(_this.displayView(date)) // displayView might return a promise - .then(function() { - _this.forceScroll(_this.computeInitialScroll(scrollState)); - _this.calendar.unfreezeContentHeight(); - _this.triggerRender(); - }) - ); - }); - }, - - - // Does everything necessary to clear the content of the view. - // Clears dates and events. Does not clear the skeleton. - // Is asychronous and returns a promise. - clear: function() { - var _this = this; - var displaying = this.displaying; - - if (displaying) { // previously displayed, or in the process of being displayed? - return displaying.then(function() { // wait for the display to finish - _this.displaying = null; - _this.clearEvents(); - return _this.clearView(); // might return a promise. chain it - }); - } - else { - return $.when(); // an immediately-resolved promise - } - }, - - - // Displays the view's non-event content, such as date-related content or anything required by events. - // Renders the view's non-content skeleton if necessary. - // Can be asynchronous and return a promise. - displayView: function(date) { - if (!this.isSkeletonRendered) { - this.renderSkeleton(); - this.isSkeletonRendered = true; - } - if (date) { - this.setDate(date); - } - if (this.render) { - this.render(); // TODO: deprecate - } - this.renderDates(); - this.updateSize(); - this.renderBusinessHours(); // might need coordinates, so should go after updateSize() - this.startNowIndicator(); - }, - - - // Unrenders the view content that was rendered in displayView. - // Can be asynchronous and return a promise. - clearView: function() { - this.unselect(); - this.stopNowIndicator(); - this.triggerUnrender(); - this.unrenderBusinessHours(); - this.unrenderDates(); - if (this.destroy) { - this.destroy(); // TODO: deprecate - } - }, - - - // Renders the basic structure of the view before any content is rendered - renderSkeleton: function() { - // subclasses should implement - }, - - - // Unrenders the basic structure of the view - unrenderSkeleton: function() { - // subclasses should implement - }, - - - // Renders the view's date-related content. - // Assumes setRange has already been called and the skeleton has already been rendered. - renderDates: function() { - // subclasses should implement - }, - - - // Unrenders the view's date-related content - unrenderDates: function() { - // subclasses should override - }, - - - // Signals that the view's content has been rendered - triggerRender: function() { - this.trigger('viewRender', this, this, this.el); - }, - - - // Signals that the view's content is about to be unrendered - triggerUnrender: function() { - this.trigger('viewDestroy', this, this, this.el); - }, - - - // Binds DOM handlers to elements that reside outside the view container, such as the document - bindGlobalHandlers: function() { - this.listenTo($(document), 'mousedown', this.handleDocumentMousedown); - this.listenTo($(document), 'touchstart', this.handleDocumentTouchStart); - this.listenTo($(document), 'touchend', this.handleDocumentTouchEnd); - }, - - - // Unbinds DOM handlers from elements that reside outside the view container - unbindGlobalHandlers: function() { - this.stopListeningTo($(document)); - }, - - - // Initializes internal variables related to theming - initThemingProps: function() { - var tm = this.opt('theme') ? 'ui' : 'fc'; - - this.widgetHeaderClass = tm + '-widget-header'; - this.widgetContentClass = tm + '-widget-content'; - this.highlightStateClass = tm + '-state-highlight'; - }, - - - /* Business Hours - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders business-hours onto the view. Assumes updateSize has already been called. - renderBusinessHours: function() { - // subclasses should implement - }, + // Renders a visual indication of an event being resized + renderEventResize: function(eventLocation, seg) { + return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements + }, - // Unrenders previously-rendered business-hours - unrenderBusinessHours: function() { - // subclasses should implement - }, + // Unrenders any visual indication of an event being resized + unrenderEventResize: function() { + this.unrenderHelper(); + }, - /* Now Indicator - ------------------------------------------------------------------------------------------------------------------*/ + /* Event Helper + ------------------------------------------------------------------------------------------------------------------*/ - // Immediately render the current time indicator and begins re-rendering it at an interval, - // which is defined by this.getNowIndicatorUnit(). - // TODO: somehow do this for the current whole day's background too - startNowIndicator: function() { - var _this = this; - var unit; - var update; - var delay; // ms wait value + // Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag) + renderHelper: function(event, sourceSeg) { + return this.renderHelperSegs(this.eventToSegs(event), sourceSeg); // returns mock event elements + }, - if (this.opt('nowIndicator')) { - unit = this.getNowIndicatorUnit(); - if (unit) { - update = proxy(this, 'updateNowIndicator'); // bind to `this` - this.initialNowDate = this.calendar.getNow(); - this.initialNowQueriedMs = +new Date(); - this.renderNowIndicator(this.initialNowDate); - this.isNowIndicatorRendered = true; + // Unrenders any mock helper event + unrenderHelper: function() { + this.unrenderHelperSegs(); + }, - // wait until the beginning of the next interval - delay = this.initialNowDate.clone().startOf(unit).add(1, unit) - this.initialNowDate; - this.nowIndicatorTimeoutID = setTimeout(function() { - _this.nowIndicatorTimeoutID = null; - update(); - delay = +moment.duration(1, unit); - delay = Math.max(100, delay); // prevent too frequent - _this.nowIndicatorIntervalID = setInterval(update, delay); // update every interval - }, delay); - } - } - }, + /* Business Hours + ------------------------------------------------------------------------------------------------------------------*/ - // rerenders the now indicator, computing the new current time from the amount of time that has passed - // since the initial getNow call. - updateNowIndicator: function() { - if (this.isNowIndicatorRendered) { - this.unrenderNowIndicator(); - this.renderNowIndicator( - this.initialNowDate.clone().add(new Date() - this.initialNowQueriedMs) // add ms - ); - } - }, + renderBusinessHours: function() { + var events = this.view.calendar.getBusinessHoursEvents(); + var segs = this.eventsToSegs(events); - // Immediately unrenders the view's current time indicator and stops any re-rendering timers. - // Won't cause side effects if indicator isn't rendered. - stopNowIndicator: function() { - if (this.isNowIndicatorRendered) { + this.renderBusinessSegs(segs); + }, - if (this.nowIndicatorTimeoutID) { - clearTimeout(this.nowIndicatorTimeoutID); - this.nowIndicatorTimeoutID = null; - } - if (this.nowIndicatorIntervalID) { - clearTimeout(this.nowIndicatorIntervalID); - this.nowIndicatorIntervalID = null; - } - this.unrenderNowIndicator(); - this.isNowIndicatorRendered = false; - } - }, + unrenderBusinessHours: function() { + this.unrenderBusinessSegs(); + }, - // Returns a string unit, like 'second' or 'minute' that defined how often the current time indicator - // should be refreshed. If something falsy is returned, no time indicator is rendered at all. - getNowIndicatorUnit: function() { - // subclasses should implement - }, + /* Now Indicator + ------------------------------------------------------------------------------------------------------------------*/ - // Renders a current time indicator at the given datetime - renderNowIndicator: function(date) { - // subclasses should implement - }, + getNowIndicatorUnit: function() { + return 'minute'; // will refresh on the minute + }, - // Undoes the rendering actions from renderNowIndicator - unrenderNowIndicator: function() { - // subclasses should implement - }, + renderNowIndicator: function(date) { + // seg system might be overkill, but it handles scenario where line needs to be rendered + // more than once because of columns with the same date (resources columns for example) + var segs = this.spanToSegs({ start: date, end: date }); + var top = this.computeDateTop(date, date); + var nodes = []; + var i; + // render lines within the columns + for (i = 0; i < segs.length; i++) { + nodes.push($('
') + .css('top', top) + .appendTo(this.colContainerEls.eq(segs[i].col))[0]); + } - /* Dimensions - ------------------------------------------------------------------------------------------------------------------*/ + // render an arrow over the axis + if (segs.length > 0) { // is the current time in view? + nodes.push($('
') + .css('top', top) + .appendTo(this.el.find('.fc-content-skeleton'))[0]); + } + this.nowIndicatorEls = $(nodes); + }, - // Refreshes anything dependant upon sizing of the container element of the grid - updateSize: function(isResize) { - var scrollState; - if (isResize) { - scrollState = this.queryScroll(); - } + unrenderNowIndicator: function() { + if (this.nowIndicatorEls) { + this.nowIndicatorEls.remove(); + this.nowIndicatorEls = null; + } + }, - this.updateHeight(isResize); - this.updateWidth(isResize); - this.updateNowIndicator(); - if (isResize) { - this.setScroll(scrollState); - } - }, + /* Selection + ------------------------------------------------------------------------------------------------------------------*/ - // Refreshes the horizontal dimensions of the calendar - updateWidth: function(isResize) { - // subclasses should implement - }, + // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight. + renderSelection: function(span) { + if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered + // normally acceps an eventLocation, span has a start/end, which is good enough + this.renderEventLocationHelper(span); + } + else { + this.renderHighlight(span); + } + }, - // Refreshes the vertical dimensions of the calendar - updateHeight: function(isResize) { - var calendar = this.calendar; // we poll the calendar for height information - this.setHeight( - calendar.getSuggestedViewHeight(), - calendar.isHeightAuto() - ); - }, + // Unrenders any visual indication of a selection + unrenderSelection: function() { + this.unrenderHelper(); + this.unrenderHighlight(); + }, - // Updates the vertical dimensions of the calendar to the specified height. - // if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height. - setHeight: function(height, isAuto) { - // subclasses should implement - }, + /* Highlight + ------------------------------------------------------------------------------------------------------------------*/ - /* Scroller - ------------------------------------------------------------------------------------------------------------------*/ + renderHighlight: function(span) { + this.renderHighlightSegs(this.spanToSegs(span)); + }, - // Computes the initial pre-configured scroll state prior to allowing the user to change it. - // Given the scroll state from the previous rendering. If first time rendering, given null. - computeInitialScroll: function(previousScrollState) { - return 0; - }, + unrenderHighlight: function() { + this.unrenderHighlightSegs(); + } + }); - // Retrieves the view's current natural scroll state. Can return an arbitrary format. - queryScroll: function() { - // subclasses must implement - }, + ;; + /* Methods for rendering SEGMENTS, pieces of content that live on the view + ( this file is no longer just for events ) + ----------------------------------------------------------------------------------------------------------------------*/ - // Sets the view's scroll state. Will accept the same format computeInitialScroll and queryScroll produce. - setScroll: function(scrollState) { - // subclasses must implement - }, + TimeGrid.mixin({ + colContainerEls: null, // containers for each column - // Sets the scroll state, making sure to overcome any predefined scroll value the browser has in mind - forceScroll: function(scrollState) { - var _this = this; + // inner-containers for each column where different types of segs live + fgContainerEls: null, + bgContainerEls: null, + helperContainerEls: null, + highlightContainerEls: null, + businessContainerEls: null, - this.setScroll(scrollState); - setTimeout(function() { - _this.setScroll(scrollState); - }, 0); - }, + // arrays of different types of displayed segments + fgSegs: null, + bgSegs: null, + helperSegs: null, + highlightSegs: null, + businessSegs: null, - /* Event Elements / Segments - ------------------------------------------------------------------------------------------------------------------*/ + // Renders the DOM that the view's content will live in + renderContentSkeleton: function() { + var cellHtml = ''; + var i; + var skeletonEl; + for (i = 0; i < this.colCnt; i++) { + cellHtml += + '' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + ''; + } - // Does everything necessary to display the given events onto the current view - displayEvents: function(events) { - var scrollState = this.queryScroll(); + skeletonEl = $( + '
' + + '' + + '' + cellHtml + '' + + '
' + + '
' + ); - this.clearEvents(); - this.renderEvents(events); - this.isEventsRendered = true; - this.setScroll(scrollState); - this.triggerEventRender(); - }, + this.colContainerEls = skeletonEl.find('.fc-content-col'); + this.helperContainerEls = skeletonEl.find('.fc-helper-container'); + this.fgContainerEls = skeletonEl.find('.fc-event-container:not(.fc-helper-container)'); + this.bgContainerEls = skeletonEl.find('.fc-bgevent-container'); + this.highlightContainerEls = skeletonEl.find('.fc-highlight-container'); + this.businessContainerEls = skeletonEl.find('.fc-business-container'); + this.bookendCells(skeletonEl.find('tr')); // TODO: do this on string level + this.el.append(skeletonEl); + }, - // Does everything necessary to clear the view's currently-rendered events - clearEvents: function() { - var scrollState; - if (this.isEventsRendered) { + /* Foreground Events + ------------------------------------------------------------------------------------------------------------------*/ - // TODO: optimize: if we know this is part of a displayEvents call, don't queryScroll/setScroll - scrollState = this.queryScroll(); - this.triggerEventUnrender(); - if (this.destroyEvents) { - this.destroyEvents(); // TODO: deprecate - } - this.unrenderEvents(); - this.setScroll(scrollState); - this.isEventsRendered = false; - } - }, + renderFgSegs: function(segs) { + segs = this.renderFgSegsIntoContainers(segs, this.fgContainerEls); + this.fgSegs = segs; + return segs; // needed for Grid::renderEvents + }, - // Renders the events onto the view. - renderEvents: function(events) { - // subclasses should implement - }, + unrenderFgSegs: function() { + this.unrenderNamedSegs('fgSegs'); + }, - // Removes event elements from the view. - unrenderEvents: function() { - // subclasses should implement - }, + /* Foreground Helper Events + ------------------------------------------------------------------------------------------------------------------*/ - // Signals that all events have been rendered - triggerEventRender: function() { - this.renderedEventSegEach(function(seg) { - this.trigger('eventAfterRender', seg.event, seg.event, seg.el); - }); - this.trigger('eventAfterAllRender'); - }, + renderHelperSegs: function(segs, sourceSeg) { + var helperEls = []; + var i, seg; + var sourceEl; + segs = this.renderFgSegsIntoContainers(segs, this.helperContainerEls); - // Signals that all event elements are about to be removed - triggerEventUnrender: function() { - this.renderedEventSegEach(function(seg) { - this.trigger('eventDestroy', seg.event, seg.event, seg.el); - }); - }, + // Try to make the segment that is in the same row as sourceSeg look the same + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + if (sourceSeg && sourceSeg.col === seg.col) { + sourceEl = sourceSeg.el; + seg.el.css({ + left: sourceEl.css('left'), + right: sourceEl.css('right'), + 'margin-left': sourceEl.css('margin-left'), + 'margin-right': sourceEl.css('margin-right') + }); + } + helperEls.push(seg.el[0]); + } + this.helperSegs = segs; - // Given an event and the default element used for rendering, returns the element that should actually be used. - // Basically runs events and elements through the eventRender hook. - resolveEventEl: function(event, el) { - var custom = this.trigger('eventRender', event, event, el); + return $(helperEls); // must return rendered helpers + }, - if (custom === false) { // means don't render at all - el = null; - } - else if (custom && custom !== true) { - el = $(custom); - } - return el; - }, + unrenderHelperSegs: function() { + this.unrenderNamedSegs('helperSegs'); + }, - // Hides all rendered event segments linked to the given event - showEvent: function(event) { - this.renderedEventSegEach(function(seg) { - seg.el.css('visibility', ''); - }, event); - }, + /* Background Events + ------------------------------------------------------------------------------------------------------------------*/ - // Shows all rendered event segments linked to the given event - hideEvent: function(event) { - this.renderedEventSegEach(function(seg) { - seg.el.css('visibility', 'hidden'); - }, event); - }, + renderBgSegs: function(segs) { + segs = this.renderFillSegEls('bgEvent', segs); // TODO: old fill system + this.updateSegVerticals(segs); + this.attachSegsByCol(this.groupSegsByCol(segs), this.bgContainerEls); + this.bgSegs = segs; + return segs; // needed for Grid::renderEvents + }, - // Iterates through event segments that have been rendered (have an el). Goes through all by default. - // If the optional `event` argument is specified, only iterates through segments linked to that event. - // The `this` value of the callback function will be the view. - renderedEventSegEach: function(func, event) { - var segs = this.getEventSegs(); - var i; + unrenderBgSegs: function() { + this.unrenderNamedSegs('bgSegs'); + }, - for (i = 0; i < segs.length; i++) { - if (!event || segs[i].event._id === event._id) { - if (segs[i].el) { - func.call(this, segs[i]); - } - } - } - }, + /* Highlight + ------------------------------------------------------------------------------------------------------------------*/ - // Retrieves all the rendered segment objects for the view - getEventSegs: function() { - // subclasses must implement - return []; - }, + renderHighlightSegs: function(segs) { + segs = this.renderFillSegEls('highlight', segs); // TODO: old fill system + this.updateSegVerticals(segs); + this.attachSegsByCol(this.groupSegsByCol(segs), this.highlightContainerEls); + this.highlightSegs = segs; + }, - /* Event Drag-n-Drop - ------------------------------------------------------------------------------------------------------------------*/ + unrenderHighlightSegs: function() { + this.unrenderNamedSegs('highlightSegs'); + }, - // Computes if the given event is allowed to be dragged by the user - isEventDraggable: function(event) { - var source = event.source || {}; - return firstDefined( - event.startEditable, - source.startEditable, - this.opt('eventStartEditable'), - event.editable, - source.editable, - this.opt('editable') - ); - }, + /* Business Hours + ------------------------------------------------------------------------------------------------------------------*/ - // Must be called when an event in the view is dropped onto new location. - // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event. - reportEventDrop: function(event, dropLocation, largeUnit, el, ev) { - var calendar = this.calendar; - var mutateResult = calendar.mutateEvent(event, dropLocation, largeUnit); - var undoFunc = function() { - mutateResult.undo(); - calendar.reportEventChange(); - }; + renderBusinessSegs: function(segs) { + segs = this.renderFillSegEls('businessHours', segs); // TODO: old fill system + this.updateSegVerticals(segs); + this.attachSegsByCol(this.groupSegsByCol(segs), this.businessContainerEls); + this.businessSegs = segs; + }, - this.triggerEventDrop(event, mutateResult.dateDelta, undoFunc, el, ev); - calendar.reportEventChange(); // will rerender events - }, + unrenderBusinessSegs: function() { + this.unrenderNamedSegs('businessSegs'); + }, - // Triggers event-drop handlers that have subscribed via the API - triggerEventDrop: function(event, dateDelta, undoFunc, el, ev) { - this.trigger('eventDrop', el[0], event, dateDelta, undoFunc, ev, {}); // {} = jqui dummy - }, + /* Seg Rendering Utils + ------------------------------------------------------------------------------------------------------------------*/ - /* External Element Drag-n-Drop - ------------------------------------------------------------------------------------------------------------------*/ + // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col + groupSegsByCol: function(segs) { + var segsByCol = []; + var i; - // Must be called when an external element, via jQuery UI, has been dropped onto the calendar. - // `meta` is the parsed data that has been embedded into the dragging event. - // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event. - reportExternalDrop: function(meta, dropLocation, el, ev, ui) { - var eventProps = meta.eventProps; - var eventInput; - var event; + for (i = 0; i < this.colCnt; i++) { + segsByCol.push([]); + } - // Try to build an event object and render it. TODO: decouple the two - if (eventProps) { - eventInput = $.extend({}, eventProps, dropLocation); - event = this.calendar.renderEvent(eventInput, meta.stick)[0]; // renderEvent returns an array - } + for (i = 0; i < segs.length; i++) { + segsByCol[segs[i].col].push(segs[i]); + } - this.triggerExternalDrop(event, dropLocation, el, ev, ui); - }, + return segsByCol; + }, - // Triggers external-drop handlers that have subscribed via the API - triggerExternalDrop: function(event, dropLocation, el, ev, ui) { + // Given segments grouped by column, insert the segments' elements into a parallel array of container + // elements, each living within a column. + attachSegsByCol: function(segsByCol, containerEls) { + var col; + var segs; + var i; - // trigger 'drop' regardless of whether element represents an event - this.trigger('drop', el[0], dropLocation.start, ev, ui); + for (col = 0; col < this.colCnt; col++) { // iterate each column grouping + segs = segsByCol[col]; - if (event) { - this.trigger('eventReceive', null, event); // signal an external event landed - } - }, + for (i = 0; i < segs.length; i++) { + containerEls.eq(col).append(segs[i].el); + } + } + }, - /* Drag-n-Drop Rendering (for both events and external elements) - ------------------------------------------------------------------------------------------------------------------*/ + // Given the name of a property of `this` object, assumed to be an array of segments, + // loops through each segment and removes from DOM. Will null-out the property afterwards. + unrenderNamedSegs: function(propName) { + var segs = this[propName]; + var i; + + if (segs) { + for (i = 0; i < segs.length; i++) { + segs[i].el.remove(); + } + this[propName] = null; + } + }, + + + + /* Foreground Event Rendering Utils + ------------------------------------------------------------------------------------------------------------------*/ + + + // Given an array of foreground segments, render a DOM element for each, computes position, + // and attaches to the column inner-container elements. + renderFgSegsIntoContainers: function(segs, containerEls) { + var segsByCol; + var col; + + segs = this.renderFgSegEls(segs); // will call fgSegHtml + segsByCol = this.groupSegsByCol(segs); + + for (col = 0; col < this.colCnt; col++) { + this.updateFgSegCoords(segsByCol[col]); + } + + this.attachSegsByCol(segsByCol, containerEls); + + return segs; + }, + + + // Renders the HTML for a single event segment's default rendering + fgSegHtml: function(seg, disableResizing) { + var view = this.view; + var event = seg.event; + var isDraggable = view.isEventDraggable(event); + var isResizableFromStart = !disableResizing && seg.isStart && view.isEventResizableFromStart(event); + var isResizableFromEnd = !disableResizing && seg.isEnd && view.isEventResizableFromEnd(event); + var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd); + var skinCss = cssToStr(this.getSegSkinCss(seg)); + var timeText; + var fullTimeText; // more verbose time text. for the print stylesheet + var startTimeText; // just the start time text + + classes.unshift('fc-time-grid-event', 'fc-v-event'); + + if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day... + // Don't display time text on segments that run entirely through a day. + // That would appear as midnight-midnight and would look dumb. + // Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am) + if (seg.isStart || seg.isEnd) { + timeText = this.getEventTimeText(seg); + fullTimeText = this.getEventTimeText(seg, 'LT'); + startTimeText = this.getEventTimeText(seg, null, false); // displayEnd=false + } + } else { + // Display the normal time text for the *event's* times + timeText = this.getEventTimeText(event); + fullTimeText = this.getEventTimeText(event, 'LT'); + startTimeText = this.getEventTimeText(event, null, false); // displayEnd=false + } + + return '' + + '
' + + (timeText ? + '
' + + '' + htmlEscape(timeText) + '' + + '
' : + '' + ) + + (event.title ? + '
' + + htmlEscape(event.title) + + '
' : + '' + ) + + '
' + + '
' + + /* TODO: write CSS for this + (isResizableFromStart ? + '
' : + '' + ) + + */ + (isResizableFromEnd ? + '
' : + '' + ) + + ''; + }, + + + /* Seg Position Utils + ------------------------------------------------------------------------------------------------------------------*/ + + + // Refreshes the CSS top/bottom coordinates for each segment element. + // Works when called after initial render, after a window resize/zoom for example. + updateSegVerticals: function(segs) { + this.computeSegVerticals(segs); + this.assignSegVerticals(segs); + }, + + + // For each segment in an array, computes and assigns its top and bottom properties + computeSegVerticals: function(segs) { + var i, seg; + + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + seg.top = this.computeDateTop(seg.start, seg.start); + seg.bottom = this.computeDateTop(seg.end, seg.start); + } + }, + + + // Given segments that already have their top/bottom properties computed, applies those values to + // the segments' elements. + assignSegVerticals: function(segs) { + var i, seg; + + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + seg.el.css(this.generateSegVerticalCss(seg)); + } + }, + + + // Generates an object with CSS properties for the top/bottom coordinates of a segment element + generateSegVerticalCss: function(seg) { + return { + top: seg.top, + bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container + }; + }, + + + /* Foreground Event Positioning Utils + ------------------------------------------------------------------------------------------------------------------*/ + + + // Given segments that are assumed to all live in the *same column*, + // compute their verical/horizontal coordinates and assign to their elements. + updateFgSegCoords: function(segs) { + this.computeSegVerticals(segs); // horizontals relies on this + this.computeFgSegHorizontals(segs); // compute horizontal coordinates, z-index's, and reorder the array + this.assignSegVerticals(segs); + this.assignFgSegHorizontals(segs); + }, + + + // Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each. + // NOTE: Also reorders the given array by date! + computeFgSegHorizontals: function(segs) { + var levels; + var level0; + var i; + + this.sortEventSegs(segs); // order by certain criteria + levels = buildSlotSegLevels(segs); + computeForwardSlotSegs(levels); + + if ((level0 = levels[0])) { + + for (i = 0; i < level0.length; i++) { + computeSlotSegPressures(level0[i]); + } + + for (i = 0; i < level0.length; i++) { + this.computeFgSegForwardBack(level0[i], 0, 0); + } + } + }, + + + // Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range + // from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and + // seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left. + // + // The segment might be part of a "series", which means consecutive segments with the same pressure + // who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of + // segments behind this one in the current series, and `seriesBackwardCoord` is the starting + // coordinate of the first segment in the series. + computeFgSegForwardBack: function(seg, seriesBackwardPressure, seriesBackwardCoord) { + var forwardSegs = seg.forwardSegs; + var i; + + if (seg.forwardCoord === undefined) { // not already computed + + if (!forwardSegs.length) { + + // if there are no forward segments, this segment should butt up against the edge + seg.forwardCoord = 1; + } + else { + + // sort highest pressure first + this.sortForwardSegs(forwardSegs); + + // this segment's forwardCoord will be calculated from the backwardCoord of the + // highest-pressure forward segment. + this.computeFgSegForwardBack(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord); + seg.forwardCoord = forwardSegs[0].backwardCoord; + } + + // calculate the backwardCoord from the forwardCoord. consider the series + seg.backwardCoord = seg.forwardCoord - + (seg.forwardCoord - seriesBackwardCoord) / // available width for series + (seriesBackwardPressure + 1); // # of segments in the series + + // use this segment's coordinates to computed the coordinates of the less-pressurized + // forward segments + for (i=0; i seg2.top && seg1.top < seg2.bottom; + } - // Computes if the given event is allowed to be resized by the user at all - isEventResizable: function(event) { - var source = event.source || {}; + ;; - return firstDefined( - event.durationEditable, - source.durationEditable, - this.opt('eventDurationEditable'), - event.editable, - source.editable, - this.opt('editable') - ); - }, + /* An abstract class from which other views inherit from + ----------------------------------------------------------------------------------------------------------------------*/ + var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, { - // Must be called when an event in the view has been resized to a new length - reportEventResize: function(event, resizeLocation, largeUnit, el, ev) { - var calendar = this.calendar; - var mutateResult = calendar.mutateEvent(event, resizeLocation, largeUnit); - var undoFunc = function() { - mutateResult.undo(); - calendar.reportEventChange(); - }; + type: null, // subclass' view name (string) + name: null, // deprecated. use `type` instead + title: null, // the text that will be displayed in the header's title - this.triggerEventResize(event, mutateResult.durationDelta, undoFunc, el, ev); - calendar.reportEventChange(); // will rerender events - }, + calendar: null, // owner Calendar object + options: null, // hash containing all options. already merged with view-specific-options + el: null, // the view's containing element. set by Calendar + displaying: null, // a promise representing the state of rendering. null if no render requested + isSkeletonRendered: false, + isEventsRendered: false, - // Triggers event-resize handlers that have subscribed via the API - triggerEventResize: function(event, durationDelta, undoFunc, el, ev) { - this.trigger('eventResize', el[0], event, durationDelta, undoFunc, ev, {}); // {} = jqui dummy - }, + // range the view is actually displaying (moments) + start: null, + end: null, // exclusive + // range the view is formally responsible for (moments) + // may be different from start/end. for example, a month view might have 1st-31st, excluding padded dates + intervalStart: null, + intervalEnd: null, // exclusive + intervalDuration: null, + intervalUnit: null, // name of largest unit being displayed, like "month" or "week" - /* Selection (time range) - ------------------------------------------------------------------------------------------------------------------*/ + isRTL: false, + isSelected: false, // boolean whether a range of time is user-selected or not + selectedEvent: null, + eventOrderSpecs: null, // criteria for ordering events when they have same date/time - // Selects a date span on the view. `start` and `end` are both Moments. - // `ev` is the native mouse event that begin the interaction. - select: function(span, ev) { - this.unselect(ev); - this.renderSelection(span); - this.reportSelection(span, ev); - }, + // classNames styled by jqui themes + widgetHeaderClass: null, + widgetContentClass: null, + highlightStateClass: null, + // for date utils, computed from options + nextDayThreshold: null, + isHiddenDayHash: null, - // Renders a visual indication of the selection - renderSelection: function(span) { - // subclasses should implement - }, + // now indicator + isNowIndicatorRendered: null, + initialNowDate: null, // result first getNow call + initialNowQueriedMs: null, // ms time the getNow was called + nowIndicatorTimeoutID: null, // for refresh timing of now indicator + nowIndicatorIntervalID: null, // " - // Called when a new selection is made. Updates internal state and triggers handlers. - reportSelection: function(span, ev) { - this.isSelected = true; - this.triggerSelect(span, ev); - }, + constructor: function(calendar, type, options, intervalDuration) { + this.calendar = calendar; + this.type = this.name = type; // .name is deprecated + this.options = options; + this.intervalDuration = intervalDuration || moment.duration(1, 'day'); - // Triggers handlers to 'select' - triggerSelect: function(span, ev) { - this.trigger( - 'select', - null, - this.calendar.applyTimezone(span.start), // convert to calendar's tz for external API - this.calendar.applyTimezone(span.end), // " - ev - ); - }, + this.nextDayThreshold = moment.duration(this.opt('nextDayThreshold')); + this.initThemingProps(); + this.initHiddenDays(); + this.isRTL = this.opt('isRTL'); + + this.eventOrderSpecs = parseFieldSpecs(this.opt('eventOrder')); + this.initialize(); + }, - // Undoes a selection. updates in the internal state and triggers handlers. - // `ev` is the native mouse event that began the interaction. - unselect: function(ev) { - if (this.isSelected) { - this.isSelected = false; - if (this.destroySelection) { - this.destroySelection(); // TODO: deprecate - } - this.unrenderSelection(); - this.trigger('unselect', null, ev); - } - }, + // A good place for subclasses to initialize member variables + initialize: function() { + // subclasses can implement + }, + + + // Retrieves an option with the given name + opt: function(name) { + return this.options[name]; + }, - // Unrenders a visual indication of selection - unrenderSelection: function() { - // subclasses should implement - }, + // Triggers handlers that are view-related. Modifies args before passing to calendar. + trigger: function(name, thisObj) { // arguments beyond thisObj are passed along + var calendar = this.calendar; - /* Event Selection - ------------------------------------------------------------------------------------------------------------------*/ + return calendar.trigger.apply( + calendar, + [name, thisObj || this].concat( + Array.prototype.slice.call(arguments, 2), // arguments beyond thisObj + [ this ] // always make the last argument a reference to the view. TODO: deprecate + ) + ); + }, - selectEvent: function(event) { - if (!this.selectedEvent || this.selectedEvent !== event) { - this.unselectEvent(); - this.renderedEventSegEach(function(seg) { - seg.el.addClass('fc-selected'); - }, event); - this.selectedEvent = event; - } - }, + /* Dates + ------------------------------------------------------------------------------------------------------------------*/ - unselectEvent: function() { - if (this.selectedEvent) { - this.renderedEventSegEach(function(seg) { - seg.el.removeClass('fc-selected'); - }, this.selectedEvent); - this.selectedEvent = null; - } - }, + // Updates all internal dates to center around the given current unzoned date. + setDate: function(date) { + this.setRange(this.computeRange(date)); + }, + + + // Updates all internal dates for displaying the given unzoned range. + setRange: function(range) { + $.extend(this, range); // assigns every property to this object's member variables + this.updateTitle(); + }, + + + // Given a single current unzoned date, produce information about what range to display. + // Subclasses can override. Must return all properties. + computeRange: function(date) { + var intervalUnit = computeIntervalUnit(this.intervalDuration); + var intervalStart = date.clone().startOf(intervalUnit); + var intervalEnd = intervalStart.clone().add(this.intervalDuration); + var start, end; + + // normalize the range's time-ambiguity + if (/year|month|week|day/.test(intervalUnit)) { // whole-days? + intervalStart.stripTime(); + intervalEnd.stripTime(); + } + else { // needs to have a time? + if (!intervalStart.hasTime()) { + intervalStart = this.calendar.time(0); // give 00:00 time + } + if (!intervalEnd.hasTime()) { + intervalEnd = this.calendar.time(0); // give 00:00 time + } + } + + start = intervalStart.clone(); + start = this.skipHiddenDays(start); + end = intervalEnd.clone(); + end = this.skipHiddenDays(end, -1, true); // exclusively move backwards + + return { + intervalUnit: intervalUnit, + intervalStart: intervalStart, + intervalEnd: intervalEnd, + start: start, + end: end + }; + }, + + + // Computes the new date when the user hits the prev button, given the current date + computePrevDate: function(date) { + return this.massageCurrentDate( + date.clone().startOf(this.intervalUnit).subtract(this.intervalDuration), -1 + ); + }, + + + // Computes the new date when the user hits the next button, given the current date + computeNextDate: function(date) { + return this.massageCurrentDate( + date.clone().startOf(this.intervalUnit).add(this.intervalDuration) + ); + }, + + + // Given an arbitrarily calculated current date of the calendar, returns a date that is ensured to be completely + // visible. `direction` is optional and indicates which direction the current date was being + // incremented or decremented (1 or -1). + massageCurrentDate: function(date, direction) { + if (this.intervalDuration.as('days') <= 1) { // if the view displays a single day or smaller + if (this.isHiddenDay(date)) { + date = this.skipHiddenDays(date, direction); + date.startOf('day'); + } + } + + return date; + }, + + + /* Title and Date Formatting + ------------------------------------------------------------------------------------------------------------------*/ + + + // Sets the view's title property to the most updated computed value + updateTitle: function() { + this.title = this.computeTitle(); + }, + + + // Computes what the title at the top of the calendar should be for this view + computeTitle: function() { + return this.formatRange( + { + // in case intervalStart/End has a time, make sure timezone is correct + start: this.calendar.applyTimezone(this.intervalStart), + end: this.calendar.applyTimezone(this.intervalEnd) + }, + this.opt('titleFormat') || this.computeTitleFormat(), + this.opt('titleRangeSeparator') + ); + }, + + + // Generates the format string that should be used to generate the title for the current date range. + // Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`. + computeTitleFormat: function() { + if (this.intervalUnit == 'year') { + return 'YYYY'; + } + else if (this.intervalUnit == 'month') { + return this.opt('monthYearFormat'); // like "September 2014" + } + else if (this.intervalDuration.as('days') > 1) { + return 'll'; // multi-day range. shorter, like "Sep 9 - 10 2014" + } + else { + return 'LL'; // one day. longer, like "September 9 2014" + } + }, + + + // Utility for formatting a range. Accepts a range object, formatting string, and optional separator. + // Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account. + // The timezones of the dates within `range` will be respected. + formatRange: function(range, formatStr, separator) { + var end = range.end; + + if (!end.hasTime()) { // all-day? + end = end.clone().subtract(1); // convert to inclusive. last ms of previous day + } + + return formatRange(range.start, end, formatStr, separator, this.opt('isRTL')); + }, + + + /* Rendering + ------------------------------------------------------------------------------------------------------------------*/ + + + // Sets the container element that the view should render inside of. + // Does other DOM-related initializations. + setElement: function(el) { + this.el = el; + this.bindGlobalHandlers(); + }, + + + // Removes the view's container element from the DOM, clearing any content beforehand. + // Undoes any other DOM-related attachments. + removeElement: function() { + this.clear(); // clears all content + + // clean up the skeleton + if (this.isSkeletonRendered) { + this.unrenderSkeleton(); + this.isSkeletonRendered = false; + } + + this.unbindGlobalHandlers(); + + this.el.remove(); + + // NOTE: don't null-out this.el in case the View was destroyed within an API callback. + // We don't null-out the View's other jQuery element references upon destroy, + // so we shouldn't kill this.el either. + }, + + + // Does everything necessary to display the view centered around the given unzoned date. + // Does every type of rendering EXCEPT rendering events. + // Is asychronous and returns a promise. + display: function(date) { + var _this = this; + var scrollState = null; + + if (this.displaying) { + scrollState = this.queryScroll(); + } + + this.calendar.freezeContentHeight(); + + return this.clear().then(function() { // clear the content first (async) + return ( + _this.displaying = + $.when(_this.displayView(date)) // displayView might return a promise + .then(function() { + _this.forceScroll(_this.computeInitialScroll(scrollState)); + _this.calendar.unfreezeContentHeight(); + _this.triggerRender(); + }) + ); + }); + }, + + + // Does everything necessary to clear the content of the view. + // Clears dates and events. Does not clear the skeleton. + // Is asychronous and returns a promise. + clear: function() { + var _this = this; + var displaying = this.displaying; + + if (displaying) { // previously displayed, or in the process of being displayed? + return displaying.then(function() { // wait for the display to finish + _this.displaying = null; + _this.clearEvents(); + return _this.clearView(); // might return a promise. chain it + }); + } + else { + return $.when(); // an immediately-resolved promise + } + }, + + + // Displays the view's non-event content, such as date-related content or anything required by events. + // Renders the view's non-content skeleton if necessary. + // Can be asynchronous and return a promise. + displayView: function(date) { + if (!this.isSkeletonRendered) { + this.renderSkeleton(); + this.isSkeletonRendered = true; + } + if (date) { + this.setDate(date); + } + if (this.render) { + this.render(); // TODO: deprecate + } + this.renderDates(); + this.updateSize(); + this.renderBusinessHours(); // might need coordinates, so should go after updateSize() + this.startNowIndicator(); + }, + + + // Unrenders the view content that was rendered in displayView. + // Can be asynchronous and return a promise. + clearView: function() { + this.unselect(); + this.stopNowIndicator(); + this.triggerUnrender(); + this.unrenderBusinessHours(); + this.unrenderDates(); + if (this.destroy) { + this.destroy(); // TODO: deprecate + } + }, + + + // Renders the basic structure of the view before any content is rendered + renderSkeleton: function() { + // subclasses should implement + }, + + + // Unrenders the basic structure of the view + unrenderSkeleton: function() { + // subclasses should implement + }, + + + // Renders the view's date-related content. + // Assumes setRange has already been called and the skeleton has already been rendered. + renderDates: function() { + // subclasses should implement + }, + + + // Unrenders the view's date-related content + unrenderDates: function() { + // subclasses should override + }, + + + // Signals that the view's content has been rendered + triggerRender: function() { + this.trigger('viewRender', this, this, this.el); + }, + + + // Signals that the view's content is about to be unrendered + triggerUnrender: function() { + this.trigger('viewDestroy', this, this, this.el); + }, + + + // Binds DOM handlers to elements that reside outside the view container, such as the document + bindGlobalHandlers: function() { + this.listenTo($(document), 'mousedown', this.handleDocumentMousedown); + this.listenTo($(document), 'touchstart', this.handleDocumentTouchStart); + this.listenTo($(document), 'touchend', this.handleDocumentTouchEnd); + }, + + + // Unbinds DOM handlers from elements that reside outside the view container + unbindGlobalHandlers: function() { + this.stopListeningTo($(document)); + }, + + + // Initializes internal variables related to theming + initThemingProps: function() { + var tm = this.opt('theme') ? 'ui' : 'fc'; + + this.widgetHeaderClass = tm + '-widget-header'; + this.widgetContentClass = tm + '-widget-content'; + this.highlightStateClass = tm + '-state-highlight'; + }, + + + /* Business Hours + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders business-hours onto the view. Assumes updateSize has already been called. + renderBusinessHours: function() { + // subclasses should implement + }, - isEventSelected: function(event) { - // event references might change on refetchEvents(), while selectedEvent doesn't, - // so compare IDs - return this.selectedEvent && this.selectedEvent._id === event._id; - }, + // Unrenders previously-rendered business-hours + unrenderBusinessHours: function() { + // subclasses should implement + }, - /* Mouse / Touch Unselecting (time range & event unselection) - ------------------------------------------------------------------------------------------------------------------*/ - // TODO: move consistently to down/start or up/end? + /* Now Indicator + ------------------------------------------------------------------------------------------------------------------*/ - handleDocumentMousedown: function(ev) { - // touch devices fire simulated mouse events on a "click". - // only process mousedown if we know this isn't a touch device. - if (!FC.isTouchEnabled && isPrimaryMouseButton(ev)) { - this.processRangeUnselect(ev); - this.processEventUnselect(ev); - } - }, - - - handleDocumentTouchStart: function(ev) { - this.processRangeUnselect(ev); - }, - - - handleDocumentTouchEnd: function(ev) { - // TODO: don't do this if because of touch-scrolling - this.processEventUnselect(ev); - }, - - - processRangeUnselect: function(ev) { - var ignore; - - // is there a time-range selection? - if (this.isSelected && this.opt('unselectAuto')) { - // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element - ignore = this.opt('unselectCancel'); - if (!ignore || !$(ev.target).closest(ignore).length) { - this.unselect(ev); - } - } - }, - - - processEventUnselect: function(ev) { - if (this.selectedEvent) { - if (!$(ev.target).closest('.fc-selected').length) { - this.unselectEvent(); - } - } - }, - - - /* Day Click - ------------------------------------------------------------------------------------------------------------------*/ - - - // Triggers handlers to 'dayClick' - // Span has start/end of the clicked area. Only the start is useful. - triggerDayClick: function(span, dayEl, ev) { - this.trigger( - 'dayClick', - dayEl, - this.calendar.applyTimezone(span.start), // convert to calendar's timezone for external API - ev - ); - }, - - - /* Date Utils - ------------------------------------------------------------------------------------------------------------------*/ - - - // Initializes internal variables related to calculating hidden days-of-week - initHiddenDays: function() { - var hiddenDays = this.opt('hiddenDays') || []; // array of day-of-week indices that are hidden - var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool) - var dayCnt = 0; - var i; - - if (this.opt('weekends') === false) { - hiddenDays.push(0, 6); // 0=sunday, 6=saturday - } - - for (i = 0; i < 7; i++) { - if ( - !(isHiddenDayHash[i] = $.inArray(i, hiddenDays) !== -1) - ) { - dayCnt++; - } - } - - if (!dayCnt) { - throw 'invalid hiddenDays'; // all days were hidden? bad. - } - - this.isHiddenDayHash = isHiddenDayHash; - }, - - - // Is the current day hidden? - // `day` is a day-of-week index (0-6), or a Moment - isHiddenDay: function(day) { - if (moment.isMoment(day)) { - day = day.day(); - } - return this.isHiddenDayHash[day]; - }, - - - // Incrementing the current day until it is no longer a hidden day, returning a copy. - // If the initial value of `date` is not a hidden day, don't do anything. - // Pass `isExclusive` as `true` if you are dealing with an end date. - // `inc` defaults to `1` (increment one day forward each time) - skipHiddenDays: function(date, inc, isExclusive) { - var out = date.clone(); - inc = inc || 1; - while ( - this.isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7] - ) { - out.add(inc, 'days'); - } - return out; - }, - - - // Returns the date range of the full days the given range visually appears to occupy. - // Returns a new range object. - computeDayRange: function(range) { - var startDay = range.start.clone().stripTime(); // the beginning of the day the range starts - var end = range.end; - var endDay = null; - var endTimeMS; - - if (end) { - endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends - endTimeMS = +end.time(); // # of milliseconds into `endDay` - - // If the end time is actually inclusively part of the next day and is equal to or - // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`. - // Otherwise, leaving it as inclusive will cause it to exclude `endDay`. - if (endTimeMS && endTimeMS >= this.nextDayThreshold) { - endDay.add(1, 'days'); - } - } - - // If no end was specified, or if it is within `startDay` but not past nextDayThreshold, - // assign the default duration of one day. - if (!end || endDay <= startDay) { - endDay = startDay.clone().add(1, 'days'); - } - - return { start: startDay, end: endDay }; - }, - - - // Does the given event visually appear to occupy more than one day? - isMultiDayEvent: function(event) { - var range = this.computeDayRange(event); // event is range-ish - - return range.end.diff(range.start, 'days') > 1; - } + // Immediately render the current time indicator and begins re-rendering it at an interval, + // which is defined by this.getNowIndicatorUnit(). + // TODO: somehow do this for the current whole day's background too + startNowIndicator: function() { + var _this = this; + var unit; + var update; + var delay; // ms wait value -}); + if (this.opt('nowIndicator')) { + unit = this.getNowIndicatorUnit(); + if (unit) { + update = proxy(this, 'updateNowIndicator'); // bind to `this` -;; + this.initialNowDate = this.calendar.getNow(); + this.initialNowQueriedMs = +new Date(); + this.renderNowIndicator(this.initialNowDate); + this.isNowIndicatorRendered = true; -/* -Embodies a div that has potential scrollbars -*/ -var Scroller = FC.Scroller = Class.extend({ + // wait until the beginning of the next interval + delay = this.initialNowDate.clone().startOf(unit).add(1, unit) - this.initialNowDate; + this.nowIndicatorTimeoutID = setTimeout(function() { + _this.nowIndicatorTimeoutID = null; + update(); + delay = +moment.duration(1, unit); + delay = Math.max(100, delay); // prevent too frequent + _this.nowIndicatorIntervalID = setInterval(update, delay); // update every interval + }, delay); + } + } + }, - el: null, // the guaranteed outer element - scrollEl: null, // the element with the scrollbars - overflowX: null, - overflowY: null, + // rerenders the now indicator, computing the new current time from the amount of time that has passed + // since the initial getNow call. + updateNowIndicator: function() { + if (this.isNowIndicatorRendered) { + this.unrenderNowIndicator(); + this.renderNowIndicator( + this.initialNowDate.clone().add(new Date() - this.initialNowQueriedMs) // add ms + ); + } + }, - constructor: function(options) { - options = options || {}; - this.overflowX = options.overflowX || options.overflow || 'auto'; - this.overflowY = options.overflowY || options.overflow || 'auto'; - }, + // Immediately unrenders the view's current time indicator and stops any re-rendering timers. + // Won't cause side effects if indicator isn't rendered. + stopNowIndicator: function() { + if (this.isNowIndicatorRendered) { - render: function() { - this.el = this.renderEl(); - this.applyOverflow(); - }, + if (this.nowIndicatorTimeoutID) { + clearTimeout(this.nowIndicatorTimeoutID); + this.nowIndicatorTimeoutID = null; + } + if (this.nowIndicatorIntervalID) { + clearTimeout(this.nowIndicatorIntervalID); + this.nowIndicatorIntervalID = null; + } + this.unrenderNowIndicator(); + this.isNowIndicatorRendered = false; + } + }, - renderEl: function() { - return (this.scrollEl = $('
')); - }, + // Returns a string unit, like 'second' or 'minute' that defined how often the current time indicator + // should be refreshed. If something falsy is returned, no time indicator is rendered at all. + getNowIndicatorUnit: function() { + // subclasses should implement + }, - // sets to natural height, unlocks overflow - clear: function() { - this.setHeight('auto'); - this.applyOverflow(); - }, + // Renders a current time indicator at the given datetime + renderNowIndicator: function(date) { + // subclasses should implement + }, - destroy: function() { - this.el.remove(); - }, + // Undoes the rendering actions from renderNowIndicator + unrenderNowIndicator: function() { + // subclasses should implement + }, - // Overflow - // ----------------------------------------------------------------------------------------------------------------- + /* Dimensions + ------------------------------------------------------------------------------------------------------------------*/ - applyOverflow: function() { - this.scrollEl.css({ - 'overflow-x': this.overflowX, - 'overflow-y': this.overflowY - }); - }, + // Refreshes anything dependant upon sizing of the container element of the grid + updateSize: function(isResize) { + var scrollState; - // Causes any 'auto' overflow values to resolves to 'scroll' or 'hidden'. - // Useful for preserving scrollbar widths regardless of future resizes. - // Can pass in scrollbarWidths for optimization. - lockOverflow: function(scrollbarWidths) { - var overflowX = this.overflowX; - var overflowY = this.overflowY; + if (isResize) { + scrollState = this.queryScroll(); + } - scrollbarWidths = scrollbarWidths || this.getScrollbarWidths(); + this.updateHeight(isResize); + this.updateWidth(isResize); + this.updateNowIndicator(); - if (overflowX === 'auto') { - overflowX = ( - scrollbarWidths.top || scrollbarWidths.bottom || // horizontal scrollbars? - // OR scrolling pane with massless scrollbars? - this.scrollEl[0].scrollWidth - 1 > this.scrollEl[0].clientWidth - // subtract 1 because of IE off-by-one issue - ) ? 'scroll' : 'hidden'; - } + if (isResize) { + this.setScroll(scrollState); + } + }, - if (overflowY === 'auto') { - overflowY = ( - scrollbarWidths.left || scrollbarWidths.right || // vertical scrollbars? - // OR scrolling pane with massless scrollbars? - this.scrollEl[0].scrollHeight - 1 > this.scrollEl[0].clientHeight - // subtract 1 because of IE off-by-one issue - ) ? 'scroll' : 'hidden'; - } - this.scrollEl.css({ 'overflow-x': overflowX, 'overflow-y': overflowY }); - }, + // Refreshes the horizontal dimensions of the calendar + updateWidth: function(isResize) { + // subclasses should implement + }, - // Getters / Setters - // ----------------------------------------------------------------------------------------------------------------- + // Refreshes the vertical dimensions of the calendar + updateHeight: function(isResize) { + var calendar = this.calendar; // we poll the calendar for height information + this.setHeight( + calendar.getSuggestedViewHeight(), + calendar.isHeightAuto() + ); + }, - setHeight: function(height) { - this.scrollEl.height(height); - }, + // Updates the vertical dimensions of the calendar to the specified height. + // if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height. + setHeight: function(height, isAuto) { + // subclasses should implement + }, - getScrollTop: function() { - return this.scrollEl.scrollTop(); - }, + /* Scroller + ------------------------------------------------------------------------------------------------------------------*/ - setScrollTop: function(top) { - this.scrollEl.scrollTop(top); - }, + // Computes the initial pre-configured scroll state prior to allowing the user to change it. + // Given the scroll state from the previous rendering. If first time rendering, given null. + computeInitialScroll: function(previousScrollState) { + return 0; + }, - getClientWidth: function() { - return this.scrollEl[0].clientWidth; - }, + // Retrieves the view's current natural scroll state. Can return an arbitrary format. + queryScroll: function() { + // subclasses must implement + }, - getClientHeight: function() { - return this.scrollEl[0].clientHeight; - }, + // Sets the view's scroll state. Will accept the same format computeInitialScroll and queryScroll produce. + setScroll: function(scrollState) { + // subclasses must implement + }, - getScrollbarWidths: function() { - return getScrollbarWidths(this.scrollEl); - } -}); + // Sets the scroll state, making sure to overcome any predefined scroll value the browser has in mind + forceScroll: function(scrollState) { + var _this = this; -;; - -var Calendar = FC.Calendar = Class.extend({ - - dirDefaults: null, // option defaults related to LTR or RTL - langDefaults: null, // option defaults related to current locale - overrides: null, // option overrides given to the fullCalendar constructor - options: null, // all defaults combined with overrides - viewSpecCache: null, // cache of view definitions - view: null, // current View object - header: null, - loadingLevel: 0, // number of simultaneous loading tasks - - - // a lot of this class' OOP logic is scoped within this constructor function, - // but in the future, write individual methods on the prototype. - constructor: Calendar_constructor, - - - // Subclasses can override this for initialization logic after the constructor has been called - initialize: function() { - }, - - - // Initializes `this.options` and other important options-related objects - initOptions: function(overrides) { - var lang, langDefaults; - var isRTL, dirDefaults; - - // converts legacy options into non-legacy ones. - // in the future, when this is removed, don't use `overrides` reference. make a copy. - overrides = massageOverrides(overrides); - - lang = overrides.lang; - langDefaults = langOptionHash[lang]; - if (!langDefaults) { - lang = Calendar.defaults.lang; - langDefaults = langOptionHash[lang] || {}; - } - - isRTL = firstDefined( - overrides.isRTL, - langDefaults.isRTL, - Calendar.defaults.isRTL - ); - dirDefaults = isRTL ? Calendar.rtlDefaults : {}; - - this.dirDefaults = dirDefaults; - this.langDefaults = langDefaults; - this.overrides = overrides; - this.options = mergeOptions([ // merge defaults and overrides. lowest to highest precedence - Calendar.defaults, // global defaults - dirDefaults, - langDefaults, - overrides - ]); - populateInstanceComputableOptions(this.options); - - this.viewSpecCache = {}; // somewhat unrelated - }, - - - // Gets information about how to create a view. Will use a cache. - getViewSpec: function(viewType) { - var cache = this.viewSpecCache; - - return cache[viewType] || (cache[viewType] = this.buildViewSpec(viewType)); - }, - - - // Given a duration singular unit, like "week" or "day", finds a matching view spec. - // Preference is given to views that have corresponding buttons. - getUnitViewSpec: function(unit) { - var viewTypes; - var i; - var spec; - - if ($.inArray(unit, intervalUnits) != -1) { - - // put views that have buttons first. there will be duplicates, but oh well - viewTypes = this.header.getViewsWithButtons(); - $.each(FC.views, function(viewType) { // all views - viewTypes.push(viewType); - }); - - for (i = 0; i < viewTypes.length; i++) { - spec = this.getViewSpec(viewTypes[i]); - if (spec) { - if (spec.singleUnit == unit) { - return spec; - } - } - } - } - }, - - - // Builds an object with information on how to create a given view - buildViewSpec: function(requestedViewType) { - var viewOverrides = this.overrides.views || {}; - var specChain = []; // for the view. lowest to highest priority - var defaultsChain = []; // for the view. lowest to highest priority - var overridesChain = []; // for the view. lowest to highest priority - var viewType = requestedViewType; - var spec; // for the view - var overrides; // for the view - var duration; - var unit; - - // iterate from the specific view definition to a more general one until we hit an actual View class - while (viewType) { - spec = fcViews[viewType]; - overrides = viewOverrides[viewType]; - viewType = null; // clear. might repopulate for another iteration - - if (typeof spec === 'function') { // TODO: deprecate - spec = { 'class': spec }; - } - - if (spec) { - specChain.unshift(spec); - defaultsChain.unshift(spec.defaults || {}); - duration = duration || spec.duration; - viewType = viewType || spec.type; - } - - if (overrides) { - overridesChain.unshift(overrides); // view-specific option hashes have options at zero-level - duration = duration || overrides.duration; - viewType = viewType || overrides.type; - } - } - - spec = mergeProps(specChain); - spec.type = requestedViewType; - if (!spec['class']) { - return false; - } - - if (duration) { - duration = moment.duration(duration); - if (duration.valueOf()) { // valid? - spec.duration = duration; - unit = computeIntervalUnit(duration); - - // view is a single-unit duration, like "week" or "day" - // incorporate options for this. lowest priority - if (duration.as(unit) === 1) { - spec.singleUnit = unit; - overridesChain.unshift(viewOverrides[unit] || {}); - } - } - } - - spec.defaults = mergeOptions(defaultsChain); - spec.overrides = mergeOptions(overridesChain); - - this.buildViewSpecOptions(spec); - this.buildViewSpecButtonText(spec, requestedViewType); - - return spec; - }, - - - // Builds and assigns a view spec's options object from its already-assigned defaults and overrides - buildViewSpecOptions: function(spec) { - spec.options = mergeOptions([ // lowest to highest priority - Calendar.defaults, // global defaults - spec.defaults, // view's defaults (from ViewSubclass.defaults) - this.dirDefaults, - this.langDefaults, // locale and dir take precedence over view's defaults! - this.overrides, // calendar's overrides (options given to constructor) - spec.overrides // view's overrides (view-specific options) - ]); - populateInstanceComputableOptions(spec.options); - }, - - - // Computes and assigns a view spec's buttonText-related options - buildViewSpecButtonText: function(spec, requestedViewType) { - - // given an options object with a possible `buttonText` hash, lookup the buttonText for the - // requested view, falling back to a generic unit entry like "week" or "day" - function queryButtonText(options) { - var buttonText = options.buttonText || {}; - return buttonText[requestedViewType] || - (spec.singleUnit ? buttonText[spec.singleUnit] : null); - } - - // highest to lowest priority - spec.buttonTextOverride = - queryButtonText(this.overrides) || // constructor-specified buttonText lookup hash takes precedence - spec.overrides.buttonText; // `buttonText` for view-specific options is a string - - // highest to lowest priority. mirrors buildViewSpecOptions - spec.buttonTextDefault = - queryButtonText(this.langDefaults) || - queryButtonText(this.dirDefaults) || - spec.defaults.buttonText || // a single string. from ViewSubclass.defaults - queryButtonText(Calendar.defaults) || - (spec.duration ? this.humanizeDuration(spec.duration) : null) || // like "3 days" - requestedViewType; // fall back to given view name - }, - - - // Given a view name for a custom view or a standard view, creates a ready-to-go View object - instantiateView: function(viewType) { - var spec = this.getViewSpec(viewType); - - return new spec['class'](this, viewType, spec.options, spec.duration); - }, - - - // Returns a boolean about whether the view is okay to instantiate at some point - isValidViewType: function(viewType) { - return Boolean(this.getViewSpec(viewType)); - }, - - - // Should be called when any type of async data fetching begins - pushLoading: function() { - if (!(this.loadingLevel++)) { - this.trigger('loading', null, true, this.view); - } - }, - - - // Should be called when any type of async data fetching completes - popLoading: function() { - if (!(--this.loadingLevel)) { - this.trigger('loading', null, false, this.view); - } - }, - - - // Given arguments to the select method in the API, returns a span (unzoned start/end and other info) - buildSelectSpan: function(zonedStartInput, zonedEndInput) { - var start = this.moment(zonedStartInput).stripZone(); - var end; - - if (zonedEndInput) { - end = this.moment(zonedEndInput).stripZone(); - } - else if (start.hasTime()) { - end = start.clone().add(this.defaultTimedEventDuration); - } - else { - end = start.clone().add(this.defaultAllDayEventDuration); - } - - return { start: start, end: end }; - } + this.setScroll(scrollState); + setTimeout(function() { + _this.setScroll(scrollState); + }, 0); + }, -}); + /* Event Elements / Segments + ------------------------------------------------------------------------------------------------------------------*/ -Calendar.mixin(EmitterMixin); + // Does everything necessary to display the given events onto the current view + displayEvents: function(events) { + var scrollState = this.queryScroll(); -function Calendar_constructor(element, overrides) { - var t = this; + this.clearEvents(); + this.renderEvents(events); + this.isEventsRendered = true; + this.setScroll(scrollState); + this.triggerEventRender(); + }, - t.initOptions(overrides || {}); - var options = this.options; + // Does everything necessary to clear the view's currently-rendered events + clearEvents: function() { + var scrollState; + if (this.isEventsRendered) { - // Exports - // ----------------------------------------------------------------------------------- + // TODO: optimize: if we know this is part of a displayEvents call, don't queryScroll/setScroll + scrollState = this.queryScroll(); - t.render = render; - t.destroy = destroy; - t.refetchEvents = refetchEvents; - t.reportEvents = reportEvents; - t.reportEventChange = reportEventChange; - t.rerenderEvents = renderEvents; // `renderEvents` serves as a rerender. an API method - t.changeView = renderView; // `renderView` will switch to another view - t.select = select; - t.unselect = unselect; - t.prev = prev; - t.next = next; - t.prevYear = prevYear; - t.nextYear = nextYear; - t.today = today; - t.gotoDate = gotoDate; - t.incrementDate = incrementDate; - t.zoomTo = zoomTo; - t.getDate = getDate; - t.getCalendar = getCalendar; - t.getView = getView; - t.option = option; - t.trigger = trigger; + this.triggerEventUnrender(); + if (this.destroyEvents) { + this.destroyEvents(); // TODO: deprecate + } + this.unrenderEvents(); + this.setScroll(scrollState); + this.isEventsRendered = false; + } + }, + // Renders the events onto the view. + renderEvents: function(events) { + // subclasses should implement + }, - // Language-data Internals - // ----------------------------------------------------------------------------------- - // Apply overrides to the current language's data + // Removes event elements from the view. + unrenderEvents: function() { + // subclasses should implement + }, - var localeData = createObject( // make a cheap copy - getMomentLocaleData(options.lang) // will fall back to en - ); - if (options.monthNames) { - localeData._months = options.monthNames; - } - if (options.monthNamesShort) { - localeData._monthsShort = options.monthNamesShort; - } - if (options.dayNames) { - localeData._weekdays = options.dayNames; - } - if (options.dayNamesShort) { - localeData._weekdaysShort = options.dayNamesShort; - } - if (options.firstDay != null) { - var _week = createObject(localeData._week); // _week: { dow: # } - _week.dow = options.firstDay; - localeData._week = _week; - } + // Signals that all events have been rendered + triggerEventRender: function() { + this.renderedEventSegEach(function(seg) { + this.trigger('eventAfterRender', seg.event, seg.event, seg.el); + }); + this.trigger('eventAfterAllRender'); + }, - // assign a normalized value, to be used by our .week() moment extension - localeData._fullCalendar_weekCalc = (function(weekCalc) { - if (typeof weekCalc === 'function') { - return weekCalc; - } - else if (weekCalc === 'local') { - return weekCalc; - } - else if (weekCalc === 'iso' || weekCalc === 'ISO') { - return 'ISO'; - } - })(options.weekNumberCalculation); - - - - // Calendar-specific Date Utilities - // ----------------------------------------------------------------------------------- - - - t.defaultAllDayEventDuration = moment.duration(options.defaultAllDayEventDuration); - t.defaultTimedEventDuration = moment.duration(options.defaultTimedEventDuration); - - - // Builds a moment using the settings of the current calendar: timezone and language. - // Accepts anything the vanilla moment() constructor accepts. - t.moment = function() { - var mom; - - if (options.timezone === 'local') { - mom = FC.moment.apply(null, arguments); - - // Force the moment to be local, because FC.moment doesn't guarantee it. - if (mom.hasTime()) { // don't give ambiguously-timed moments a local zone - mom.local(); - } - } - else if (options.timezone === 'UTC') { - mom = FC.moment.utc.apply(null, arguments); // process as UTC - } - else { - mom = FC.moment.parseZone.apply(null, arguments); // let the input decide the zone - } - if ('_locale' in mom) { // moment 2.8 and above - mom._locale = localeData; - } - else { // pre-moment-2.8 - mom._lang = localeData; - } + // Signals that all event elements are about to be removed + triggerEventUnrender: function() { + this.renderedEventSegEach(function(seg) { + this.trigger('eventDestroy', seg.event, seg.event, seg.el); + }); + }, - return mom; - }; + // Given an event and the default element used for rendering, returns the element that should actually be used. + // Basically runs events and elements through the eventRender hook. + resolveEventEl: function(event, el) { + var custom = this.trigger('eventRender', event, event, el); - // Returns a boolean about whether or not the calendar knows how to calculate - // the timezone offset of arbitrary dates in the current timezone. - t.getIsAmbigTimezone = function() { - return options.timezone !== 'local' && options.timezone !== 'UTC'; - }; + if (custom === false) { // means don't render at all + el = null; + } + else if (custom && custom !== true) { + el = $(custom); + } + return el; + }, - // Returns a copy of the given date in the current timezone. Has no effect on dates without times. - t.applyTimezone = function(date) { - if (!date.hasTime()) { - return date.clone(); - } - var zonedDate = t.moment(date.toArray()); - var timeAdjust = date.time() - zonedDate.time(); - var adjustedZonedDate; + // Hides all rendered event segments linked to the given event + showEvent: function(event) { + this.renderedEventSegEach(function(seg) { + seg.el.css('visibility', ''); + }, event); + }, - // Safari sometimes has problems with this coersion when near DST. Adjust if necessary. (bug #2396) - if (timeAdjust) { // is the time result different than expected? - adjustedZonedDate = zonedDate.clone().add(timeAdjust); // add milliseconds - if (date.time() - adjustedZonedDate.time() === 0) { // does it match perfectly now? - zonedDate = adjustedZonedDate; - } - } - return zonedDate; - }; + // Shows all rendered event segments linked to the given event + hideEvent: function(event) { + this.renderedEventSegEach(function(seg) { + seg.el.css('visibility', 'hidden'); + }, event); + }, - // Returns a moment for the current date, as defined by the client's computer or from the `now` option. - // Will return an moment with an ambiguous timezone. - t.getNow = function() { - var now = options.now; - if (typeof now === 'function') { - now = now(); - } - return t.moment(now).stripZone(); - }; + // Iterates through event segments that have been rendered (have an el). Goes through all by default. + // If the optional `event` argument is specified, only iterates through segments linked to that event. + // The `this` value of the callback function will be the view. + renderedEventSegEach: function(func, event) { + var segs = this.getEventSegs(); + var i; + for (i = 0; i < segs.length; i++) { + if (!event || segs[i].event._id === event._id) { + if (segs[i].el) { + func.call(this, segs[i]); + } + } + } + }, - // Get an event's normalized end date. If not present, calculate it from the defaults. - t.getEventEnd = function(event) { - if (event.end) { - return event.end.clone(); - } - else { - return t.getDefaultEventEnd(event.allDay, event.start); - } - }; + // Retrieves all the rendered segment objects for the view + getEventSegs: function() { + // subclasses must implement + return []; + }, - // Given an event's allDay status and start date, return what its fallback end date should be. - // TODO: rename to computeDefaultEventEnd - t.getDefaultEventEnd = function(allDay, zonedStart) { - var end = zonedStart.clone(); - if (allDay) { - end.stripTime().add(t.defaultAllDayEventDuration); - } - else { - end.add(t.defaultTimedEventDuration); - } + /* Event Drag-n-Drop + ------------------------------------------------------------------------------------------------------------------*/ - if (t.getIsAmbigTimezone()) { - end.stripZone(); // we don't know what the tzo should be - } - return end; - }; + // Computes if the given event is allowed to be dragged by the user + isEventDraggable: function(event) { + var source = event.source || {}; + return firstDefined( + event.startEditable, + source.startEditable, + this.opt('eventStartEditable'), + event.editable, + source.editable, + this.opt('editable') + ); + }, - // Produces a human-readable string for the given duration. - // Side-effect: changes the locale of the given duration. - t.humanizeDuration = function(duration) { - return (duration.locale || duration.lang).call(duration, options.lang) // works moment-pre-2.8 - .humanize(); - }; + // Must be called when an event in the view is dropped onto new location. + // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event. + reportEventDrop: function(event, dropLocation, largeUnit, el, ev) { + var calendar = this.calendar; + var mutateResult = calendar.mutateEvent(event, dropLocation, largeUnit); + var undoFunc = function() { + mutateResult.undo(); + calendar.reportEventChange(); + }; + this.triggerEventDrop(event, mutateResult.dateDelta, undoFunc, el, ev); + calendar.reportEventChange(); // will rerender events + }, - // Imports - // ----------------------------------------------------------------------------------- + // Triggers event-drop handlers that have subscribed via the API + triggerEventDrop: function(event, dateDelta, undoFunc, el, ev) { + this.trigger('eventDrop', el[0], event, dateDelta, undoFunc, ev, {}); // {} = jqui dummy + }, - EventManager.call(t, options); - var isFetchNeeded = t.isFetchNeeded; - var fetchEvents = t.fetchEvents; + /* External Element Drag-n-Drop + ------------------------------------------------------------------------------------------------------------------*/ - // Locals - // ----------------------------------------------------------------------------------- + // Must be called when an external element, via jQuery UI, has been dropped onto the calendar. + // `meta` is the parsed data that has been embedded into the dragging event. + // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event. + reportExternalDrop: function(meta, dropLocation, el, ev, ui) { + var eventProps = meta.eventProps; + var eventInput; + var event; + // Try to build an event object and render it. TODO: decouple the two + if (eventProps) { + eventInput = $.extend({}, eventProps, dropLocation); + event = this.calendar.renderEvent(eventInput, meta.stick)[0]; // renderEvent returns an array + } - var _element = element[0]; - var header; - var headerElement; - var content; - var tm; // for making theme classes - var currentView; // NOTE: keep this in sync with this.view - var viewsByType = {}; // holds all instantiated view instances, current or not - var suggestedViewHeight; - var windowResizeProxy; // wraps the windowResize function - var ignoreWindowResize = 0; - var events = []; - var date; // unzoned + this.triggerExternalDrop(event, dropLocation, el, ev, ui); + }, + // Triggers external-drop handlers that have subscribed via the API + triggerExternalDrop: function(event, dropLocation, el, ev, ui) { - // Main Rendering - // ----------------------------------------------------------------------------------- + // trigger 'drop' regardless of whether element represents an event + this.trigger('drop', el[0], dropLocation.start, ev, ui); + if (event) { + this.trigger('eventReceive', null, event); // signal an external event landed + } + }, - // compute the initial ambig-timezone date - if (options.defaultDate != null) { - date = t.moment(options.defaultDate).stripZone(); - } - else { - date = t.getNow(); // getNow already returns unzoned - } + /* Drag-n-Drop Rendering (for both events and external elements) + ------------------------------------------------------------------------------------------------------------------*/ - function render() { - if (!content) { - initialRender(); - } - else if (elementVisible()) { - // mainly for the public API - calcSize(); - renderView(); - } - } + // Renders a visual indication of a event or external-element drag over the given drop zone. + // If an external-element, seg will be `null`. + // Must return elements used for any mock events. + renderDrag: function(dropLocation, seg) { + // subclasses must implement + }, - function initialRender() { - tm = options.theme ? 'ui' : 'fc'; - element.addClass('fc'); - element.addClass( - FC.isTouchEnabled ? 'fc-touch' : 'fc-cursor' - ); + // Unrenders a visual indication of an event or external-element being dragged. + unrenderDrag: function() { + // subclasses must implement + }, - if (options.isRTL) { - element.addClass('fc-rtl'); - } - else { - element.addClass('fc-ltr'); - } - if (options.theme) { - element.addClass('ui-widget'); - } - else { - element.addClass('fc-unthemed'); - } + /* Event Resizing + ------------------------------------------------------------------------------------------------------------------*/ - content = $("
").prependTo(element); - header = t.header = new Header(t, options); - headerElement = header.render(); - if (headerElement) { - element.prepend(headerElement); - } + // Computes if the given event is allowed to be resized from its starting edge + isEventResizableFromStart: function(event) { + return this.opt('eventResizableFromStart') && this.isEventResizable(event); + }, - renderView(options.defaultView); - if (options.handleWindowResize) { - windowResizeProxy = debounce(windowResize, options.windowResizeDelay); // prevents rapid calls - $(window).resize(windowResizeProxy); - } - } + // Computes if the given event is allowed to be resized from its ending edge + isEventResizableFromEnd: function(event) { + return this.isEventResizable(event); + }, - function destroy() { + // Computes if the given event is allowed to be resized by the user at all + isEventResizable: function(event) { + var source = event.source || {}; - if (currentView) { - currentView.removeElement(); + return firstDefined( + event.durationEditable, + source.durationEditable, + this.opt('eventDurationEditable'), + event.editable, + source.editable, + this.opt('editable') + ); + }, - // NOTE: don't null-out currentView/t.view in case API methods are called after destroy. - // It is still the "current" view, just not rendered. - } - header.removeElement(); - content.remove(); - element.removeClass('fc fc-touch fc-cursor fc-ltr fc-rtl fc-unthemed ui-widget'); + // Must be called when an event in the view has been resized to a new length + reportEventResize: function(event, resizeLocation, largeUnit, el, ev) { + var calendar = this.calendar; + var mutateResult = calendar.mutateEvent(event, resizeLocation, largeUnit); + var undoFunc = function() { + mutateResult.undo(); + calendar.reportEventChange(); + }; - if (windowResizeProxy) { - $(window).unbind('resize', windowResizeProxy); - } - } + this.triggerEventResize(event, mutateResult.durationDelta, undoFunc, el, ev); + calendar.reportEventChange(); // will rerender events + }, - function elementVisible() { - return element.is(':visible'); - } + // Triggers event-resize handlers that have subscribed via the API + triggerEventResize: function(event, durationDelta, undoFunc, el, ev) { + this.trigger('eventResize', el[0], event, durationDelta, undoFunc, ev, {}); // {} = jqui dummy + }, + /* Selection (time range) + ------------------------------------------------------------------------------------------------------------------*/ - // View Rendering - // ----------------------------------------------------------------------------------- + // Selects a date span on the view. `start` and `end` are both Moments. + // `ev` is the native mouse event that begin the interaction. + select: function(span, ev) { + this.unselect(ev); + this.renderSelection(span); + this.reportSelection(span, ev); + }, - // Renders a view because of a date change, view-type change, or for the first time. - // If not given a viewType, keep the current view but render different dates. - function renderView(viewType) { - ignoreWindowResize++; - // if viewType is changing, remove the old view's rendering - if (currentView && viewType && currentView.type !== viewType) { - header.deactivateButton(currentView.type); - freezeContentHeight(); // prevent a scroll jump when view element is removed - currentView.removeElement(); - currentView = t.view = null; - } + // Renders a visual indication of the selection + renderSelection: function(span) { + // subclasses should implement + }, - // if viewType changed, or the view was never created, create a fresh view - if (!currentView && viewType) { - currentView = t.view = - viewsByType[viewType] || - (viewsByType[viewType] = t.instantiateView(viewType)); - currentView.setElement( - $("
").appendTo(content) - ); - header.activateButton(viewType); - } + // Called when a new selection is made. Updates internal state and triggers handlers. + reportSelection: function(span, ev) { + this.isSelected = true; + this.triggerSelect(span, ev); + }, - if (currentView) { - // in case the view should render a period of time that is completely hidden - date = currentView.massageCurrentDate(date); + // Triggers handlers to 'select' + triggerSelect: function(span, ev) { + this.trigger( + 'select', + null, + this.calendar.applyTimezone(span.start), // convert to calendar's tz for external API + this.calendar.applyTimezone(span.end), // " + ev + ); + }, - // render or rerender the view - if ( - !currentView.displaying || - !date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change - ) { - if (elementVisible()) { - currentView.display(date); // will call freezeContentHeight - unfreezeContentHeight(); // immediately unfreeze regardless of whether display is async + // Undoes a selection. updates in the internal state and triggers handlers. + // `ev` is the native mouse event that began the interaction. + unselect: function(ev) { + if (this.isSelected) { + this.isSelected = false; + if (this.destroySelection) { + this.destroySelection(); // TODO: deprecate + } + this.unrenderSelection(); + this.trigger('unselect', null, ev); + } + }, - // need to do this after View::render, so dates are calculated - updateHeaderTitle(); - updateTodayButton(); - getAndRenderEvents(); - } - } - } + // Unrenders a visual indication of selection + unrenderSelection: function() { + // subclasses should implement + }, - unfreezeContentHeight(); // undo any lone freezeContentHeight calls - ignoreWindowResize--; - } + /* Event Selection + ------------------------------------------------------------------------------------------------------------------*/ - // Resizing - // ----------------------------------------------------------------------------------- + selectEvent: function(event) { + if (!this.selectedEvent || this.selectedEvent !== event) { + this.unselectEvent(); + this.renderedEventSegEach(function(seg) { + seg.el.addClass('fc-selected'); + }, event); + this.selectedEvent = event; + } + }, - t.getSuggestedViewHeight = function() { - if (suggestedViewHeight === undefined) { - calcSize(); - } - return suggestedViewHeight; - }; + unselectEvent: function() { + if (this.selectedEvent) { + this.renderedEventSegEach(function(seg) { + seg.el.removeClass('fc-selected'); + }, this.selectedEvent); + this.selectedEvent = null; + } + }, - t.isHeightAuto = function() { - return options.contentHeight === 'auto' || options.height === 'auto'; - }; + isEventSelected: function(event) { + // event references might change on refetchEvents(), while selectedEvent doesn't, + // so compare IDs + return this.selectedEvent && this.selectedEvent._id === event._id; + }, - function updateSize(shouldRecalc) { - if (elementVisible()) { + /* Mouse / Touch Unselecting (time range & event unselection) + ------------------------------------------------------------------------------------------------------------------*/ + // TODO: move consistently to down/start or up/end? - if (shouldRecalc) { - _calcSize(); - } - ignoreWindowResize++; - currentView.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto() - ignoreWindowResize--; + handleDocumentMousedown: function(ev) { + // touch devices fire simulated mouse events on a "click". + // only process mousedown if we know this isn't a touch device. + if (!this.calendar.isTouch && isPrimaryMouseButton(ev)) { + this.processRangeUnselect(ev); + this.processEventUnselect(ev); + } + }, + + + handleDocumentTouchStart: function(ev) { + this.processRangeUnselect(ev); + }, + + + handleDocumentTouchEnd: function(ev) { + // TODO: don't do this if because of touch-scrolling + this.processEventUnselect(ev); + }, + + + processRangeUnselect: function(ev) { + var ignore; + + // is there a time-range selection? + if (this.isSelected && this.opt('unselectAuto')) { + // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element + ignore = this.opt('unselectCancel'); + if (!ignore || !$(ev.target).closest(ignore).length) { + this.unselect(ev); + } + } + }, + + + processEventUnselect: function(ev) { + if (this.selectedEvent) { + if (!$(ev.target).closest('.fc-selected').length) { + this.unselectEvent(); + } + } + }, + + + /* Day Click + ------------------------------------------------------------------------------------------------------------------*/ + + + // Triggers handlers to 'dayClick' + // Span has start/end of the clicked area. Only the start is useful. + triggerDayClick: function(span, dayEl, ev) { + this.trigger( + 'dayClick', + dayEl, + this.calendar.applyTimezone(span.start), // convert to calendar's timezone for external API + ev + ); + }, + + + /* Date Utils + ------------------------------------------------------------------------------------------------------------------*/ + + + // Initializes internal variables related to calculating hidden days-of-week + initHiddenDays: function() { + var hiddenDays = this.opt('hiddenDays') || []; // array of day-of-week indices that are hidden + var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool) + var dayCnt = 0; + var i; + + if (this.opt('weekends') === false) { + hiddenDays.push(0, 6); // 0=sunday, 6=saturday + } + + for (i = 0; i < 7; i++) { + if ( + !(isHiddenDayHash[i] = $.inArray(i, hiddenDays) !== -1) + ) { + dayCnt++; + } + } - return true; // signal success - } - } + if (!dayCnt) { + throw 'invalid hiddenDays'; // all days were hidden? bad. + } + this.isHiddenDayHash = isHiddenDayHash; + }, - function calcSize() { - if (elementVisible()) { - _calcSize(); - } - } + // Is the current day hidden? + // `day` is a day-of-week index (0-6), or a Moment + isHiddenDay: function(day) { + if (moment.isMoment(day)) { + day = day.day(); + } + return this.isHiddenDayHash[day]; + }, - function _calcSize() { // assumes elementVisible - if (typeof options.contentHeight === 'number') { // exists and not 'auto' - suggestedViewHeight = options.contentHeight; - } - else if (typeof options.height === 'number') { // exists and not 'auto' - suggestedViewHeight = options.height - (headerElement ? headerElement.outerHeight(true) : 0); - } - else { - suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5)); - } - } + // Incrementing the current day until it is no longer a hidden day, returning a copy. + // If the initial value of `date` is not a hidden day, don't do anything. + // Pass `isExclusive` as `true` if you are dealing with an end date. + // `inc` defaults to `1` (increment one day forward each time) + skipHiddenDays: function(date, inc, isExclusive) { + var out = date.clone(); + inc = inc || 1; + while ( + this.isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7] + ) { + out.add(inc, 'days'); + } + return out; + }, - function windowResize(ev) { - if ( - !ignoreWindowResize && - ev.target === window && // so we don't process jqui "resize" events that have bubbled up - currentView.start // view has already been rendered - ) { - if (updateSize(true)) { - currentView.trigger('windowResize', _element); - } - } - } + // Returns the date range of the full days the given range visually appears to occupy. + // Returns a new range object. + computeDayRange: function(range) { + var startDay = range.start.clone().stripTime(); // the beginning of the day the range starts + var end = range.end; + var endDay = null; + var endTimeMS; + + if (end) { + endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends + endTimeMS = +end.time(); // # of milliseconds into `endDay` + // If the end time is actually inclusively part of the next day and is equal to or + // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`. + // Otherwise, leaving it as inclusive will cause it to exclude `endDay`. + if (endTimeMS && endTimeMS >= this.nextDayThreshold) { + endDay.add(1, 'days'); + } + } + + // If no end was specified, or if it is within `startDay` but not past nextDayThreshold, + // assign the default duration of one day. + if (!end || endDay <= startDay) { + endDay = startDay.clone().add(1, 'days'); + } - /* Event Fetching/Rendering - -----------------------------------------------------------------------------*/ - // TODO: going forward, most of this stuff should be directly handled by the view + return { start: startDay, end: endDay }; + }, - function refetchEvents() { // can be called as an API method - destroyEvents(); // so that events are cleared before user starts waiting for AJAX - fetchAndRenderEvents(); - } + // Does the given event visually appear to occupy more than one day? + isMultiDayEvent: function(event) { + var range = this.computeDayRange(event); // event is range-ish + return range.end.diff(range.start, 'days') > 1; + } - function renderEvents() { // destroys old events if previously rendered - if (elementVisible()) { - freezeContentHeight(); - currentView.displayEvents(events); - unfreezeContentHeight(); - } - } + }); + ;; - function destroyEvents() { - freezeContentHeight(); - currentView.clearEvents(); - unfreezeContentHeight(); - } + /* + Embodies a div that has potential scrollbars + */ + var Scroller = FC.Scroller = Class.extend({ + el: null, // the guaranteed outer element + scrollEl: null, // the element with the scrollbars + overflowX: null, + overflowY: null, - function getAndRenderEvents() { - if (!options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) { - fetchAndRenderEvents(); - } - else { - renderEvents(); - } - } + constructor: function(options) { + options = options || {}; + this.overflowX = options.overflowX || options.overflow || 'auto'; + this.overflowY = options.overflowY || options.overflow || 'auto'; + }, - function fetchAndRenderEvents() { - fetchEvents(currentView.start, currentView.end); - // ... will call reportEvents - // ... which will call renderEvents - } + render: function() { + this.el = this.renderEl(); + this.applyOverflow(); + }, - // called when event data arrives - function reportEvents(_events) { - events = _events; - renderEvents(); - } + renderEl: function() { + return (this.scrollEl = $('
')); + }, - // called when a single event's data has been changed - function reportEventChange() { - renderEvents(); - } + // sets to natural height, unlocks overflow + clear: function() { + this.setHeight('auto'); + this.applyOverflow(); + }, - /* Header Updating - -----------------------------------------------------------------------------*/ + destroy: function() { + this.el.remove(); + }, - function updateHeaderTitle() { - header.updateTitle(currentView.title); - } + // Overflow + // ----------------------------------------------------------------------------------------------------------------- - function updateTodayButton() { - var now = t.getNow(); - if (now.isWithin(currentView.intervalStart, currentView.intervalEnd)) { - header.disableButton('today'); - } - else { - header.enableButton('today'); - } - } + applyOverflow: function() { + this.scrollEl.css({ + 'overflow-x': this.overflowX, + 'overflow-y': this.overflowY + }); + }, + // Causes any 'auto' overflow values to resolves to 'scroll' or 'hidden'. + // Useful for preserving scrollbar widths regardless of future resizes. + // Can pass in scrollbarWidths for optimization. + lockOverflow: function(scrollbarWidths) { + var overflowX = this.overflowX; + var overflowY = this.overflowY; - /* Selection - -----------------------------------------------------------------------------*/ + scrollbarWidths = scrollbarWidths || this.getScrollbarWidths(); + if (overflowX === 'auto') { + overflowX = ( + scrollbarWidths.top || scrollbarWidths.bottom || // horizontal scrollbars? + // OR scrolling pane with massless scrollbars? + this.scrollEl[0].scrollWidth - 1 > this.scrollEl[0].clientWidth + // subtract 1 because of IE off-by-one issue + ) ? 'scroll' : 'hidden'; + } - // this public method receives start/end dates in any format, with any timezone - function select(zonedStartInput, zonedEndInput) { - currentView.select( - t.buildSelectSpan.apply(t, arguments) - ); - } + if (overflowY === 'auto') { + overflowY = ( + scrollbarWidths.left || scrollbarWidths.right || // vertical scrollbars? + // OR scrolling pane with massless scrollbars? + this.scrollEl[0].scrollHeight - 1 > this.scrollEl[0].clientHeight + // subtract 1 because of IE off-by-one issue + ) ? 'scroll' : 'hidden'; + } + this.scrollEl.css({ 'overflow-x': overflowX, 'overflow-y': overflowY }); + }, - function unselect() { // safe to be called before renderView - if (currentView) { - currentView.unselect(); - } - } + // Getters / Setters + // ----------------------------------------------------------------------------------------------------------------- - /* Date - -----------------------------------------------------------------------------*/ + setHeight: function(height) { + this.scrollEl.height(height); + }, - function prev() { - date = currentView.computePrevDate(date); - renderView(); - } + getScrollTop: function() { + return this.scrollEl.scrollTop(); + }, - function next() { - date = currentView.computeNextDate(date); - renderView(); - } + setScrollTop: function(top) { + this.scrollEl.scrollTop(top); + }, - function prevYear() { - date.add(-1, 'years'); - renderView(); - } + getClientWidth: function() { + return this.scrollEl[0].clientWidth; + }, - function nextYear() { - date.add(1, 'years'); - renderView(); - } + getClientHeight: function() { + return this.scrollEl[0].clientHeight; + }, - function today() { - date = t.getNow(); - renderView(); - } + getScrollbarWidths: function() { + return getScrollbarWidths(this.scrollEl); + } + }); - function gotoDate(zonedDateInput) { - date = t.moment(zonedDateInput).stripZone(); - renderView(); - } + ;; + var Calendar = FC.Calendar = Class.extend({ - function incrementDate(delta) { - date.add(moment.duration(delta)); - renderView(); - } + dirDefaults: null, // option defaults related to LTR or RTL + langDefaults: null, // option defaults related to current locale + overrides: null, // option overrides given to the fullCalendar constructor + options: null, // all defaults combined with overrides + viewSpecCache: null, // cache of view definitions + view: null, // current View object + header: null, + loadingLevel: 0, // number of simultaneous loading tasks + isTouch: false, - // Forces navigation to a view for the given date. - // `viewType` can be a specific view name or a generic one like "week" or "day". - function zoomTo(newDate, viewType) { - var spec; + // a lot of this class' OOP logic is scoped within this constructor function, + // but in the future, write individual methods on the prototype. + constructor: Calendar_constructor, - viewType = viewType || 'day'; // day is default zoom - spec = t.getViewSpec(viewType) || t.getUnitViewSpec(viewType); - date = newDate.clone(); - renderView(spec ? spec.type : null); - } + // Subclasses can override this for initialization logic after the constructor has been called + initialize: function() { + }, - // for external API - function getDate() { - return t.applyTimezone(date); // infuse the calendar's timezone - } + // Initializes `this.options` and other important options-related objects + initOptions: function(overrides) { + var lang, langDefaults; + var isRTL, dirDefaults; + + // converts legacy options into non-legacy ones. + // in the future, when this is removed, don't use `overrides` reference. make a copy. + overrides = massageOverrides(overrides); + + lang = overrides.lang; + langDefaults = langOptionHash[lang]; + if (!langDefaults) { + lang = Calendar.defaults.lang; + langDefaults = langOptionHash[lang] || {}; + } + + isRTL = firstDefined( + overrides.isRTL, + langDefaults.isRTL, + Calendar.defaults.isRTL + ); + dirDefaults = isRTL ? Calendar.rtlDefaults : {}; + + this.dirDefaults = dirDefaults; + this.langDefaults = langDefaults; + this.overrides = overrides; + this.options = mergeOptions([ // merge defaults and overrides. lowest to highest precedence + Calendar.defaults, // global defaults + dirDefaults, + langDefaults, + overrides + ]); + populateInstanceComputableOptions(this.options); + + this.isTouch = this.options.isTouch != null ? + this.options.isTouch : + FC.isTouch; + + this.viewSpecCache = {}; // somewhat unrelated + }, + + + // Gets information about how to create a view. Will use a cache. + getViewSpec: function(viewType) { + var cache = this.viewSpecCache; + + return cache[viewType] || (cache[viewType] = this.buildViewSpec(viewType)); + }, + + + // Given a duration singular unit, like "week" or "day", finds a matching view spec. + // Preference is given to views that have corresponding buttons. + getUnitViewSpec: function(unit) { + var viewTypes; + var i; + var spec; + + if ($.inArray(unit, intervalUnits) != -1) { + + // put views that have buttons first. there will be duplicates, but oh well + viewTypes = this.header.getViewsWithButtons(); + $.each(FC.views, function(viewType) { // all views + viewTypes.push(viewType); + }); + + for (i = 0; i < viewTypes.length; i++) { + spec = this.getViewSpec(viewTypes[i]); + if (spec) { + if (spec.singleUnit == unit) { + return spec; + } + } + } + } + }, + + + // Builds an object with information on how to create a given view + buildViewSpec: function(requestedViewType) { + var viewOverrides = this.overrides.views || {}; + var specChain = []; // for the view. lowest to highest priority + var defaultsChain = []; // for the view. lowest to highest priority + var overridesChain = []; // for the view. lowest to highest priority + var viewType = requestedViewType; + var spec; // for the view + var overrides; // for the view + var duration; + var unit; + + // iterate from the specific view definition to a more general one until we hit an actual View class + while (viewType) { + spec = fcViews[viewType]; + overrides = viewOverrides[viewType]; + viewType = null; // clear. might repopulate for another iteration + + if (typeof spec === 'function') { // TODO: deprecate + spec = { 'class': spec }; + } + + if (spec) { + specChain.unshift(spec); + defaultsChain.unshift(spec.defaults || {}); + duration = duration || spec.duration; + viewType = viewType || spec.type; + } + + if (overrides) { + overridesChain.unshift(overrides); // view-specific option hashes have options at zero-level + duration = duration || overrides.duration; + viewType = viewType || overrides.type; + } + } + + spec = mergeProps(specChain); + spec.type = requestedViewType; + if (!spec['class']) { + return false; + } + + if (duration) { + duration = moment.duration(duration); + if (duration.valueOf()) { // valid? + spec.duration = duration; + unit = computeIntervalUnit(duration); + + // view is a single-unit duration, like "week" or "day" + // incorporate options for this. lowest priority + if (duration.as(unit) === 1) { + spec.singleUnit = unit; + overridesChain.unshift(viewOverrides[unit] || {}); + } + } + } + + spec.defaults = mergeOptions(defaultsChain); + spec.overrides = mergeOptions(overridesChain); + + this.buildViewSpecOptions(spec); + this.buildViewSpecButtonText(spec, requestedViewType); + + return spec; + }, + + + // Builds and assigns a view spec's options object from its already-assigned defaults and overrides + buildViewSpecOptions: function(spec) { + spec.options = mergeOptions([ // lowest to highest priority + Calendar.defaults, // global defaults + spec.defaults, // view's defaults (from ViewSubclass.defaults) + this.dirDefaults, + this.langDefaults, // locale and dir take precedence over view's defaults! + this.overrides, // calendar's overrides (options given to constructor) + spec.overrides // view's overrides (view-specific options) + ]); + populateInstanceComputableOptions(spec.options); + }, + + + // Computes and assigns a view spec's buttonText-related options + buildViewSpecButtonText: function(spec, requestedViewType) { + + // given an options object with a possible `buttonText` hash, lookup the buttonText for the + // requested view, falling back to a generic unit entry like "week" or "day" + function queryButtonText(options) { + var buttonText = options.buttonText || {}; + return buttonText[requestedViewType] || + (spec.singleUnit ? buttonText[spec.singleUnit] : null); + } + + // highest to lowest priority + spec.buttonTextOverride = + queryButtonText(this.overrides) || // constructor-specified buttonText lookup hash takes precedence + spec.overrides.buttonText; // `buttonText` for view-specific options is a string + + // highest to lowest priority. mirrors buildViewSpecOptions + spec.buttonTextDefault = + queryButtonText(this.langDefaults) || + queryButtonText(this.dirDefaults) || + spec.defaults.buttonText || // a single string. from ViewSubclass.defaults + queryButtonText(Calendar.defaults) || + (spec.duration ? this.humanizeDuration(spec.duration) : null) || // like "3 days" + requestedViewType; // fall back to given view name + }, + + + // Given a view name for a custom view or a standard view, creates a ready-to-go View object + instantiateView: function(viewType) { + var spec = this.getViewSpec(viewType); + + return new spec['class'](this, viewType, spec.options, spec.duration); + }, + + + // Returns a boolean about whether the view is okay to instantiate at some point + isValidViewType: function(viewType) { + return Boolean(this.getViewSpec(viewType)); + }, + + + // Should be called when any type of async data fetching begins + pushLoading: function() { + if (!(this.loadingLevel++)) { + this.trigger('loading', null, true, this.view); + } + }, + + + // Should be called when any type of async data fetching completes + popLoading: function() { + if (!(--this.loadingLevel)) { + this.trigger('loading', null, false, this.view); + } + }, + + + // Given arguments to the select method in the API, returns a span (unzoned start/end and other info) + buildSelectSpan: function(zonedStartInput, zonedEndInput) { + var start = this.moment(zonedStartInput).stripZone(); + var end; + + if (zonedEndInput) { + end = this.moment(zonedEndInput).stripZone(); + } + else if (start.hasTime()) { + end = start.clone().add(this.defaultTimedEventDuration); + } + else { + end = start.clone().add(this.defaultAllDayEventDuration); + } + + return { start: start, end: end }; + } + + }); + + + Calendar.mixin(EmitterMixin); + + + function Calendar_constructor(element, overrides) { + var t = this; + + + t.initOptions(overrides || {}); + var options = this.options; + + + // Exports + // ----------------------------------------------------------------------------------- + + t.render = render; + t.destroy = destroy; + t.refetchEvents = refetchEvents; + t.reportEvents = reportEvents; + t.reportEventChange = reportEventChange; + t.rerenderEvents = renderEvents; // `renderEvents` serves as a rerender. an API method + t.changeView = renderView; // `renderView` will switch to another view + t.select = select; + t.unselect = unselect; + t.prev = prev; + t.next = next; + t.prevYear = prevYear; + t.nextYear = nextYear; + t.today = today; + t.gotoDate = gotoDate; + t.incrementDate = incrementDate; + t.zoomTo = zoomTo; + t.getDate = getDate; + t.getCalendar = getCalendar; + t.getView = getView; + t.option = option; + t.trigger = trigger; + + + + // Language-data Internals + // ----------------------------------------------------------------------------------- + // Apply overrides to the current language's data + + + var localeData = createObject( // make a cheap copy + getMomentLocaleData(options.lang) // will fall back to en + ); + + if (options.monthNames) { + localeData._months = options.monthNames; + } + if (options.monthNamesShort) { + localeData._monthsShort = options.monthNamesShort; + } + if (options.dayNames) { + localeData._weekdays = options.dayNames; + } + if (options.dayNamesShort) { + localeData._weekdaysShort = options.dayNamesShort; + } + if (options.firstDay != null) { + var _week = createObject(localeData._week); // _week: { dow: # } + _week.dow = options.firstDay; + localeData._week = _week; + } + + // assign a normalized value, to be used by our .week() moment extension + localeData._fullCalendar_weekCalc = (function(weekCalc) { + if (typeof weekCalc === 'function') { + return weekCalc; + } + else if (weekCalc === 'local') { + return weekCalc; + } + else if (weekCalc === 'iso' || weekCalc === 'ISO') { + return 'ISO'; + } + })(options.weekNumberCalculation); + + + + // Calendar-specific Date Utilities + // ----------------------------------------------------------------------------------- + + + t.defaultAllDayEventDuration = moment.duration(options.defaultAllDayEventDuration); + t.defaultTimedEventDuration = moment.duration(options.defaultTimedEventDuration); + + + // Builds a moment using the settings of the current calendar: timezone and language. + // Accepts anything the vanilla moment() constructor accepts. + t.moment = function() { + var mom; + + if (options.timezone === 'local') { + mom = FC.moment.apply(null, arguments); + + // Force the moment to be local, because FC.moment doesn't guarantee it. + if (mom.hasTime()) { // don't give ambiguously-timed moments a local zone + mom.local(); + } + } + else if (options.timezone === 'UTC') { + mom = FC.moment.utc.apply(null, arguments); // process as UTC + } + else { + mom = FC.moment.parseZone.apply(null, arguments); // let the input decide the zone + } + + if ('_locale' in mom) { // moment 2.8 and above + mom._locale = localeData; + } + else { // pre-moment-2.8 + mom._lang = localeData; + } + + return mom; + }; + + + // Returns a boolean about whether or not the calendar knows how to calculate + // the timezone offset of arbitrary dates in the current timezone. + t.getIsAmbigTimezone = function() { + return options.timezone !== 'local' && options.timezone !== 'UTC'; + }; + + + // Returns a copy of the given date in the current timezone. Has no effect on dates without times. + t.applyTimezone = function(date) { + if (!date.hasTime()) { + return date.clone(); + } + var zonedDate = t.moment(date.toArray()); + var timeAdjust = date.time() - zonedDate.time(); + var adjustedZonedDate; + // Safari sometimes has problems with this coersion when near DST. Adjust if necessary. (bug #2396) + if (timeAdjust) { // is the time result different than expected? + adjustedZonedDate = zonedDate.clone().add(timeAdjust); // add milliseconds + if (date.time() - adjustedZonedDate.time() === 0) { // does it match perfectly now? + zonedDate = adjustedZonedDate; + } + } + + return zonedDate; + }; + + + // Returns a moment for the current date, as defined by the client's computer or from the `now` option. + // Will return an moment with an ambiguous timezone. + t.getNow = function() { + var now = options.now; + if (typeof now === 'function') { + now = now(); + } + return t.moment(now).stripZone(); + }; - /* Height "Freezing" - -----------------------------------------------------------------------------*/ - // TODO: move this into the view - t.freezeContentHeight = freezeContentHeight; - t.unfreezeContentHeight = unfreezeContentHeight; + // Get an event's normalized end date. If not present, calculate it from the defaults. + t.getEventEnd = function(event) { + if (event.end) { + return event.end.clone(); + } + else { + return t.getDefaultEventEnd(event.allDay, event.start); + } + }; - function freezeContentHeight() { - content.css({ - width: '100%', - height: content.height(), - overflow: 'hidden' - }); - } + // Given an event's allDay status and start date, return what its fallback end date should be. + // TODO: rename to computeDefaultEventEnd + t.getDefaultEventEnd = function(allDay, zonedStart) { + var end = zonedStart.clone(); + if (allDay) { + end.stripTime().add(t.defaultAllDayEventDuration); + } + else { + end.add(t.defaultTimedEventDuration); + } - function unfreezeContentHeight() { - content.css({ - width: '', - height: '', - overflow: '' - }); - } + if (t.getIsAmbigTimezone()) { + end.stripZone(); // we don't know what the tzo should be + } + return end; + }; + + + // Produces a human-readable string for the given duration. + // Side-effect: changes the locale of the given duration. + t.humanizeDuration = function(duration) { + return (duration.locale || duration.lang).call(duration, options.lang) // works moment-pre-2.8 + .humanize(); + }; - /* Misc - -----------------------------------------------------------------------------*/ + // Imports + // ----------------------------------------------------------------------------------- - function getCalendar() { - return t; - } + EventManager.call(t, options); + var isFetchNeeded = t.isFetchNeeded; + var fetchEvents = t.fetchEvents; - function getView() { - return currentView; - } - function option(name, value) { - if (value === undefined) { - return options[name]; - } - if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') { - options[name] = value; - updateSize(true); // true = allow recalculation of height - } - } + // Locals + // ----------------------------------------------------------------------------------- - function trigger(name, thisObj) { // overrides the Emitter's trigger method :( - var args = Array.prototype.slice.call(arguments, 2); + var _element = element[0]; + var header; + var headerElement; + var content; + var tm; // for making theme classes + var currentView; // NOTE: keep this in sync with this.view + var viewsByType = {}; // holds all instantiated view instances, current or not + var suggestedViewHeight; + var windowResizeProxy; // wraps the windowResize function + var ignoreWindowResize = 0; + var events = []; + var date; // unzoned - thisObj = thisObj || _element; - this.triggerWith(name, thisObj, args); // Emitter's method - if (options[name]) { - return options[name].apply(thisObj, args); - } - } - t.initialize(); -} + // Main Rendering + // ----------------------------------------------------------------------------------- -;; -Calendar.defaults = { + // compute the initial ambig-timezone date + if (options.defaultDate != null) { + date = t.moment(options.defaultDate).stripZone(); + } + else { + date = t.getNow(); // getNow already returns unzoned + } - 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, - - //nowIndicator: false, - - scrollTime: '06:00:00', - - // event ajax - lazyFetching: true, - startParam: 'start', - endParam: 'end', - timezoneParam: 'timezone', - - timezone: false, - - //allDayDefault: undefined, - - // locale - isRTL: false, - buttonText: { - prev: "prev", - next: "next", - prevYear: "prev year", - nextYear: "next year", - year: 'year', // TODO: locale files need to specify this - 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' - }, - - //eventResizableFromStart: false, - dragOpacity: .75, - dragRevertDuration: 500, - dragScroll: true, - - //selectable: false, - unselectAuto: true, - - dropAccept: '*', - - eventOrder: 'title', - - eventLimit: false, - eventLimitText: 'more', - eventLimitClick: 'popover', - dayPopoverFormat: 'LL', - - handleWindowResize: true, - windowResizeDelay: 200, // milliseconds before an updateSize happens - - longPressDelay: 1000 - -}; - - -Calendar.englishDefaults = { // used by lang.js - dayPopoverFormat: 'dddd, MMMM D' -}; - - -Calendar.rtlDefaults = { // right-to-left defaults - header: { // TODO: smarter solution (first/center/last ?) - 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' - } -}; + function render() { + if (!content) { + initialRender(); + } + else if (elementVisible()) { + // mainly for the public API + calcSize(); + renderView(); + } + } -;; -var langOptionHash = FC.langs = {}; // initialize and expose + function initialRender() { + tm = options.theme ? 'ui' : 'fc'; + element.addClass('fc'); + element.addClass( + t.isTouch ? 'fc-touch' : 'fc-cursor' + ); -// TODO: document the structure and ordering of a FullCalendar lang file -// TODO: rename everything "lang" to "locale", like what the moment project did + if (options.isRTL) { + element.addClass('fc-rtl'); + } + else { + element.addClass('fc-ltr'); + } + if (options.theme) { + element.addClass('ui-widget'); + } + else { + element.addClass('fc-unthemed'); + } -// 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) { + content = $("
").prependTo(element); - // get the FullCalendar internal option hash for this language. create if necessary - var fcOptions = langOptionHash[langCode] || (langOptionHash[langCode] = {}); + header = t.header = new Header(t, options); + headerElement = header.render(); + if (headerElement) { + element.prepend(headerElement); + } - // transfer some simple options from datepicker to fc - fcOptions.isRTL = dpOptions.isRTL; - fcOptions.weekNumberTitle = dpOptions.weekHeader; + renderView(options.defaultView); - // compute some more complex options from datepicker - $.each(dpComputableOptions, function(name, func) { - fcOptions[name] = func(dpOptions); - }); + if (options.handleWindowResize) { + windowResizeProxy = debounce(windowResize, options.windowResizeDelay); // prevents rapid calls + $(window).resize(windowResizeProxy); + } + } - // 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; + function destroy() { - // Alias 'en' to the default language data. Do this every time. - $.datepicker.regional.en = $.datepicker.regional['']; + if (currentView) { + currentView.removeElement(); - // Set as Datepicker's global defaults. - $.datepicker.setDefaults(dpOptions); - } -}; + // NOTE: don't null-out currentView/t.view in case API methods are called after destroy. + // It is still the "current" view, just not rendered. + } + header.removeElement(); + content.remove(); + element.removeClass('fc fc-touch fc-cursor fc-ltr fc-rtl fc-unthemed ui-widget'); -// Sets FullCalendar-specific translations. Will set the language as the global default. -FC.lang = function(langCode, newFcOptions) { - var fcOptions; - var momOptions; + if (windowResizeProxy) { + $(window).unbind('resize', windowResizeProxy); + } + } - // 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) { - fcOptions = langOptionHash[langCode] = mergeOptions([ fcOptions, newFcOptions ]); - } + function elementVisible() { + return element.is(':visible'); + } - // 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] == null) { - fcOptions[name] = func(momOptions, fcOptions); - } - }); - - // set it as the default language for FullCalendar - Calendar.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 = { - - buttonText: 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 M/D" -> "Fri 9/15" - 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:mma" -> "6:00pm" - mediumTimeFormat: function(momOptions) { // can't be called `timeFormat` because collides with option - return momOptions.longDateFormat('LT') - .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand - }, - - // Produces format strings like "h(:mm)a" -> "6pm" / "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" / "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 "ha" / "H" -> "6pm" / "18" - hourFormat: function(momOptions) { - return momOptions.longDateFormat('LT') - .replace(':mm', '') - .replace(/(\Wmm)$/, '') // 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" -> "6:30" (with no AM/PM) - noMeridiemTimeFormat: function(momOptions) { - return momOptions.longDateFormat('LT') - .replace(/\s*a$/i, ''); // remove trailing AM/PM - } -}; - - -// options that should be computed off live calendar options (considers override options) -// TODO: best place for this? related to lang? -// TODO: flipping text based on isRTL is a bad idea because the CSS `direction` might want to handle it -var instanceComputableOptions = { - - // Produces format strings for results like "Mo 16" - smallDayDateFormat: function(options) { - return options.isRTL ? - 'D dd' : - 'dd D'; - }, - - // Produces format strings for results like "Wk 5" - weekFormat: function(options) { - return options.isRTL ? - 'w[ ' + options.weekNumberTitle + ']' : - '[' + options.weekNumberTitle + ' ]w'; - }, - - // Produces format strings for results like "Wk5" - smallWeekFormat: function(options) { - return options.isRTL ? - 'w[' + options.weekNumberTitle + ']' : - '[' + options.weekNumberTitle + ']w'; - } + // View Rendering + // ----------------------------------------------------------------------------------- -}; -function populateInstanceComputableOptions(options) { - $.each(instanceComputableOptions, function(name, func) { - if (options[name] == null) { - options[name] = func(options); - } - }); -} + // Renders a view because of a date change, view-type change, or for the first time. + // If not given a viewType, keep the current view but render different dates. + function renderView(viewType) { + ignoreWindowResize++; + // if viewType is changing, remove the old view's rendering + if (currentView && viewType && currentView.type !== viewType) { + header.deactivateButton(currentView.type); + freezeContentHeight(); // prevent a scroll jump when view element is removed + currentView.removeElement(); + currentView = t.view = null; + } -// 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 -} + // if viewType changed, or the view was never created, create a fresh view + if (!currentView && viewType) { + currentView = t.view = + viewsByType[viewType] || + (viewsByType[viewType] = t.instantiateView(viewType)); + currentView.setElement( + $("
").appendTo(content) + ); + header.activateButton(viewType); + } -// Initialize English by forcing computation of moment-derived options. -// Also, sets it as the default. -FC.lang('en', Calendar.englishDefaults); + if (currentView) { -;; + // in case the view should render a period of time that is completely hidden + date = currentView.massageCurrentDate(date); -/* Top toolbar area with buttons and title -----------------------------------------------------------------------------------------------------------------------*/ -// TODO: rename all header-related things to "toolbar" + // render or rerender the view + if ( + !currentView.displaying || + !date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change + ) { + if (elementVisible()) { -function Header(calendar, options) { - var t = this; + currentView.display(date); // will call freezeContentHeight + unfreezeContentHeight(); // immediately unfreeze regardless of whether display is async - // exports - t.render = render; - t.removeElement = removeElement; - t.updateTitle = updateTitle; - t.activateButton = activateButton; - t.deactivateButton = deactivateButton; - t.disableButton = disableButton; - t.enableButton = enableButton; - t.getViewsWithButtons = getViewsWithButtons; + // need to do this after View::render, so dates are calculated + updateHeaderTitle(); + updateTodayButton(); - // locals - var el = $(); - var viewsWithButtons = []; - var tm; + getAndRenderEvents(); + } + } + } + unfreezeContentHeight(); // undo any lone freezeContentHeight calls + ignoreWindowResize--; + } - function render() { - var sections = options.header; - tm = options.theme ? 'ui' : 'fc'; - if (sections) { - el = $("
") - .append(renderSection('left')) - .append(renderSection('right')) - .append(renderSection('center')) - .append('
'); + // Resizing + // ----------------------------------------------------------------------------------- - return el; - } - } + t.getSuggestedViewHeight = function() { + if (suggestedViewHeight === undefined) { + calcSize(); + } + return suggestedViewHeight; + }; - function removeElement() { - el.remove(); - el = $(); - } + t.isHeightAuto = function() { + return options.contentHeight === 'auto' || options.height === 'auto'; + }; - function renderSection(position) { - var sectionEl = $('
'); - var buttonStr = options.header[position]; - - if (buttonStr) { - $.each(buttonStr.split(' '), function(i) { - var groupChildren = $(); - var isOnlyButtons = true; - var groupEl; - - $.each(this.split(','), function(j, buttonName) { - var customButtonProps; - var viewSpec; - var buttonClick; - var overrideText; // text explicitly set by calendar's constructor options. overcomes icons - var defaultText; - var themeIcon; - var normalIcon; - var innerHtml; - var classes; - var button; // the element - - if (buttonName == 'title') { - groupChildren = groupChildren.add($('

 

')); // we always want it to take up height - isOnlyButtons = false; - } - else { - if ((customButtonProps = (calendar.options.customButtons || {})[buttonName])) { - buttonClick = function(ev) { - if (customButtonProps.click) { - customButtonProps.click.call(button[0], ev); - } - }; - overrideText = ''; // icons will override text - defaultText = customButtonProps.text; - } - else if ((viewSpec = calendar.getViewSpec(buttonName))) { - buttonClick = function() { - calendar.changeView(buttonName); - }; - viewsWithButtons.push(buttonName); - overrideText = viewSpec.buttonTextOverride; - defaultText = viewSpec.buttonTextDefault; - } - else if (calendar[buttonName]) { // a calendar method - buttonClick = function() { - calendar[buttonName](); - }; - overrideText = (calendar.overrides.buttonText || {})[buttonName]; - defaultText = options.buttonText[buttonName]; // everything else is considered default - } - - if (buttonClick) { - - themeIcon = - customButtonProps ? - customButtonProps.themeIcon : - options.themeButtonIcons[buttonName]; - - normalIcon = - customButtonProps ? - customButtonProps.icon : - options.buttonIcons[buttonName]; - - if (overrideText) { - innerHtml = htmlEscape(overrideText); - } - else if (themeIcon && options.theme) { - innerHtml = ""; - } - else if (normalIcon && !options.theme) { - innerHtml = ""; - } - else { - innerHtml = htmlEscape(defaultText); - } - - classes = [ - 'fc-' + buttonName + '-button', - tm + '-button', - tm + '-state-default' - ]; - - button = $( // type="button" so that it doesn't submit a form - '' - ) - .click(function(ev) { - // don't process clicks for disabled buttons - if (!button.hasClass(tm + '-state-disabled')) { - - buttonClick(ev); - - // after the click action, if the button becomes the "active" tab, or disabled, - // it should never have a hover class, so remove it now. - if ( - button.hasClass(tm + '-state-active') || - button.hasClass(tm + '-state-disabled') - ) { - button.removeClass(tm + '-state-hover'); - } - } - }) - .mousedown(function() { - // the *down* effect (mouse pressed in). - // only on buttons that are not the "active" tab, or disabled - button - .not('.' + tm + '-state-active') - .not('.' + tm + '-state-disabled') - .addClass(tm + '-state-down'); - }) - .mouseup(function() { - // undo the *down* effect - button.removeClass(tm + '-state-down'); - }) - .hover( - function() { - // the *hover* effect. - // only on buttons that are not the "active" tab, or disabled - button - .not('.' + tm + '-state-active') - .not('.' + tm + '-state-disabled') - .addClass(tm + '-state-hover'); - }, - function() { - // undo the *hover* effect - button - .removeClass(tm + '-state-hover') - .removeClass(tm + '-state-down'); // if mouseleave happens before mouseup - } - ); - - groupChildren = groupChildren.add(button); - } - } - }); - - if (isOnlyButtons) { - groupChildren - .first().addClass(tm + '-corner-left').end() - .last().addClass(tm + '-corner-right').end(); - } - - if (groupChildren.length > 1) { - groupEl = $('
'); - if (isOnlyButtons) { - groupEl.addClass('fc-button-group'); - } - groupEl.append(groupChildren); - sectionEl.append(groupEl); - } - else { - sectionEl.append(groupChildren); // 1 or 0 children - } - }); - } - - return sectionEl; - } + function updateSize(shouldRecalc) { + if (elementVisible()) { - function updateTitle(text) { - el.find('h2').text(text); - } + if (shouldRecalc) { + _calcSize(); + } + ignoreWindowResize++; + currentView.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto() + ignoreWindowResize--; - function activateButton(buttonName) { - el.find('.fc-' + buttonName + '-button') - .addClass(tm + '-state-active'); - } + return true; // signal success + } + } - function deactivateButton(buttonName) { - el.find('.fc-' + buttonName + '-button') - .removeClass(tm + '-state-active'); - } + function calcSize() { + if (elementVisible()) { + _calcSize(); + } + } - function disableButton(buttonName) { - el.find('.fc-' + buttonName + '-button') - .attr('disabled', 'disabled') - .addClass(tm + '-state-disabled'); - } + function _calcSize() { // assumes elementVisible + if (typeof options.contentHeight === 'number') { // exists and not 'auto' + suggestedViewHeight = options.contentHeight; + } + else if (typeof options.height === 'number') { // exists and not 'auto' + suggestedViewHeight = options.height - (headerElement ? headerElement.outerHeight(true) : 0); + } + else { + suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5)); + } + } - function enableButton(buttonName) { - el.find('.fc-' + buttonName + '-button') - .removeAttr('disabled') - .removeClass(tm + '-state-disabled'); - } + function windowResize(ev) { + if ( + !ignoreWindowResize && + ev.target === window && // so we don't process jqui "resize" events that have bubbled up + currentView.start // view has already been rendered + ) { + if (updateSize(true)) { + currentView.trigger('windowResize', _element); + } + } + } - function getViewsWithButtons() { - return viewsWithButtons; - } -} + /* Event Fetching/Rendering + -----------------------------------------------------------------------------*/ + // TODO: going forward, most of this stuff should be directly handled by the view -;; -FC.sourceNormalizers = []; -FC.sourceFetchers = []; + function refetchEvents() { // can be called as an API method + destroyEvents(); // so that events are cleared before user starts waiting for AJAX + fetchAndRenderEvents(); + } -var ajaxDefaults = { - dataType: 'json', - cache: false -}; -var eventGUID = 1; + function renderEvents() { // destroys old events if previously rendered + if (elementVisible()) { + freezeContentHeight(); + currentView.displayEvents(events); + unfreezeContentHeight(); + } + } -function EventManager(options) { // assumed to be a calendar - var t = this; + function destroyEvents() { + freezeContentHeight(); + currentView.clearEvents(); + unfreezeContentHeight(); + } - // exports - t.isFetchNeeded = isFetchNeeded; - t.fetchEvents = fetchEvents; - t.addEventSource = addEventSource; - t.removeEventSource = removeEventSource; - t.updateEvent = updateEvent; - t.renderEvent = renderEvent; - t.removeEvents = removeEvents; - t.clientEvents = clientEvents; - t.mutateEvent = mutateEvent; - t.normalizeEventDates = normalizeEventDates; - t.normalizeEventTimes = normalizeEventTimes; + function getAndRenderEvents() { + if (!options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) { + fetchAndRenderEvents(); + } + else { + renderEvents(); + } + } - // imports - var reportEvents = t.reportEvents; + function fetchAndRenderEvents() { + fetchEvents(currentView.start, currentView.end); + // ... will call reportEvents + // ... which will call renderEvents + } - // locals - var stickySource = { events: [] }; - var sources = [ stickySource ]; - var rangeStart, rangeEnd; - var currentFetchID = 0; - var pendingSourceCnt = 0; - var cache = []; // holds events that have already been expanded + // called when event data arrives + function reportEvents(_events) { + events = _events; + renderEvents(); + } - $.each( - (options.events ? [ options.events ] : []).concat(options.eventSources || []), - function(i, sourceInput) { - var source = buildEventSource(sourceInput); - if (source) { - sources.push(source); - } - } - ); + // called when a single event's data has been changed + function reportEventChange() { + renderEvents(); + } - /* Fetching - -----------------------------------------------------------------------------*/ + /* Header Updating + -----------------------------------------------------------------------------*/ - // start and end are assumed to be unzoned - function isFetchNeeded(start, end) { - return !rangeStart || // nothing has been fetched yet? - start < rangeStart || end > rangeEnd; // is part of the new range outside of the old range? - } + function updateHeaderTitle() { + header.updateTitle(currentView.title); + } - function fetchEvents(start, end) { - rangeStart = start; - rangeEnd = end; - cache = []; - var fetchID = ++currentFetchID; - var len = sources.length; - pendingSourceCnt = len; - for (var i=0; i "September 2014" + monthYearFormat: function(dpOptions) { + return dpOptions.showMonthAfterYear ? + 'YYYY[' + dpOptions.yearSuffix + '] MMMM' : + 'MMMM YYYY[' + dpOptions.yearSuffix + ']'; + } + + }; + + var momComputableOptions = { + + // Produces format strings like "ddd M/D" -> "Fri 9/15" + 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:mma" -> "6:00pm" + mediumTimeFormat: function(momOptions) { // can't be called `timeFormat` because collides with option + return momOptions.longDateFormat('LT') + .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand + }, + + // Produces format strings like "h(:mm)a" -> "6pm" / "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" / "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 "ha" / "H" -> "6pm" / "18" + hourFormat: function(momOptions) { + return momOptions.longDateFormat('LT') + .replace(':mm', '') + .replace(/(\Wmm)$/, '') // 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" -> "6:30" (with no AM/PM) + noMeridiemTimeFormat: function(momOptions) { + return momOptions.longDateFormat('LT') + .replace(/\s*a$/i, ''); // remove trailing AM/PM + } + + }; + + + // options that should be computed off live calendar options (considers override options) + // TODO: best place for this? related to lang? + // TODO: flipping text based on isRTL is a bad idea because the CSS `direction` might want to handle it + var instanceComputableOptions = { + + // Produces format strings for results like "Mo 16" + smallDayDateFormat: function(options) { + return options.isRTL ? + 'D dd' : + 'dd D'; + }, + + // Produces format strings for results like "Wk 5" + weekFormat: function(options) { + return options.isRTL ? + 'w[ ' + options.weekNumberTitle + ']' : + '[' + options.weekNumberTitle + ' ]w'; + }, + + // Produces format strings for results like "Wk5" + smallWeekFormat: function(options) { + return options.isRTL ? + 'w[' + options.weekNumberTitle + ']' : + '[' + options.weekNumberTitle + ']w'; + } + + }; + + function populateInstanceComputableOptions(options) { + $.each(instanceComputableOptions, function(name, func) { + if (options[name] == null) { + options[name] = func(options); + } + }); + } + + + // 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', Calendar.englishDefaults); + + ;; + + /* Top toolbar area with buttons and title + ----------------------------------------------------------------------------------------------------------------------*/ + // TODO: rename all header-related things to "toolbar" + + function Header(calendar, options) { + var t = this; + + // exports + t.render = render; + t.removeElement = removeElement; + t.updateTitle = updateTitle; + t.activateButton = activateButton; + t.deactivateButton = deactivateButton; + t.disableButton = disableButton; + t.enableButton = enableButton; + t.getViewsWithButtons = getViewsWithButtons; + + // locals + var el = $(); + var viewsWithButtons = []; + var tm; + + + function render() { + var sections = options.header; + + tm = options.theme ? 'ui' : 'fc'; + + if (sections) { + el = $("
") + .append(renderSection('left')) + .append(renderSection('right')) + .append(renderSection('center')) + .append('
'); + + return el; + } + } + + + function removeElement() { + el.remove(); + el = $(); + } + + + function renderSection(position) { + var sectionEl = $('
'); + var buttonStr = options.header[position]; + + if (buttonStr) { + $.each(buttonStr.split(' '), function(i) { + var groupChildren = $(); + var isOnlyButtons = true; + var groupEl; + + $.each(this.split(','), function(j, buttonName) { + var customButtonProps; + var viewSpec; + var buttonClick; + var overrideText; // text explicitly set by calendar's constructor options. overcomes icons + var defaultText; + var themeIcon; + var normalIcon; + var innerHtml; + var classes; + var button; // the element + + if (buttonName == 'title') { + groupChildren = groupChildren.add($('

 

')); // we always want it to take up height + isOnlyButtons = false; + } + else { + if ((customButtonProps = (calendar.options.customButtons || {})[buttonName])) { + buttonClick = function(ev) { + if (customButtonProps.click) { + customButtonProps.click.call(button[0], ev); + } + }; + overrideText = ''; // icons will override text + defaultText = customButtonProps.text; + } + else if ((viewSpec = calendar.getViewSpec(buttonName))) { + buttonClick = function() { + calendar.changeView(buttonName); + }; + viewsWithButtons.push(buttonName); + overrideText = viewSpec.buttonTextOverride; + defaultText = viewSpec.buttonTextDefault; + } + else if (calendar[buttonName]) { // a calendar method + buttonClick = function() { + calendar[buttonName](); + }; + overrideText = (calendar.overrides.buttonText || {})[buttonName]; + defaultText = options.buttonText[buttonName]; // everything else is considered default + } + + if (buttonClick) { + + themeIcon = + customButtonProps ? + customButtonProps.themeIcon : + options.themeButtonIcons[buttonName]; + + normalIcon = + customButtonProps ? + customButtonProps.icon : + options.buttonIcons[buttonName]; + + if (overrideText) { + innerHtml = htmlEscape(overrideText); + } + else if (themeIcon && options.theme) { + innerHtml = ""; + } + else if (normalIcon && !options.theme) { + innerHtml = ""; + } + else { + innerHtml = htmlEscape(defaultText); + } + + classes = [ + 'fc-' + buttonName + '-button', + tm + '-button', + tm + '-state-default' + ]; + + button = $( // type="button" so that it doesn't submit a form + '' + ) + .click(function(ev) { + // don't process clicks for disabled buttons + if (!button.hasClass(tm + '-state-disabled')) { + + buttonClick(ev); + + // after the click action, if the button becomes the "active" tab, or disabled, + // it should never have a hover class, so remove it now. + if ( + button.hasClass(tm + '-state-active') || + button.hasClass(tm + '-state-disabled') + ) { + button.removeClass(tm + '-state-hover'); + } + } + }) + .mousedown(function() { + // the *down* effect (mouse pressed in). + // only on buttons that are not the "active" tab, or disabled + button + .not('.' + tm + '-state-active') + .not('.' + tm + '-state-disabled') + .addClass(tm + '-state-down'); + }) + .mouseup(function() { + // undo the *down* effect + button.removeClass(tm + '-state-down'); + }) + .hover( + function() { + // the *hover* effect. + // only on buttons that are not the "active" tab, or disabled + button + .not('.' + tm + '-state-active') + .not('.' + tm + '-state-disabled') + .addClass(tm + '-state-hover'); + }, + function() { + // undo the *hover* effect + button + .removeClass(tm + '-state-hover') + .removeClass(tm + '-state-down'); // if mouseleave happens before mouseup + } + ); + + groupChildren = groupChildren.add(button); + } + } + }); + + if (isOnlyButtons) { + groupChildren + .first().addClass(tm + '-corner-left').end() + .last().addClass(tm + '-corner-right').end(); + } + + if (groupChildren.length > 1) { + groupEl = $('
'); + if (isOnlyButtons) { + groupEl.addClass('fc-button-group'); + } + groupEl.append(groupChildren); + sectionEl.append(groupEl); + } + else { + sectionEl.append(groupChildren); // 1 or 0 children + } + }); + } + + return sectionEl; + } + + + function updateTitle(text) { + el.find('h2').text(text); + } + + + function activateButton(buttonName) { + el.find('.fc-' + buttonName + '-button') + .addClass(tm + '-state-active'); + } + + + function deactivateButton(buttonName) { + el.find('.fc-' + buttonName + '-button') + .removeClass(tm + '-state-active'); + } + + + function disableButton(buttonName) { + el.find('.fc-' + buttonName + '-button') + .attr('disabled', 'disabled') + .addClass(tm + '-state-disabled'); + } + + + function enableButton(buttonName) { + el.find('.fc-' + buttonName + '-button') + .removeAttr('disabled') + .removeClass(tm + '-state-disabled'); + } + + + function getViewsWithButtons() { + return viewsWithButtons; + } + + } + + ;; + + FC.sourceNormalizers = []; + FC.sourceFetchers = []; + + var ajaxDefaults = { + dataType: 'json', + cache: false + }; + + var eventGUID = 1; + + + function EventManager(options) { // assumed to be a calendar + var t = this; + + + // exports + t.isFetchNeeded = isFetchNeeded; + t.fetchEvents = fetchEvents; + t.addEventSource = addEventSource; + t.removeEventSource = removeEventSource; + t.updateEvent = updateEvent; + t.renderEvent = renderEvent; + t.removeEvents = removeEvents; + t.clientEvents = clientEvents; + t.mutateEvent = mutateEvent; + t.normalizeEventDates = normalizeEventDates; + t.normalizeEventTimes = normalizeEventTimes; + + + // imports + var reportEvents = t.reportEvents; + + + // locals + var stickySource = { events: [] }; + var sources = [ stickySource ]; + var rangeStart, rangeEnd; + var currentFetchID = 0; + var pendingSourceCnt = 0; + var cache = []; // holds events that have already been expanded + + + $.each( + (options.events ? [ options.events ] : []).concat(options.eventSources || []), + function(i, sourceInput) { + var source = buildEventSource(sourceInput); + if (source) { + sources.push(source); + } + } + ); + + + + /* Fetching + -----------------------------------------------------------------------------*/ + + + // start and end are assumed to be unzoned + function isFetchNeeded(start, end) { + return !rangeStart || // nothing has been fetched yet? + start < rangeStart || end > rangeEnd; // is part of the new range outside of the old range? + } + + + function fetchEvents(start, end) { + rangeStart = start; + rangeEnd = end; + cache = []; + var fetchID = ++currentFetchID; + var len = sources.length; + pendingSourceCnt = len; + for (var i=0; i= eventStart && range.end <= eventEnd; + } - // Does the event's date range fully contain the given range? - // start/end already assumed to have stripped zones :( - function eventContainsRange(event, range) { - var eventStart = event.start.clone().stripZone(); - var eventEnd = t.getEventEnd(event).stripZone(); + // Does the event's date range intersect with the given range? + // start/end already assumed to have stripped zones :( + function eventIntersectsRange(event, range) { + var eventStart = event.start.clone().stripZone(); + var eventEnd = t.getEventEnd(event).stripZone(); - return range.start >= eventStart && range.end <= eventEnd; - } + return range.start < eventEnd && range.end > eventStart; + } - // Does the event's date range intersect with the given range? - // start/end already assumed to have stripped zones :( - function eventIntersectsRange(event, range) { - var eventStart = event.start.clone().stripZone(); - var eventEnd = t.getEventEnd(event).stripZone(); + t.getEventCache = function() { + return cache; + }; - return range.start < eventEnd && range.end > eventStart; - } + } - t.getEventCache = function() { - return cache; - }; + // Returns a list of events that the given event should be compared against when being considered for a move to + // the specified span. Attached to the Calendar's prototype because EventManager is a mixin for a Calendar. + Calendar.prototype.getPeerEvents = function(span, event) { + var cache = this.getEventCache(); + var peerEvents = []; + var i, otherEvent; -} + for (i = 0; i < cache.length; i++) { + otherEvent = cache[i]; + if ( + !event || + event._id !== otherEvent._id // don't compare the event to itself or other related [repeating] events + ) { + peerEvents.push(otherEvent); + } + } + return peerEvents; + }; -// Returns a list of events that the given event should be compared against when being considered for a move to -// the specified span. Attached to the Calendar's prototype because EventManager is a mixin for a Calendar. -Calendar.prototype.getPeerEvents = function(span, event) { - var cache = this.getEventCache(); - var peerEvents = []; - var i, otherEvent; - for (i = 0; i < cache.length; i++) { - otherEvent = cache[i]; - if ( - !event || - event._id !== otherEvent._id // don't compare the event to itself or other related [repeating] events - ) { - peerEvents.push(otherEvent); - } - } + // updates the "backup" properties, which are preserved in order to compute diffs later on. + function backupEventDates(event) { + event._allDay = event.allDay; + event._start = event.start.clone(); + event._end = event.end ? event.end.clone() : null; + } - return peerEvents; -}; + ;; + /* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells. + ----------------------------------------------------------------------------------------------------------------------*/ + // It is a manager for a DayGrid subcomponent, which does most of the heavy lifting. + // It is responsible for managing width/height. -// updates the "backup" properties, which are preserved in order to compute diffs later on. -function backupEventDates(event) { - event._allDay = event.allDay; - event._start = event.start.clone(); - event._end = event.end ? event.end.clone() : null; -} + var BasicView = FC.BasicView = View.extend({ -;; + scroller: null, -/* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells. -----------------------------------------------------------------------------------------------------------------------*/ -// It is a manager for a DayGrid subcomponent, which does most of the heavy lifting. -// It is responsible for managing width/height. + dayGridClass: DayGrid, // class the dayGrid will be instantiated from (overridable by subclasses) + dayGrid: null, // the main subcomponent that does most of the heavy lifting -var BasicView = FC.BasicView = View.extend({ + dayNumbersVisible: false, // display day numbers on each day cell? + weekNumbersVisible: false, // display week numbers along the side? - scroller: null, + weekNumberWidth: null, // width of all the week-number cells running down the side - dayGridClass: DayGrid, // class the dayGrid will be instantiated from (overridable by subclasses) - dayGrid: null, // the main subcomponent that does most of the heavy lifting + headContainerEl: null, // div that hold's the dayGrid's rendered date header + headRowEl: null, // the fake row element of the day-of-week header - dayNumbersVisible: false, // display day numbers on each day cell? - weekNumbersVisible: false, // display week numbers along the side? - weekNumberWidth: null, // width of all the week-number cells running down the side + initialize: function() { + this.dayGrid = this.instantiateDayGrid(); - headContainerEl: null, // div that hold's the dayGrid's rendered date header - headRowEl: null, // the fake row element of the day-of-week header + this.scroller = new Scroller({ + overflowX: 'hidden', + overflowY: 'auto' + }); + }, - initialize: function() { - this.dayGrid = this.instantiateDayGrid(); + // Generates the DayGrid object this view needs. Draws from this.dayGridClass + instantiateDayGrid: function() { + // generate a subclass on the fly with BasicView-specific behavior + // TODO: cache this subclass + var subclass = this.dayGridClass.extend(basicDayGridMethods); - this.scroller = new Scroller({ - overflowX: 'hidden', - overflowY: 'auto' - }); - }, + return new subclass(this); + }, - // Generates the DayGrid object this view needs. Draws from this.dayGridClass - instantiateDayGrid: function() { - // generate a subclass on the fly with BasicView-specific behavior - // TODO: cache this subclass - var subclass = this.dayGridClass.extend(basicDayGridMethods); + // Sets the display range and computes all necessary dates + setRange: function(range) { + View.prototype.setRange.call(this, range); // call the super-method - return new subclass(this); - }, + this.dayGrid.breakOnWeeks = /year|month|week/.test(this.intervalUnit); // do before setRange + this.dayGrid.setRange(range); + }, - // Sets the display range and computes all necessary dates - setRange: function(range) { - View.prototype.setRange.call(this, range); // call the super-method + // Compute the value to feed into setRange. Overrides superclass. + computeRange: function(date) { + var range = View.prototype.computeRange.call(this, date); // get value from the super-method - this.dayGrid.breakOnWeeks = /year|month|week/.test(this.intervalUnit); // do before setRange - this.dayGrid.setRange(range); - }, + // year and month views should be aligned with weeks. this is already done for week + if (/year|month/.test(range.intervalUnit)) { + range.start.startOf('week'); + range.start = this.skipHiddenDays(range.start); + // make end-of-week if not already + if (range.end.weekday()) { + range.end.add(1, 'week').startOf('week'); + range.end = this.skipHiddenDays(range.end, -1, true); // exclusively move backwards + } + } - // Compute the value to feed into setRange. Overrides superclass. - computeRange: function(date) { - var range = View.prototype.computeRange.call(this, date); // get value from the super-method + return range; + }, - // year and month views should be aligned with weeks. this is already done for week - if (/year|month/.test(range.intervalUnit)) { - range.start.startOf('week'); - range.start = this.skipHiddenDays(range.start); - // make end-of-week if not already - if (range.end.weekday()) { - range.end.add(1, 'week').startOf('week'); - range.end = this.skipHiddenDays(range.end, -1, true); // exclusively move backwards - } - } + // Renders the view into `this.el`, which should already be assigned + renderDates: function() { - return range; - }, + this.dayNumbersVisible = this.dayGrid.rowCnt > 1; // TODO: make grid responsible + this.weekNumbersVisible = this.opt('weekNumbers'); + this.dayGrid.numbersVisible = this.dayNumbersVisible || this.weekNumbersVisible; + this.el.addClass('fc-basic-view').html(this.renderSkeletonHtml()); + this.renderHead(); - // Renders the view into `this.el`, which should already be assigned - renderDates: function() { + this.scroller.render(); + var dayGridContainerEl = this.scroller.el.addClass('fc-day-grid-container'); + var dayGridEl = $('
').appendTo(dayGridContainerEl); + this.el.find('.fc-body > tr > td').append(dayGridContainerEl); - this.dayNumbersVisible = this.dayGrid.rowCnt > 1; // TODO: make grid responsible - this.weekNumbersVisible = this.opt('weekNumbers'); - this.dayGrid.numbersVisible = this.dayNumbersVisible || this.weekNumbersVisible; + this.dayGrid.setElement(dayGridEl); + this.dayGrid.renderDates(this.hasRigidRows()); + }, - this.el.addClass('fc-basic-view').html(this.renderSkeletonHtml()); - this.renderHead(); - this.scroller.render(); - var dayGridContainerEl = this.scroller.el.addClass('fc-day-grid-container'); - var dayGridEl = $('
').appendTo(dayGridContainerEl); - this.el.find('.fc-body > tr > td').append(dayGridContainerEl); + // render the day-of-week headers + renderHead: function() { + this.headContainerEl = + this.el.find('.fc-head-container') + .html(this.dayGrid.renderHeadHtml()); + this.headRowEl = this.headContainerEl.find('.fc-row'); + }, - this.dayGrid.setElement(dayGridEl); - this.dayGrid.renderDates(this.hasRigidRows()); - }, + // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering, + // always completely kill the dayGrid's rendering. + unrenderDates: function() { + this.dayGrid.unrenderDates(); + this.dayGrid.removeElement(); + this.scroller.destroy(); + }, - // render the day-of-week headers - renderHead: function() { - this.headContainerEl = - this.el.find('.fc-head-container') - .html(this.dayGrid.renderHeadHtml()); - this.headRowEl = this.headContainerEl.find('.fc-row'); - }, + renderBusinessHours: function() { + this.dayGrid.renderBusinessHours(); + }, - // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering, - // always completely kill the dayGrid's rendering. - unrenderDates: function() { - this.dayGrid.unrenderDates(); - this.dayGrid.removeElement(); - this.scroller.destroy(); - }, + // Builds the HTML skeleton for the view. + // The day-grid component will render inside of a container defined by this HTML. + renderSkeletonHtml: function() { + return '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
'; + }, - renderBusinessHours: function() { - this.dayGrid.renderBusinessHours(); - }, + // Generates an HTML attribute string for setting the width of the week number column, if it is known + weekNumberStyleAttr: function() { + if (this.weekNumberWidth !== null) { + return 'style="width:' + this.weekNumberWidth + 'px"'; + } + return ''; + }, - // Builds the HTML skeleton for the view. - // The day-grid component will render inside of a container defined by this HTML. - renderSkeletonHtml: function() { - return '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '
'; - }, + // Determines whether each row should have a constant height + hasRigidRows: function() { + var eventLimit = this.opt('eventLimit'); + return eventLimit && typeof eventLimit !== 'number'; + }, - // Generates an HTML attribute string for setting the width of the week number column, if it is known - weekNumberStyleAttr: function() { - if (this.weekNumberWidth !== null) { - return 'style="width:' + this.weekNumberWidth + 'px"'; - } - return ''; - }, + /* Dimensions + ------------------------------------------------------------------------------------------------------------------*/ - // Determines whether each row should have a constant height - hasRigidRows: function() { - var eventLimit = this.opt('eventLimit'); - return eventLimit && typeof eventLimit !== 'number'; - }, + // Refreshes the horizontal dimensions of the view + updateWidth: function() { + if (this.weekNumbersVisible) { + // Make sure all week number cells running down the side have the same width. + // Record the width for cells created later. + this.weekNumberWidth = matchCellWidths( + this.el.find('.fc-week-number') + ); + } + }, - /* Dimensions - ------------------------------------------------------------------------------------------------------------------*/ + // Adjusts the vertical dimensions of the view to the specified values + setHeight: function(totalHeight, isAuto) { + var eventLimit = this.opt('eventLimit'); + var scrollerHeight; + var scrollbarWidths; - // Refreshes the horizontal dimensions of the view - updateWidth: function() { - if (this.weekNumbersVisible) { - // Make sure all week number cells running down the side have the same width. - // Record the width for cells created later. - this.weekNumberWidth = matchCellWidths( - this.el.find('.fc-week-number') - ); - } - }, + // reset all heights to be natural + this.scroller.clear(); + uncompensateScroll(this.headRowEl); + this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed - // Adjusts the vertical dimensions of the view to the specified values - setHeight: function(totalHeight, isAuto) { - var eventLimit = this.opt('eventLimit'); - var scrollerHeight; - var scrollbarWidths; + // is the event limit a constant level number? + if (eventLimit && typeof eventLimit === 'number') { + this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after + } - // reset all heights to be natural - this.scroller.clear(); - uncompensateScroll(this.headRowEl); + // distribute the height to the rows + // (totalHeight is a "recommended" value if isAuto) + scrollerHeight = this.computeScrollerHeight(totalHeight); + this.setGridHeight(scrollerHeight, isAuto); - this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed + // is the event limit dynamically calculated? + if (eventLimit && typeof eventLimit !== 'number') { + this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set + } - // is the event limit a constant level number? - if (eventLimit && typeof eventLimit === 'number') { - this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after - } + if (!isAuto) { // should we force dimensions of the scroll container? - // distribute the height to the rows - // (totalHeight is a "recommended" value if isAuto) - scrollerHeight = this.computeScrollerHeight(totalHeight); - this.setGridHeight(scrollerHeight, isAuto); + this.scroller.setHeight(scrollerHeight); + scrollbarWidths = this.scroller.getScrollbarWidths(); - // is the event limit dynamically calculated? - if (eventLimit && typeof eventLimit !== 'number') { - this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set - } + if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars? - if (!isAuto) { // should we force dimensions of the scroll container? + compensateScroll(this.headRowEl, scrollbarWidths); - this.scroller.setHeight(scrollerHeight); - scrollbarWidths = this.scroller.getScrollbarWidths(); + // doing the scrollbar compensation might have created text overflow which created more height. redo + scrollerHeight = this.computeScrollerHeight(totalHeight); + this.scroller.setHeight(scrollerHeight); + } - if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars? + // guarantees the same scrollbar widths + this.scroller.lockOverflow(scrollbarWidths); + } + }, - compensateScroll(this.headRowEl, scrollbarWidths); - // doing the scrollbar compensation might have created text overflow which created more height. redo - scrollerHeight = this.computeScrollerHeight(totalHeight); - this.scroller.setHeight(scrollerHeight); - } + // given a desired total height of the view, returns what the height of the scroller should be + computeScrollerHeight: function(totalHeight) { + return totalHeight - + subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller + }, - // guarantees the same scrollbar widths - this.scroller.lockOverflow(scrollbarWidths); - } - }, + // Sets the height of just the DayGrid component in this view + setGridHeight: function(height, isAuto) { + if (isAuto) { + undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding + } + else { + distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows + } + }, - // given a desired total height of the view, returns what the height of the scroller should be - computeScrollerHeight: function(totalHeight) { - return totalHeight - - subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller - }, + /* Scroll + ------------------------------------------------------------------------------------------------------------------*/ - // Sets the height of just the DayGrid component in this view - setGridHeight: function(height, isAuto) { - if (isAuto) { - undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding - } - else { - distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows - } - }, + queryScroll: function() { + return this.scroller.getScrollTop(); + }, - /* Scroll - ------------------------------------------------------------------------------------------------------------------*/ + setScroll: function(top) { + this.scroller.setScrollTop(top); + }, - queryScroll: function() { - return this.scroller.getScrollTop(); - }, + /* Hit Areas + ------------------------------------------------------------------------------------------------------------------*/ + // forward all hit-related method calls to dayGrid - setScroll: function(top) { - this.scroller.setScrollTop(top); - }, + prepareHits: function() { + this.dayGrid.prepareHits(); + }, - /* Hit Areas - ------------------------------------------------------------------------------------------------------------------*/ - // forward all hit-related method calls to dayGrid + releaseHits: function() { + this.dayGrid.releaseHits(); + }, - prepareHits: function() { - this.dayGrid.prepareHits(); - }, + queryHit: function(left, top) { + return this.dayGrid.queryHit(left, top); + }, - releaseHits: function() { - this.dayGrid.releaseHits(); - }, + getHitSpan: function(hit) { + return this.dayGrid.getHitSpan(hit); + }, - queryHit: function(left, top) { - return this.dayGrid.queryHit(left, top); - }, + getHitEl: function(hit) { + return this.dayGrid.getHitEl(hit); + }, - getHitSpan: function(hit) { - return this.dayGrid.getHitSpan(hit); - }, + /* Events + ------------------------------------------------------------------------------------------------------------------*/ - getHitEl: function(hit) { - return this.dayGrid.getHitEl(hit); - }, + // Renders the given events onto the view and populates the segments array + renderEvents: function(events) { + this.dayGrid.renderEvents(events); - /* Events - ------------------------------------------------------------------------------------------------------------------*/ + this.updateHeight(); // must compensate for events that overflow the row + }, - // Renders the given events onto the view and populates the segments array - renderEvents: function(events) { - this.dayGrid.renderEvents(events); + // Retrieves all segment objects that are rendered in the view + getEventSegs: function() { + return this.dayGrid.getEventSegs(); + }, - this.updateHeight(); // must compensate for events that overflow the row - }, + // Unrenders all event elements and clears internal segment data + unrenderEvents: function() { + this.dayGrid.unrenderEvents(); - // Retrieves all segment objects that are rendered in the view - getEventSegs: function() { - return this.dayGrid.getEventSegs(); - }, + // we DON'T need to call updateHeight() because: + // A) a renderEvents() call always happens after this, which will eventually call updateHeight() + // B) in IE8, this causes a flash whenever events are rerendered + }, - // Unrenders all event elements and clears internal segment data - unrenderEvents: function() { - this.dayGrid.unrenderEvents(); + /* Dragging (for both events and external elements) + ------------------------------------------------------------------------------------------------------------------*/ - // we DON'T need to call updateHeight() because: - // A) a renderEvents() call always happens after this, which will eventually call updateHeight() - // B) in IE8, this causes a flash whenever events are rerendered - }, + // A returned value of `true` signals that a mock "helper" event has been rendered. + renderDrag: function(dropLocation, seg) { + return this.dayGrid.renderDrag(dropLocation, seg); + }, - /* Dragging (for both events and external elements) - ------------------------------------------------------------------------------------------------------------------*/ + unrenderDrag: function() { + this.dayGrid.unrenderDrag(); + }, - // A returned value of `true` signals that a mock "helper" event has been rendered. - renderDrag: function(dropLocation, seg) { - return this.dayGrid.renderDrag(dropLocation, seg); - }, + /* Selection + ------------------------------------------------------------------------------------------------------------------*/ - unrenderDrag: function() { - this.dayGrid.unrenderDrag(); - }, + // Renders a visual indication of a selection + renderSelection: function(span) { + this.dayGrid.renderSelection(span); + }, - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ + // Unrenders a visual indications of a selection + unrenderSelection: function() { + this.dayGrid.unrenderSelection(); + } - // Renders a visual indication of a selection - renderSelection: function(span) { - this.dayGrid.renderSelection(span); - }, + }); - // Unrenders a visual indications of a selection - unrenderSelection: function() { - this.dayGrid.unrenderSelection(); - } + // Methods that will customize the rendering behavior of the BasicView's dayGrid + var basicDayGridMethods = { -}); + // Generates the HTML that will go before the day-of week header cells + renderHeadIntroHtml: function() { + var view = this.view; -// Methods that will customize the rendering behavior of the BasicView's dayGrid -var basicDayGridMethods = { + if (view.weekNumbersVisible) { + return '' + + '' + + '' + // needed for matchCellWidths + htmlEscape(view.opt('weekNumberTitle')) + + '' + + ''; + } + return ''; + }, - // Generates the HTML that will go before the day-of week header cells - renderHeadIntroHtml: function() { - var view = this.view; - if (view.weekNumbersVisible) { - return '' + - '' + - '' + // needed for matchCellWidths - htmlEscape(view.opt('weekNumberTitle')) + - '' + - ''; - } + // Generates the HTML that will go before content-skeleton cells that display the day/week numbers + renderNumberIntroHtml: function(row) { + var view = this.view; - return ''; - }, + if (view.weekNumbersVisible) { + return '' + + '' + + '' + // needed for matchCellWidths + this.getCellDate(row, 0).format('w') + + '' + + ''; + } + return ''; + }, - // Generates the HTML that will go before content-skeleton cells that display the day/week numbers - renderNumberIntroHtml: function(row) { - var view = this.view; - if (view.weekNumbersVisible) { - return '' + - '' + - '' + // needed for matchCellWidths - this.getCellDate(row, 0).format('w') + - '' + - ''; - } + // Generates the HTML that goes before the day bg cells for each day-row + renderBgIntroHtml: function() { + var view = this.view; - return ''; - }, + if (view.weekNumbersVisible) { + return ''; + } + return ''; + }, - // Generates the HTML that goes before the day bg cells for each day-row - renderBgIntroHtml: function() { - var view = this.view; - if (view.weekNumbersVisible) { - return ''; - } + // Generates the HTML that goes before every other type of row generated by DayGrid. + // Affects helper-skeleton and highlight-skeleton rows. + renderIntroHtml: function() { + var view = this.view; - return ''; - }, + if (view.weekNumbersVisible) { + return ''; + } + return ''; + } - // Generates the HTML that goes before every other type of row generated by DayGrid. - // Affects helper-skeleton and highlight-skeleton rows. - renderIntroHtml: function() { - var view = this.view; + }; - if (view.weekNumbersVisible) { - return ''; - } + ;; - return ''; - } + /* A month view with day cells running in rows (one-per-week) and columns + ----------------------------------------------------------------------------------------------------------------------*/ -}; + var MonthView = FC.MonthView = BasicView.extend({ -;; + // Produces information about what range to display + computeRange: function(date) { + var range = BasicView.prototype.computeRange.call(this, date); // get value from super-method + var rowCnt; -/* A month view with day cells running in rows (one-per-week) and columns -----------------------------------------------------------------------------------------------------------------------*/ + // ensure 6 weeks + if (this.isFixedWeeks()) { + rowCnt = Math.ceil(range.end.diff(range.start, 'weeks', true)); // could be partial weeks due to hiddenDays + range.end.add(6 - rowCnt, 'weeks'); + } -var MonthView = FC.MonthView = BasicView.extend({ + return range; + }, - // Produces information about what range to display - computeRange: function(date) { - var range = BasicView.prototype.computeRange.call(this, date); // get value from super-method - var rowCnt; - // ensure 6 weeks - if (this.isFixedWeeks()) { - rowCnt = Math.ceil(range.end.diff(range.start, 'weeks', true)); // could be partial weeks due to hiddenDays - range.end.add(6 - rowCnt, 'weeks'); - } + // Overrides the default BasicView behavior to have special multi-week auto-height logic + setGridHeight: function(height, isAuto) { - return range; - }, + isAuto = isAuto || this.opt('weekMode') === 'variable'; // LEGACY: weekMode is deprecated + // if auto, make the height of each row the height that it would be if there were 6 weeks + if (isAuto) { + height *= this.rowCnt / 6; + } - // Overrides the default BasicView behavior to have special multi-week auto-height logic - setGridHeight: function(height, isAuto) { + distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows + }, - isAuto = isAuto || this.opt('weekMode') === 'variable'; // LEGACY: weekMode is deprecated - // if auto, make the height of each row the height that it would be if there were 6 weeks - if (isAuto) { - height *= this.rowCnt / 6; - } + isFixedWeeks: function() { + var weekMode = this.opt('weekMode'); // LEGACY: weekMode is deprecated + if (weekMode) { + return weekMode === 'fixed'; // if any other type of weekMode, assume NOT fixed + } - distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows - }, + return this.opt('fixedWeekCount'); + } + }); - isFixedWeeks: function() { - var weekMode = this.opt('weekMode'); // LEGACY: weekMode is deprecated - if (weekMode) { - return weekMode === 'fixed'; // if any other type of weekMode, assume NOT fixed - } + ;; - return this.opt('fixedWeekCount'); - } + fcViews.basic = { + 'class': BasicView + }; -}); + fcViews.basicDay = { + type: 'basic', + duration: { days: 1 } + }; -;; + fcViews.basicWeek = { + type: 'basic', + duration: { weeks: 1 } + }; -fcViews.basic = { - 'class': BasicView -}; + fcViews.month = { + 'class': MonthView, + duration: { months: 1 }, // important for prev/next + defaults: { + fixedWeekCount: true + } + }; + ;; -fcViews.basicDay = { - type: 'basic', - duration: { days: 1 } -}; + /* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically. + ----------------------------------------------------------------------------------------------------------------------*/ + // Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on). + // Responsible for managing width/height. -fcViews.basicWeek = { - type: 'basic', - duration: { weeks: 1 } -}; + var AgendaView = FC.AgendaView = View.extend({ -fcViews.month = { - 'class': MonthView, - duration: { months: 1 }, // important for prev/next - defaults: { - fixedWeekCount: true - } -}; -;; + scroller: null, -/* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically. -----------------------------------------------------------------------------------------------------------------------*/ -// Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on). -// Responsible for managing width/height. + timeGridClass: TimeGrid, // class used to instantiate the timeGrid. subclasses can override + timeGrid: null, // the main time-grid subcomponent of this view -var AgendaView = FC.AgendaView = View.extend({ + dayGridClass: DayGrid, // class used to instantiate the dayGrid. subclasses can override + dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null - scroller: null, + axisWidth: null, // the width of the time axis running down the side - timeGridClass: TimeGrid, // class used to instantiate the timeGrid. subclasses can override - timeGrid: null, // the main time-grid subcomponent of this view + headContainerEl: null, // div that hold's the timeGrid's rendered date header + noScrollRowEls: null, // set of fake row elements that must compensate when scroller has scrollbars - dayGridClass: DayGrid, // class used to instantiate the dayGrid. subclasses can override - dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null + // when the time-grid isn't tall enough to occupy the given height, we render an
underneath + bottomRuleEl: null, - axisWidth: null, // the width of the time axis running down the side - headContainerEl: null, // div that hold's the timeGrid's rendered date header - noScrollRowEls: null, // set of fake row elements that must compensate when scroller has scrollbars + initialize: function() { + this.timeGrid = this.instantiateTimeGrid(); - // when the time-grid isn't tall enough to occupy the given height, we render an
underneath - bottomRuleEl: null, + if (this.opt('allDaySlot')) { // should we display the "all-day" area? + this.dayGrid = this.instantiateDayGrid(); // the all-day subcomponent of this view + } + this.scroller = new Scroller({ + overflowX: 'hidden', + overflowY: 'auto' + }); + }, - initialize: function() { - this.timeGrid = this.instantiateTimeGrid(); - if (this.opt('allDaySlot')) { // should we display the "all-day" area? - this.dayGrid = this.instantiateDayGrid(); // the all-day subcomponent of this view - } + // Instantiates the TimeGrid object this view needs. Draws from this.timeGridClass + instantiateTimeGrid: function() { + var subclass = this.timeGridClass.extend(agendaTimeGridMethods); - this.scroller = new Scroller({ - overflowX: 'hidden', - overflowY: 'auto' - }); - }, + return new subclass(this); + }, - // Instantiates the TimeGrid object this view needs. Draws from this.timeGridClass - instantiateTimeGrid: function() { - var subclass = this.timeGridClass.extend(agendaTimeGridMethods); + // Instantiates the DayGrid object this view might need. Draws from this.dayGridClass + instantiateDayGrid: function() { + var subclass = this.dayGridClass.extend(agendaDayGridMethods); - return new subclass(this); - }, + return new subclass(this); + }, - // Instantiates the DayGrid object this view might need. Draws from this.dayGridClass - instantiateDayGrid: function() { - var subclass = this.dayGridClass.extend(agendaDayGridMethods); + /* Rendering + ------------------------------------------------------------------------------------------------------------------*/ - return new subclass(this); - }, + // Sets the display range and computes all necessary dates + setRange: function(range) { + View.prototype.setRange.call(this, range); // call the super-method - /* Rendering - ------------------------------------------------------------------------------------------------------------------*/ + this.timeGrid.setRange(range); + if (this.dayGrid) { + this.dayGrid.setRange(range); + } + }, - // Sets the display range and computes all necessary dates - setRange: function(range) { - View.prototype.setRange.call(this, range); // call the super-method + // Renders the view into `this.el`, which has already been assigned + renderDates: function() { - this.timeGrid.setRange(range); - if (this.dayGrid) { - this.dayGrid.setRange(range); - } - }, + this.el.addClass('fc-agenda-view').html(this.renderSkeletonHtml()); + this.renderHead(); + this.scroller.render(); + var timeGridWrapEl = this.scroller.el.addClass('fc-time-grid-container'); + var timeGridEl = $('
').appendTo(timeGridWrapEl); + this.el.find('.fc-body > tr > td').append(timeGridWrapEl); - // Renders the view into `this.el`, which has already been assigned - renderDates: function() { + this.timeGrid.setElement(timeGridEl); + this.timeGrid.renderDates(); - this.el.addClass('fc-agenda-view').html(this.renderSkeletonHtml()); - this.renderHead(); + // the
that sometimes displays under the time-grid + this.bottomRuleEl = $('
') + .appendTo(this.timeGrid.el); // inject it into the time-grid - this.scroller.render(); - var timeGridWrapEl = this.scroller.el.addClass('fc-time-grid-container'); - var timeGridEl = $('
').appendTo(timeGridWrapEl); - this.el.find('.fc-body > tr > td').append(timeGridWrapEl); + if (this.dayGrid) { + this.dayGrid.setElement(this.el.find('.fc-day-grid')); + this.dayGrid.renderDates(); - this.timeGrid.setElement(timeGridEl); - this.timeGrid.renderDates(); + // have the day-grid extend it's coordinate area over the
dividing the two grids + this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight(); + } - // the
that sometimes displays under the time-grid - this.bottomRuleEl = $('
') - .appendTo(this.timeGrid.el); // inject it into the time-grid + this.noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller + }, - if (this.dayGrid) { - this.dayGrid.setElement(this.el.find('.fc-day-grid')); - this.dayGrid.renderDates(); - // have the day-grid extend it's coordinate area over the
dividing the two grids - this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight(); - } + // render the day-of-week headers + renderHead: function() { + this.headContainerEl = + this.el.find('.fc-head-container') + .html(this.timeGrid.renderHeadHtml()); + }, - this.noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller - }, + // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering, + // always completely kill each grid's rendering. + unrenderDates: function() { + this.timeGrid.unrenderDates(); + this.timeGrid.removeElement(); - // render the day-of-week headers - renderHead: function() { - this.headContainerEl = - this.el.find('.fc-head-container') - .html(this.timeGrid.renderHeadHtml()); - }, + if (this.dayGrid) { + this.dayGrid.unrenderDates(); + this.dayGrid.removeElement(); + } + this.scroller.destroy(); + }, - // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering, - // always completely kill each grid's rendering. - unrenderDates: function() { - this.timeGrid.unrenderDates(); - this.timeGrid.removeElement(); - if (this.dayGrid) { - this.dayGrid.unrenderDates(); - this.dayGrid.removeElement(); - } + // Builds the HTML skeleton for the view. + // The day-grid and time-grid components will render inside containers defined by this HTML. + renderSkeletonHtml: function() { + return '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + + (this.dayGrid ? + '
' + + '
' : + '' + ) + + '
'; + }, - this.scroller.destroy(); - }, + // Generates an HTML attribute string for setting the width of the axis, if it is known + axisStyleAttr: function() { + if (this.axisWidth !== null) { + return 'style="width:' + this.axisWidth + 'px"'; + } + return ''; + }, - // Builds the HTML skeleton for the view. - // The day-grid and time-grid components will render inside containers defined by this HTML. - renderSkeletonHtml: function() { - return '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '
' + - (this.dayGrid ? - '
' + - '
' : - '' - ) + - '
'; - }, + /* Business Hours + ------------------------------------------------------------------------------------------------------------------*/ - // Generates an HTML attribute string for setting the width of the axis, if it is known - axisStyleAttr: function() { - if (this.axisWidth !== null) { - return 'style="width:' + this.axisWidth + 'px"'; - } - return ''; - }, + renderBusinessHours: function() { + this.timeGrid.renderBusinessHours(); - /* Business Hours - ------------------------------------------------------------------------------------------------------------------*/ + if (this.dayGrid) { + this.dayGrid.renderBusinessHours(); + } + }, - renderBusinessHours: function() { - this.timeGrid.renderBusinessHours(); + unrenderBusinessHours: function() { + this.timeGrid.unrenderBusinessHours(); - if (this.dayGrid) { - this.dayGrid.renderBusinessHours(); - } - }, + if (this.dayGrid) { + this.dayGrid.unrenderBusinessHours(); + } + }, - unrenderBusinessHours: function() { - this.timeGrid.unrenderBusinessHours(); + /* Now Indicator + ------------------------------------------------------------------------------------------------------------------*/ - if (this.dayGrid) { - this.dayGrid.unrenderBusinessHours(); - } - }, + getNowIndicatorUnit: function() { + return this.timeGrid.getNowIndicatorUnit(); + }, - /* Now Indicator - ------------------------------------------------------------------------------------------------------------------*/ + renderNowIndicator: function(date) { + this.timeGrid.renderNowIndicator(date); + }, - getNowIndicatorUnit: function() { - return this.timeGrid.getNowIndicatorUnit(); - }, + unrenderNowIndicator: function() { + this.timeGrid.unrenderNowIndicator(); + }, - renderNowIndicator: function(date) { - this.timeGrid.renderNowIndicator(date); - }, + /* Dimensions + ------------------------------------------------------------------------------------------------------------------*/ - unrenderNowIndicator: function() { - this.timeGrid.unrenderNowIndicator(); - }, + updateSize: function(isResize) { + this.timeGrid.updateSize(isResize); - /* Dimensions - ------------------------------------------------------------------------------------------------------------------*/ + View.prototype.updateSize.call(this, isResize); // call the super-method + }, - updateSize: function(isResize) { - this.timeGrid.updateSize(isResize); + // Refreshes the horizontal dimensions of the view + updateWidth: function() { + // make all axis cells line up, and record the width so newly created axis cells will have it + this.axisWidth = matchCellWidths(this.el.find('.fc-axis')); + }, - View.prototype.updateSize.call(this, isResize); // call the super-method - }, + // Adjusts the vertical dimensions of the view to the specified values + setHeight: function(totalHeight, isAuto) { + var eventLimit; + var scrollerHeight; + var scrollbarWidths; - // Refreshes the horizontal dimensions of the view - updateWidth: function() { - // make all axis cells line up, and record the width so newly created axis cells will have it - this.axisWidth = matchCellWidths(this.el.find('.fc-axis')); - }, + // reset all dimensions back to the original state + this.bottomRuleEl.hide(); // .show() will be called later if this
is necessary + this.scroller.clear(); // sets height to 'auto' and clears overflow + uncompensateScroll(this.noScrollRowEls); + // limit number of events in the all-day area + if (this.dayGrid) { + this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed - // Adjusts the vertical dimensions of the view to the specified values - setHeight: function(totalHeight, isAuto) { - var eventLimit; - var scrollerHeight; - var scrollbarWidths; + eventLimit = this.opt('eventLimit'); + if (eventLimit && typeof eventLimit !== 'number') { + eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number + } + if (eventLimit) { + this.dayGrid.limitRows(eventLimit); + } + } - // reset all dimensions back to the original state - this.bottomRuleEl.hide(); // .show() will be called later if this
is necessary - this.scroller.clear(); // sets height to 'auto' and clears overflow - uncompensateScroll(this.noScrollRowEls); + if (!isAuto) { // should we force dimensions of the scroll container? - // limit number of events in the all-day area - if (this.dayGrid) { - this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed + scrollerHeight = this.computeScrollerHeight(totalHeight); + this.scroller.setHeight(scrollerHeight); + scrollbarWidths = this.scroller.getScrollbarWidths(); - eventLimit = this.opt('eventLimit'); - if (eventLimit && typeof eventLimit !== 'number') { - eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number - } - if (eventLimit) { - this.dayGrid.limitRows(eventLimit); - } - } + if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars? - if (!isAuto) { // should we force dimensions of the scroll container? + // make the all-day and header rows lines up + compensateScroll(this.noScrollRowEls, scrollbarWidths); - scrollerHeight = this.computeScrollerHeight(totalHeight); - this.scroller.setHeight(scrollerHeight); - scrollbarWidths = this.scroller.getScrollbarWidths(); + // the scrollbar compensation might have changed text flow, which might affect height, so recalculate + // and reapply the desired height to the scroller. + scrollerHeight = this.computeScrollerHeight(totalHeight); + this.scroller.setHeight(scrollerHeight); + } - if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars? + // guarantees the same scrollbar widths + this.scroller.lockOverflow(scrollbarWidths); - // make the all-day and header rows lines up - compensateScroll(this.noScrollRowEls, scrollbarWidths); + // if there's any space below the slats, show the horizontal rule. + // this won't cause any new overflow, because lockOverflow already called. + if (this.timeGrid.getTotalSlatHeight() < scrollerHeight) { + this.bottomRuleEl.show(); + } + } + }, - // the scrollbar compensation might have changed text flow, which might affect height, so recalculate - // and reapply the desired height to the scroller. - scrollerHeight = this.computeScrollerHeight(totalHeight); - this.scroller.setHeight(scrollerHeight); - } - // guarantees the same scrollbar widths - this.scroller.lockOverflow(scrollbarWidths); + // given a desired total height of the view, returns what the height of the scroller should be + computeScrollerHeight: function(totalHeight) { + return totalHeight - + subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller + }, - // if there's any space below the slats, show the horizontal rule. - // this won't cause any new overflow, because lockOverflow already called. - if (this.timeGrid.getTotalSlatHeight() < scrollerHeight) { - this.bottomRuleEl.show(); - } - } - }, + /* Scroll + ------------------------------------------------------------------------------------------------------------------*/ - // given a desired total height of the view, returns what the height of the scroller should be - computeScrollerHeight: function(totalHeight) { - return totalHeight - - subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller - }, + // Computes the initial pre-configured scroll state prior to allowing the user to change it + computeInitialScroll: function() { + var scrollTime = moment.duration(this.opt('scrollTime')); + var top = this.timeGrid.computeTimeTop(scrollTime); - /* Scroll - ------------------------------------------------------------------------------------------------------------------*/ + // zoom can give weird floating-point values. rather scroll a little bit further + top = Math.ceil(top); + if (top) { + top++; // to overcome top border that slots beyond the first have. looks better + } - // Computes the initial pre-configured scroll state prior to allowing the user to change it - computeInitialScroll: function() { - var scrollTime = moment.duration(this.opt('scrollTime')); - var top = this.timeGrid.computeTimeTop(scrollTime); + return top; + }, - // zoom can give weird floating-point values. rather scroll a little bit further - top = Math.ceil(top); - if (top) { - top++; // to overcome top border that slots beyond the first have. looks better - } + queryScroll: function() { + return this.scroller.getScrollTop(); + }, - return top; - }, + setScroll: function(top) { + this.scroller.setScrollTop(top); + }, - queryScroll: function() { - return this.scroller.getScrollTop(); - }, + /* Hit Areas + ------------------------------------------------------------------------------------------------------------------*/ + // forward all hit-related method calls to the grids (dayGrid might not be defined) - setScroll: function(top) { - this.scroller.setScrollTop(top); - }, + prepareHits: function() { + this.timeGrid.prepareHits(); + if (this.dayGrid) { + this.dayGrid.prepareHits(); + } + }, - /* Hit Areas - ------------------------------------------------------------------------------------------------------------------*/ - // forward all hit-related method calls to the grids (dayGrid might not be defined) + releaseHits: function() { + this.timeGrid.releaseHits(); + if (this.dayGrid) { + this.dayGrid.releaseHits(); + } + }, - prepareHits: function() { - this.timeGrid.prepareHits(); - if (this.dayGrid) { - this.dayGrid.prepareHits(); - } - }, + queryHit: function(left, top) { + var hit = this.timeGrid.queryHit(left, top); - releaseHits: function() { - this.timeGrid.releaseHits(); - if (this.dayGrid) { - this.dayGrid.releaseHits(); - } - }, + if (!hit && this.dayGrid) { + hit = this.dayGrid.queryHit(left, top); + } + return hit; + }, - queryHit: function(left, top) { - var hit = this.timeGrid.queryHit(left, top); - if (!hit && this.dayGrid) { - hit = this.dayGrid.queryHit(left, top); - } + getHitSpan: function(hit) { + // TODO: hit.component is set as a hack to identify where the hit came from + return hit.component.getHitSpan(hit); + }, - return hit; - }, + getHitEl: function(hit) { + // TODO: hit.component is set as a hack to identify where the hit came from + return hit.component.getHitEl(hit); + }, - getHitSpan: function(hit) { - // TODO: hit.component is set as a hack to identify where the hit came from - return hit.component.getHitSpan(hit); - }, + /* Events + ------------------------------------------------------------------------------------------------------------------*/ - getHitEl: function(hit) { - // TODO: hit.component is set as a hack to identify where the hit came from - return hit.component.getHitEl(hit); - }, + // Renders events onto the view and populates the View's segment array + renderEvents: function(events) { + var dayEvents = []; + var timedEvents = []; + var daySegs = []; + var timedSegs; + var i; - /* Events - ------------------------------------------------------------------------------------------------------------------*/ + // separate the events into all-day and timed + for (i = 0; i < events.length; i++) { + if (events[i].allDay) { + dayEvents.push(events[i]); + } + else { + timedEvents.push(events[i]); + } + } + // render the events in the subcomponents + timedSegs = this.timeGrid.renderEvents(timedEvents); + if (this.dayGrid) { + daySegs = this.dayGrid.renderEvents(dayEvents); + } - // Renders events onto the view and populates the View's segment array - renderEvents: function(events) { - var dayEvents = []; - var timedEvents = []; - var daySegs = []; - var timedSegs; - var i; + // the all-day area is flexible and might have a lot of events, so shift the height + this.updateHeight(); + }, - // separate the events into all-day and timed - for (i = 0; i < events.length; i++) { - if (events[i].allDay) { - dayEvents.push(events[i]); - } - else { - timedEvents.push(events[i]); - } - } - // render the events in the subcomponents - timedSegs = this.timeGrid.renderEvents(timedEvents); - if (this.dayGrid) { - daySegs = this.dayGrid.renderEvents(dayEvents); - } + // Retrieves all segment objects that are rendered in the view + getEventSegs: function() { + return this.timeGrid.getEventSegs().concat( + this.dayGrid ? this.dayGrid.getEventSegs() : [] + ); + }, - // the all-day area is flexible and might have a lot of events, so shift the height - this.updateHeight(); - }, + // Unrenders all event elements and clears internal segment data + unrenderEvents: function() { - // Retrieves all segment objects that are rendered in the view - getEventSegs: function() { - return this.timeGrid.getEventSegs().concat( - this.dayGrid ? this.dayGrid.getEventSegs() : [] - ); - }, + // unrender the events in the subcomponents + this.timeGrid.unrenderEvents(); + if (this.dayGrid) { + this.dayGrid.unrenderEvents(); + } + // we DON'T need to call updateHeight() because: + // A) a renderEvents() call always happens after this, which will eventually call updateHeight() + // B) in IE8, this causes a flash whenever events are rerendered + }, - // Unrenders all event elements and clears internal segment data - unrenderEvents: function() { - // unrender the events in the subcomponents - this.timeGrid.unrenderEvents(); - if (this.dayGrid) { - this.dayGrid.unrenderEvents(); - } + /* Dragging (for events and external elements) + ------------------------------------------------------------------------------------------------------------------*/ - // we DON'T need to call updateHeight() because: - // A) a renderEvents() call always happens after this, which will eventually call updateHeight() - // B) in IE8, this causes a flash whenever events are rerendered - }, + // A returned value of `true` signals that a mock "helper" event has been rendered. + renderDrag: function(dropLocation, seg) { + if (dropLocation.start.hasTime()) { + return this.timeGrid.renderDrag(dropLocation, seg); + } + else if (this.dayGrid) { + return this.dayGrid.renderDrag(dropLocation, seg); + } + }, - /* Dragging (for events and external elements) - ------------------------------------------------------------------------------------------------------------------*/ + unrenderDrag: function() { + this.timeGrid.unrenderDrag(); + if (this.dayGrid) { + this.dayGrid.unrenderDrag(); + } + }, - // A returned value of `true` signals that a mock "helper" event has been rendered. - renderDrag: function(dropLocation, seg) { - if (dropLocation.start.hasTime()) { - return this.timeGrid.renderDrag(dropLocation, seg); - } - else if (this.dayGrid) { - return this.dayGrid.renderDrag(dropLocation, seg); - } - }, + /* Selection + ------------------------------------------------------------------------------------------------------------------*/ - unrenderDrag: function() { - this.timeGrid.unrenderDrag(); - if (this.dayGrid) { - this.dayGrid.unrenderDrag(); - } - }, + // Renders a visual indication of a selection + renderSelection: function(span) { + if (span.start.hasTime() || span.end.hasTime()) { + this.timeGrid.renderSelection(span); + } + else if (this.dayGrid) { + this.dayGrid.renderSelection(span); + } + }, - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ + // Unrenders a visual indications of a selection + unrenderSelection: function() { + this.timeGrid.unrenderSelection(); + if (this.dayGrid) { + this.dayGrid.unrenderSelection(); + } + } - // Renders a visual indication of a selection - renderSelection: function(span) { - if (span.start.hasTime() || span.end.hasTime()) { - this.timeGrid.renderSelection(span); - } - else if (this.dayGrid) { - this.dayGrid.renderSelection(span); - } - }, + }); - // Unrenders a visual indications of a selection - unrenderSelection: function() { - this.timeGrid.unrenderSelection(); - if (this.dayGrid) { - this.dayGrid.unrenderSelection(); - } - } + // Methods that will customize the rendering behavior of the AgendaView's timeGrid + // TODO: move into TimeGrid + var agendaTimeGridMethods = { + -}); + // Generates the HTML that will go before the day-of week header cells + renderHeadIntroHtml: function() { + var view = this.view; + var weekText; + if (view.opt('weekNumbers')) { + weekText = this.start.format(view.opt('smallWeekFormat')); -// Methods that will customize the rendering behavior of the AgendaView's timeGrid -// TODO: move into TimeGrid -var agendaTimeGridMethods = { + return '' + + '' + + '' + // needed for matchCellWidths + htmlEscape(weekText) + + '' + + ''; + } + else { + return ''; + } + }, - // Generates the HTML that will go before the day-of week header cells - renderHeadIntroHtml: function() { - var view = this.view; - var weekText; + // Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column. + renderBgIntroHtml: function() { + var view = this.view; - if (view.opt('weekNumbers')) { - weekText = this.start.format(view.opt('smallWeekFormat')); + return ''; + }, - return '' + - '' + - '' + // needed for matchCellWidths - htmlEscape(weekText) + - '' + - ''; - } - else { - return ''; - } - }, + // Generates the HTML that goes before all other types of cells. + // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid. + renderIntroHtml: function() { + var view = this.view; - // Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column. - renderBgIntroHtml: function() { - var view = this.view; + return ''; + } - return ''; - }, + }; - // Generates the HTML that goes before all other types of cells. - // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid. - renderIntroHtml: function() { - var view = this.view; + // Methods that will customize the rendering behavior of the AgendaView's dayGrid + var agendaDayGridMethods = { - return ''; - } -}; + // Generates the HTML that goes before the all-day cells + renderBgIntroHtml: function() { + var view = this.view; + return '' + + '' + + '' + // needed for matchCellWidths + (view.opt('allDayHtml') || htmlEscape(view.opt('allDayText'))) + + '' + + ''; + }, -// Methods that will customize the rendering behavior of the AgendaView's dayGrid -var agendaDayGridMethods = { + // Generates the HTML that goes before all other types of cells. + // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid. + renderIntroHtml: function() { + var view = this.view; - // Generates the HTML that goes before the all-day cells - renderBgIntroHtml: function() { - var view = this.view; + return ''; + } - return '' + - '' + - '' + // needed for matchCellWidths - (view.opt('allDayHtml') || htmlEscape(view.opt('allDayText'))) + - '' + - ''; - }, + }; + ;; - // Generates the HTML that goes before all other types of cells. - // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid. - renderIntroHtml: function() { - var view = this.view; + var AGENDA_ALL_DAY_EVENT_LIMIT = 5; - return ''; - } + // potential nice values for the slot-duration and interval-duration + // from largest to smallest + var AGENDA_STOCK_SUB_DURATIONS = [ + { hours: 1 }, + { minutes: 30 }, + { minutes: 15 }, + { seconds: 30 }, + { seconds: 15 } + ]; -}; - -;; - -var AGENDA_ALL_DAY_EVENT_LIMIT = 5; - -// potential nice values for the slot-duration and interval-duration -// from largest to smallest -var AGENDA_STOCK_SUB_DURATIONS = [ - { hours: 1 }, - { minutes: 30 }, - { minutes: 15 }, - { seconds: 30 }, - { seconds: 15 } -]; - -fcViews.agenda = { - 'class': AgendaView, - defaults: { - allDaySlot: true, - allDayText: 'all-day', - slotDuration: '00:30:00', - minTime: '00:00:00', - maxTime: '24:00:00', - slotEventOverlap: true // a bad name. confused with overlap/constraint system - } -}; + fcViews.agenda = { + 'class': AgendaView, + defaults: { + allDaySlot: true, + allDayText: 'all-day', + slotDuration: '00:30:00', + minTime: '00:00:00', + maxTime: '24:00:00', + slotEventOverlap: true // a bad name. confused with overlap/constraint system + } + }; -fcViews.agendaDay = { - type: 'agenda', - duration: { days: 1 } -}; + fcViews.agendaDay = { + type: 'agenda', + duration: { days: 1 } + }; -fcViews.agendaWeek = { - type: 'agenda', - duration: { weeks: 1 } -}; -;; + fcViews.agendaWeek = { + type: 'agenda', + duration: { weeks: 1 } + }; + ;; -return FC; // export for Node/CommonJS + return FC; // export for Node/CommonJS }); diff --git a/src/calendar/lib/lang-all.js b/src/calendar/lib/lang-all.js index 23838f1..4262a21 100644 --- a/src/calendar/lib/lang-all.js +++ b/src/calendar/lib/lang-all.js @@ -10,6 +10,7 @@ weekdays: "الأحد_الإتنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"), weekdaysShort: "احد_اتنين_ثلاثاء_اربعاء_خميس_جمعة_سبت".split("_"), weekdaysMin: "ح_ن_ث_ر_خ_ج_س".split("_"), + weekdaysParseExact: !0, longDateFormat: { LT: "HH:mm", LTS: "HH:mm:ss", @@ -52,14 +53,14 @@ prevText: "<السابق", nextText: "التالي>", currentText: "اليوم", - monthNames: ["كانون الثاني", "شباط", "آذار", "نيسان", "مايو", "حزيران", "تموز", "آب", "أيلول", "تشرين الأول", "تشرين الثاني", "كانون الأول"], + monthNames: ["يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو", "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"], monthNamesShort: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"], dayNames: ["الأحد", "الاثنين", "الثلاثاء", "الأربعاء", "الخميس", "الجمعة", "السبت"], - dayNamesShort: ["الأحد", "الاثنين", "الثلاثاء", "الأربعاء", "الخميس", "الجمعة", "السبت"], + dayNamesShort: ["أحد", "اثنين", "ثلاثاء", "أربعاء", "خميس", "جمعة", "سبت"], dayNamesMin: ["ح", "ن", "ث", "ر", "خ", "ج", "س"], weekHeader: "أسبوع", dateFormat: "dd/mm/yy", - firstDay: 6, + firstDay: 0, isRTL: !0, showMonthAfterYear: !1, yearSuffix: "" @@ -78,102 +79,103 @@ ! function() { "use strict"; var a = { - 1: "١", - 2: "٢", - 3: "٣", - 4: "٤", - 5: "٥", - 6: "٦", - 7: "٧", - 8: "٨", - 9: "٩", - 0: "٠" - }, - c = { - "١": "1", - "٢": "2", - "٣": "3", - "٤": "4", - "٥": "5", - "٦": "6", - "٧": "7", - "٨": "8", - "٩": "9", - "٠": "0" - }, - d = (b.defineLocale || b.lang).call(b, "ar-sa", { - months: "يناير_فبراير_مارس_أبريل_مايو_يونيو_يوليو_أغسطس_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"), - monthsShort: "يناير_فبراير_مارس_أبريل_مايو_يونيو_يوليو_أغسطس_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"), - weekdays: "الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"), - weekdaysShort: "أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت".split("_"), - weekdaysMin: "ح_ن_ث_ر_خ_ج_س".split("_"), - longDateFormat: { - LT: "HH:mm", - LTS: "HH:mm:ss", - L: "DD/MM/YYYY", - LL: "D MMMM YYYY", - LLL: "D MMMM YYYY HH:mm", - LLLL: "dddd D MMMM YYYY HH:mm" - }, - meridiemParse: /ص|م/, - isPM: function(a) { - return "م" === a - }, - meridiem: function(a, b, c) { - return 12 > a ? "ص" : "م" - }, - calendar: { - sameDay: "[اليوم على الساعة] LT", - nextDay: "[غدا على الساعة] LT", - nextWeek: "dddd [على الساعة] LT", - lastDay: "[أمس على الساعة] LT", - lastWeek: "dddd [على الساعة] LT", - sameElse: "L" - }, - relativeTime: { - future: "في %s", - past: "منذ %s", - s: "ثوان", - m: "دقيقة", - mm: "%d دقائق", - h: "ساعة", - hh: "%d ساعات", - d: "يوم", - dd: "%d أيام", - M: "شهر", - MM: "%d أشهر", - y: "سنة", - yy: "%d سنوات" - }, - preparse: function(a) { - return a.replace(/[١٢٣٤٥٦٧٨٩٠]/g, function(a) { - return c[a] - }).replace(/،/g, ",") - }, - postformat: function(b) { - return b.replace(/\d/g, function(b) { - return a[b] - }).replace(/,/g, "،") - }, - week: { - dow: 6, - doy: 12 - } - }); + 1: "١", + 2: "٢", + 3: "٣", + 4: "٤", + 5: "٥", + 6: "٦", + 7: "٧", + 8: "٨", + 9: "٩", + 0: "٠" + }, + c = { + "١": "1", + "٢": "2", + "٣": "3", + "٤": "4", + "٥": "5", + "٦": "6", + "٧": "7", + "٨": "8", + "٩": "9", + "٠": "0" + }, + d = (b.defineLocale || b.lang).call(b, "ar-sa", { + months: "يناير_فبراير_مارس_أبريل_مايو_يونيو_يوليو_أغسطس_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"), + monthsShort: "يناير_فبراير_مارس_أبريل_مايو_يونيو_يوليو_أغسطس_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"), + weekdays: "الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"), + weekdaysShort: "أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت".split("_"), + weekdaysMin: "ح_ن_ث_ر_خ_ج_س".split("_"), + weekdaysParseExact: !0, + longDateFormat: { + LT: "HH:mm", + LTS: "HH:mm:ss", + L: "DD/MM/YYYY", + LL: "D MMMM YYYY", + LLL: "D MMMM YYYY HH:mm", + LLLL: "dddd D MMMM YYYY HH:mm" + }, + meridiemParse: /ص|م/, + isPM: function(a) { + return "م" === a + }, + meridiem: function(a, b, c) { + return 12 > a ? "ص" : "م" + }, + calendar: { + sameDay: "[اليوم على الساعة] LT", + nextDay: "[غدا على الساعة] LT", + nextWeek: "dddd [على الساعة] LT", + lastDay: "[أمس على الساعة] LT", + lastWeek: "dddd [على الساعة] LT", + sameElse: "L" + }, + relativeTime: { + future: "في %s", + past: "منذ %s", + s: "ثوان", + m: "دقيقة", + mm: "%d دقائق", + h: "ساعة", + hh: "%d ساعات", + d: "يوم", + dd: "%d أيام", + M: "شهر", + MM: "%d أشهر", + y: "سنة", + yy: "%d سنوات" + }, + preparse: function(a) { + return a.replace(/[١٢٣٤٥٦٧٨٩٠]/g, function(a) { + return c[a] + }).replace(/،/g, ",") + }, + postformat: function(b) { + return b.replace(/\d/g, function(b) { + return a[b] + }).replace(/,/g, "،") + }, + week: { + dow: 6, + doy: 12 + } + }); return d }(), a.fullCalendar.datepickerLang("ar-sa", "ar", { closeText: "إغلاق", prevText: "<السابق", nextText: "التالي>", currentText: "اليوم", - monthNames: ["كانون الثاني", "شباط", "آذار", "نيسان", "مايو", "حزيران", "تموز", "آب", "أيلول", "تشرين الأول", "تشرين الثاني", "كانون الأول"], + monthNames: ["يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو", "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"], monthNamesShort: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"], dayNames: ["الأحد", "الاثنين", "الثلاثاء", "الأربعاء", "الخميس", "الجمعة", "السبت"], - dayNamesShort: ["الأحد", "الاثنين", "الثلاثاء", "الأربعاء", "الخميس", "الجمعة", "السبت"], + dayNamesShort: ["أحد", "اثنين", "ثلاثاء", "أربعاء", "خميس", "جمعة", "سبت"], dayNamesMin: ["ح", "ن", "ث", "ر", "خ", "ج", "س"], weekHeader: "أسبوع", dateFormat: "dd/mm/yy", - firstDay: 6, + firstDay: 0, isRTL: !0, showMonthAfterYear: !1, yearSuffix: "" @@ -197,6 +199,7 @@ weekdays: "الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"), weekdaysShort: "أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت".split("_"), weekdaysMin: "ح_ن_ث_ر_خ_ج_س".split("_"), + weekdaysParseExact: !0, longDateFormat: { LT: "HH:mm", LTS: "HH:mm:ss", @@ -239,14 +242,14 @@ prevText: "<السابق", nextText: "التالي>", currentText: "اليوم", - monthNames: ["كانون الثاني", "شباط", "آذار", "نيسان", "مايو", "حزيران", "تموز", "آب", "أيلول", "تشرين الأول", "تشرين الثاني", "كانون الأول"], + monthNames: ["يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو", "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"], monthNamesShort: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"], dayNames: ["الأحد", "الاثنين", "الثلاثاء", "الأربعاء", "الخميس", "الجمعة", "السبت"], - dayNamesShort: ["الأحد", "الاثنين", "الثلاثاء", "الأربعاء", "الخميس", "الجمعة", "السبت"], + dayNamesShort: ["أحد", "اثنين", "ثلاثاء", "أربعاء", "خميس", "جمعة", "سبت"], dayNamesMin: ["ح", "ن", "ث", "ر", "خ", "ج", "س"], weekHeader: "أسبوع", dateFormat: "dd/mm/yy", - firstDay: 6, + firstDay: 0, isRTL: !0, showMonthAfterYear: !1, yearSuffix: "" @@ -265,121 +268,122 @@ ! function() { "use strict"; var a = { - 1: "١", - 2: "٢", - 3: "٣", - 4: "٤", - 5: "٥", - 6: "٦", - 7: "٧", - 8: "٨", - 9: "٩", - 0: "٠" - }, - c = { - "١": "1", - "٢": "2", - "٣": "3", - "٤": "4", - "٥": "5", - "٦": "6", - "٧": "7", - "٨": "8", - "٩": "9", - "٠": "0" - }, - d = function(a) { - return 0 === a ? 0 : 1 === a ? 1 : 2 === a ? 2 : a % 100 >= 3 && 10 >= a % 100 ? 3 : a % 100 >= 11 ? 4 : 5 - }, - e = { - s: ["أقل من ثانية", "ثانية واحدة", ["ثانيتان", "ثانيتين"], "%d ثوان", "%d ثانية", "%d ثانية"], - m: ["أقل من دقيقة", "دقيقة واحدة", ["دقيقتان", "دقيقتين"], "%d دقائق", "%d دقيقة", "%d دقيقة"], - h: ["أقل من ساعة", "ساعة واحدة", ["ساعتان", "ساعتين"], "%d ساعات", "%d ساعة", "%d ساعة"], - d: ["أقل من يوم", "يوم واحد", ["يومان", "يومين"], "%d أيام", "%d يومًا", "%d يوم"], - M: ["أقل من شهر", "شهر واحد", ["شهران", "شهرين"], "%d أشهر", "%d شهرا", "%d شهر"], - y: ["أقل من عام", "عام واحد", ["عامان", "عامين"], "%d أعوام", "%d عامًا", "%d عام"] - }, - f = function(a) { - return function(b, c, f, g) { - var h = d(b), - i = e[a][d(b)]; - return 2 === h && (i = i[c ? 0 : 1]), i.replace(/%d/i, b) - } + 1: "١", + 2: "٢", + 3: "٣", + 4: "٤", + 5: "٥", + 6: "٦", + 7: "٧", + 8: "٨", + 9: "٩", + 0: "٠" + }, + c = { + "١": "1", + "٢": "2", + "٣": "3", + "٤": "4", + "٥": "5", + "٦": "6", + "٧": "7", + "٨": "8", + "٩": "9", + "٠": "0" + }, + d = function(a) { + return 0 === a ? 0 : 1 === a ? 1 : 2 === a ? 2 : a % 100 >= 3 && 10 >= a % 100 ? 3 : a % 100 >= 11 ? 4 : 5 + }, + e = { + s: ["أقل من ثانية", "ثانية واحدة", ["ثانيتان", "ثانيتين"], "%d ثوان", "%d ثانية", "%d ثانية"], + m: ["أقل من دقيقة", "دقيقة واحدة", ["دقيقتان", "دقيقتين"], "%d دقائق", "%d دقيقة", "%d دقيقة"], + h: ["أقل من ساعة", "ساعة واحدة", ["ساعتان", "ساعتين"], "%d ساعات", "%d ساعة", "%d ساعة"], + d: ["أقل من يوم", "يوم واحد", ["يومان", "يومين"], "%d أيام", "%d يومًا", "%d يوم"], + M: ["أقل من شهر", "شهر واحد", ["شهران", "شهرين"], "%d أشهر", "%d شهرا", "%d شهر"], + y: ["أقل من عام", "عام واحد", ["عامان", "عامين"], "%d أعوام", "%d عامًا", "%d عام"] + }, + f = function(a) { + return function(b, c, f, g) { + var h = d(b), + i = e[a][d(b)]; + return 2 === h && (i = i[c ? 0 : 1]), i.replace(/%d/i, b) + } + }, + g = ["كانون الثاني يناير", "شباط فبراير", "آذار مارس", "نيسان أبريل", "أيار مايو", "حزيران يونيو", "تموز يوليو", "آب أغسطس", "أيلول سبتمبر", "تشرين الأول أكتوبر", "تشرين الثاني نوفمبر", "كانون الأول ديسمبر"], + h = (b.defineLocale || b.lang).call(b, "ar", { + months: g, + monthsShort: g, + weekdays: "الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"), + weekdaysShort: "أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت".split("_"), + weekdaysMin: "ح_ن_ث_ر_خ_ج_س".split("_"), + weekdaysParseExact: !0, + longDateFormat: { + LT: "HH:mm", + LTS: "HH:mm:ss", + L: "D/‏M/‏YYYY", + LL: "D MMMM YYYY", + LLL: "D MMMM YYYY HH:mm", + LLLL: "dddd D MMMM YYYY HH:mm" }, - g = ["كانون الثاني يناير", "شباط فبراير", "آذار مارس", "نيسان أبريل", "أيار مايو", "حزيران يونيو", "تموز يوليو", "آب أغسطس", "أيلول سبتمبر", "تشرين الأول أكتوبر", "تشرين الثاني نوفمبر", "كانون الأول ديسمبر"], - h = (b.defineLocale || b.lang).call(b, "ar", { - months: g, - monthsShort: g, - weekdays: "الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"), - weekdaysShort: "أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت".split("_"), - weekdaysMin: "ح_ن_ث_ر_خ_ج_س".split("_"), - longDateFormat: { - LT: "HH:mm", - LTS: "HH:mm:ss", - L: "D/‏M/‏YYYY", - LL: "D MMMM YYYY", - LLL: "D MMMM YYYY HH:mm", - LLLL: "dddd D MMMM YYYY HH:mm" - }, - meridiemParse: /ص|م/, - isPM: function(a) { - return "م" === a - }, - meridiem: function(a, b, c) { - return 12 > a ? "ص" : "م" - }, - calendar: { - sameDay: "[اليوم عند الساعة] LT", - nextDay: "[غدًا عند الساعة] LT", - nextWeek: "dddd [عند الساعة] LT", - lastDay: "[أمس عند الساعة] LT", - lastWeek: "dddd [عند الساعة] LT", - sameElse: "L" - }, - relativeTime: { - future: "بعد %s", - past: "منذ %s", - s: f("s"), - m: f("m"), - mm: f("m"), - h: f("h"), - hh: f("h"), - d: f("d"), - dd: f("d"), - M: f("M"), - MM: f("M"), - y: f("y"), - yy: f("y") - }, - preparse: function(a) { - return a.replace(/\u200f/g, "").replace(/[١٢٣٤٥٦٧٨٩٠]/g, function(a) { - return c[a] - }).replace(/،/g, ",") - }, - postformat: function(b) { - return b.replace(/\d/g, function(b) { - return a[b] - }).replace(/,/g, "،") - }, - week: { - dow: 6, - doy: 12 - } - }); + meridiemParse: /ص|م/, + isPM: function(a) { + return "م" === a + }, + meridiem: function(a, b, c) { + return 12 > a ? "ص" : "م" + }, + calendar: { + sameDay: "[اليوم عند الساعة] LT", + nextDay: "[غدًا عند الساعة] LT", + nextWeek: "dddd [عند الساعة] LT", + lastDay: "[أمس عند الساعة] LT", + lastWeek: "dddd [عند الساعة] LT", + sameElse: "L" + }, + relativeTime: { + future: "بعد %s", + past: "منذ %s", + s: f("s"), + m: f("m"), + mm: f("m"), + h: f("h"), + hh: f("h"), + d: f("d"), + dd: f("d"), + M: f("M"), + MM: f("M"), + y: f("y"), + yy: f("y") + }, + preparse: function(a) { + return a.replace(/\u200f/g, "").replace(/[١٢٣٤٥٦٧٨٩٠]/g, function(a) { + return c[a] + }).replace(/،/g, ",") + }, + postformat: function(b) { + return b.replace(/\d/g, function(b) { + return a[b] + }).replace(/,/g, "،") + }, + week: { + dow: 6, + doy: 12 + } + }); return h }(), a.fullCalendar.datepickerLang("ar", "ar", { closeText: "إغلاق", prevText: "<السابق", nextText: "التالي>", currentText: "اليوم", - monthNames: ["كانون الثاني", "شباط", "آذار", "نيسان", "مايو", "حزيران", "تموز", "آب", "أيلول", "تشرين الأول", "تشرين الثاني", "كانون الأول"], + monthNames: ["يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو", "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"], monthNamesShort: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"], dayNames: ["الأحد", "الاثنين", "الثلاثاء", "الأربعاء", "الخميس", "الجمعة", "السبت"], - dayNamesShort: ["الأحد", "الاثنين", "الثلاثاء", "الأربعاء", "الخميس", "الجمعة", "السبت"], + dayNamesShort: ["أحد", "اثنين", "ثلاثاء", "أربعاء", "خميس", "جمعة", "سبت"], dayNamesMin: ["ح", "ن", "ث", "ر", "خ", "ج", "س"], weekHeader: "أسبوع", dateFormat: "dd/mm/yy", - firstDay: 6, + firstDay: 0, isRTL: !0, showMonthAfterYear: !1, yearSuffix: "" @@ -421,12 +425,12 @@ case 0: case 3: case 6: - return "[В изминалата] dddd [в] LT"; + return "[В изминалата] dddd [в] LT"; case 1: case 2: case 4: case 5: - return "[В изминалия] dddd [в] LT" + return "[В изминалия] dddd [в] LT" } }, sameElse: "L" @@ -449,7 +453,7 @@ ordinalParse: /\d{1,2}-(ев|ен|ти|ви|ри|ми)/, ordinal: function(a) { var b = a % 10, - c = a % 100; + c = a % 100; return 0 === a ? a + "-ев" : 0 === c ? a + "-ен" : c > 10 && 20 > c ? a + "-ти" : 1 === b ? a + "-ви" : 2 === b ? a + "-ри" : 7 === b || 8 === b ? a + "-ми" : a + "-ти" }, week: { @@ -494,9 +498,11 @@ var a = (b.defineLocale || b.lang).call(b, "ca", { months: "gener_febrer_març_abril_maig_juny_juliol_agost_setembre_octubre_novembre_desembre".split("_"), monthsShort: "gen._febr._mar._abr._mai._jun._jul._ag._set._oct._nov._des.".split("_"), + monthsParseExact: !0, weekdays: "diumenge_dilluns_dimarts_dimecres_dijous_divendres_dissabte".split("_"), weekdaysShort: "dg._dl._dt._dc._dj._dv._ds.".split("_"), weekdaysMin: "Dg_Dl_Dt_Dc_Dj_Dv_Ds".split("_"), + weekdaysParseExact: !0, longDateFormat: { LT: "H:mm", LTS: "H:mm:ss", @@ -541,7 +547,7 @@ ordinalParse: /\d{1,2}(r|n|t|è|a)/, ordinal: function(a, b) { var c = 1 === a ? "r" : 2 === a ? "n" : 3 === a ? "r" : 4 === a ? "t" : "è"; - return ("w" === b || "W" === b) && (c = "a"), a + c + return "w" !== b && "W" !== b || (c = "a"), a + c }, week: { dow: 1, @@ -588,121 +594,121 @@ var f = b + " "; switch (d) { case "s": - return c || e ? "pár sekund" : "pár sekundami"; + return c || e ? "pár sekund" : "pár sekundami"; case "m": - return c ? "minuta" : e ? "minutu" : "minutou"; + return c ? "minuta" : e ? "minutu" : "minutou"; case "mm": - return c || e ? f + (a(b) ? "minuty" : "minut") : f + "minutami"; + return c || e ? f + (a(b) ? "minuty" : "minut") : f + "minutami"; case "h": - return c ? "hodina" : e ? "hodinu" : "hodinou"; + return c ? "hodina" : e ? "hodinu" : "hodinou"; case "hh": - return c || e ? f + (a(b) ? "hodiny" : "hodin") : f + "hodinami"; + return c || e ? f + (a(b) ? "hodiny" : "hodin") : f + "hodinami"; case "d": - return c || e ? "den" : "dnem"; + return c || e ? "den" : "dnem"; case "dd": - return c || e ? f + (a(b) ? "dny" : "dní") : f + "dny"; + return c || e ? f + (a(b) ? "dny" : "dní") : f + "dny"; case "M": - return c || e ? "měsíc" : "měsícem"; + return c || e ? "měsíc" : "měsícem"; case "MM": - return c || e ? f + (a(b) ? "měsíce" : "měsíců") : f + "měsíci"; + return c || e ? f + (a(b) ? "měsíce" : "měsíců") : f + "měsíci"; case "y": - return c || e ? "rok" : "rokem"; + return c || e ? "rok" : "rokem"; case "yy": - return c || e ? f + (a(b) ? "roky" : "let") : f + "lety" + return c || e ? f + (a(b) ? "roky" : "let") : f + "lety" } } var d = "leden_únor_březen_duben_květen_červen_červenec_srpen_září_říjen_listopad_prosinec".split("_"), - e = "led_úno_bře_dub_kvě_čvn_čvc_srp_zář_říj_lis_pro".split("_"), - f = (b.defineLocale || b.lang).call(b, "cs", { - months: d, - monthsShort: e, - monthsParse: function(a, b) { - var c, d = []; - for (c = 0; 12 > c; c++) d[c] = new RegExp("^" + a[c] + "$|^" + b[c] + "$", "i"); - return d - }(d, e), - shortMonthsParse: function(a) { - var b, c = []; - for (b = 0; 12 > b; b++) c[b] = new RegExp("^" + a[b] + "$", "i"); - return c - }(e), - longMonthsParse: function(a) { - var b, c = []; - for (b = 0; 12 > b; b++) c[b] = new RegExp("^" + a[b] + "$", "i"); - return c - }(d), - weekdays: "neděle_pondělí_úterý_středa_čtvrtek_pátek_sobota".split("_"), - weekdaysShort: "ne_po_út_st_čt_pá_so".split("_"), - weekdaysMin: "ne_po_út_st_čt_pá_so".split("_"), - longDateFormat: { - LT: "H:mm", - LTS: "H:mm:ss", - L: "DD.MM.YYYY", - LL: "D. MMMM YYYY", - LLL: "D. MMMM YYYY H:mm", - LLLL: "dddd D. MMMM YYYY H:mm" - }, - calendar: { - sameDay: "[dnes v] LT", - nextDay: "[zítra v] LT", - nextWeek: function() { - switch (this.day()) { - case 0: - return "[v neděli v] LT"; - case 1: - case 2: - return "[v] dddd [v] LT"; - case 3: - return "[ve středu v] LT"; - case 4: - return "[ve čtvrtek v] LT"; - case 5: - return "[v pátek v] LT"; - case 6: - return "[v sobotu v] LT" - } - }, - lastDay: "[včera v] LT", - lastWeek: function() { - switch (this.day()) { - case 0: - return "[minulou neděli v] LT"; - case 1: - case 2: - return "[minulé] dddd [v] LT"; - case 3: - return "[minulou středu v] LT"; - case 4: - case 5: - return "[minulý] dddd [v] LT"; - case 6: - return "[minulou sobotu v] LT" - } - }, - sameElse: "L" + e = "led_úno_bře_dub_kvě_čvn_čvc_srp_zář_říj_lis_pro".split("_"), + f = (b.defineLocale || b.lang).call(b, "cs", { + months: d, + monthsShort: e, + monthsParse: function(a, b) { + var c, d = []; + for (c = 0; 12 > c; c++) d[c] = new RegExp("^" + a[c] + "$|^" + b[c] + "$", "i"); + return d + }(d, e), + shortMonthsParse: function(a) { + var b, c = []; + for (b = 0; 12 > b; b++) c[b] = new RegExp("^" + a[b] + "$", "i"); + return c + }(e), + longMonthsParse: function(a) { + var b, c = []; + for (b = 0; 12 > b; b++) c[b] = new RegExp("^" + a[b] + "$", "i"); + return c + }(d), + weekdays: "neděle_pondělí_úterý_středa_čtvrtek_pátek_sobota".split("_"), + weekdaysShort: "ne_po_út_st_čt_pá_so".split("_"), + weekdaysMin: "ne_po_út_st_čt_pá_so".split("_"), + longDateFormat: { + LT: "H:mm", + LTS: "H:mm:ss", + L: "DD.MM.YYYY", + LL: "D. MMMM YYYY", + LLL: "D. MMMM YYYY H:mm", + LLLL: "dddd D. MMMM YYYY H:mm" + }, + calendar: { + sameDay: "[dnes v] LT", + nextDay: "[zítra v] LT", + nextWeek: function() { + switch (this.day()) { + case 0: + return "[v neděli v] LT"; + case 1: + case 2: + return "[v] dddd [v] LT"; + case 3: + return "[ve středu v] LT"; + case 4: + return "[ve čtvrtek v] LT"; + case 5: + return "[v pátek v] LT"; + case 6: + return "[v sobotu v] LT" + } }, - relativeTime: { - future: "za %s", - past: "před %s", - s: c, - m: c, - mm: c, - h: c, - hh: c, - d: c, - dd: c, - M: c, - MM: c, - y: c, - yy: c + lastDay: "[včera v] LT", + lastWeek: function() { + switch (this.day()) { + case 0: + return "[minulou neděli v] LT"; + case 1: + case 2: + return "[minulé] dddd [v] LT"; + case 3: + return "[minulou středu v] LT"; + case 4: + case 5: + return "[minulý] dddd [v] LT"; + case 6: + return "[minulou sobotu v] LT" + } }, - ordinalParse: /\d{1,2}\./, - ordinal: "%d.", - week: { - dow: 1, - doy: 4 - } - }); + sameElse: "L" + }, + relativeTime: { + future: "za %s", + past: "před %s", + s: c, + m: c, + mm: c, + h: c, + hh: c, + d: c, + dd: c, + M: c, + MM: c, + y: c, + yy: c + }, + ordinalParse: /\d{1,2}\./, + ordinal: "%d.", + week: { + dow: 1, + doy: 4 + } + }); return f }(), a.fullCalendar.datepickerLang("cs", "cs", { closeText: "Zavřít", @@ -828,9 +834,11 @@ var c = (b.defineLocale || b.lang).call(b, "de-at", { months: "Jänner_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"), monthsShort: "Jän._Febr._Mrz._Apr._Mai_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"), + monthsParseExact: !0, weekdays: "Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"), weekdaysShort: "So._Mo._Di._Mi._Do._Fr._Sa.".split("_"), weekdaysMin: "So_Mo_Di_Mi_Do_Fr_Sa".split("_"), + weekdaysParseExact: !0, longDateFormat: { LT: "HH:mm", LTS: "HH:mm:ss", @@ -919,9 +927,11 @@ var c = (b.defineLocale || b.lang).call(b, "de", { months: "Januar_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"), monthsShort: "Jan._Febr._Mrz._Apr._Mai_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"), + monthsParseExact: !0, weekdays: "Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"), weekdaysShort: "So._Mo._Di._Mi._Do._Fr._Sa.".split("_"), weekdaysMin: "So_Mo_Di_Mi_Do_Fr_Sa".split("_"), + weekdaysParseExact: !0, longDateFormat: { LT: "HH:mm", LTS: "HH:mm:ss", @@ -1030,16 +1040,16 @@ lastWeek: function() { switch (this.day()) { case 6: - return "[το προηγούμενο] dddd [{}] LT"; + return "[το προηγούμενο] dddd [{}] LT"; default: - return "[την προηγούμενη] dddd [{}] LT" + return "[την προηγούμενη] dddd [{}] LT" } }, sameElse: "L" }, calendar: function(b, c) { var d = this._calendarEl[b], - e = c && c.hours(); + e = c && c.hours(); return a(d) && (d = d.apply(c)), d.replace("{}", e % 12 === 1 ? "στη" : "στις") }, relativeTime: { @@ -1097,10 +1107,7 @@ "use strict"; var a = (b.defineLocale || b.lang).call(b, "en-au", { months: "January_February_March_April_May_June_July_August_September_October_November_December".split("_"), - monthsShort: "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sept_Oct_Nov_Dec".split("_"), - monthsParse: [/^jan/i, /^feb/i, /^mar/i, /^apr/i, /^may/i, /^jun/i, /^jul/i, /^aug/i, /^sep/i, /^oct/i, /^nov/i, /^dec/i], - longMonthsParse: [/^january$/i, /^february$/i, /^march$/i, /^april$/i, /^may$/i, /^june$/i, /^july$/i, /^august$/i, /^september$/i, /^october$/i, /^november$/i, /^december$/i], - shortMonthsParse: [/^jan$/i, /^feb$/i, /^mar$/i, /^apr$/i, /^may$/i, /^jun$/i, /^jul$/i, /^aug/i, /^sept?$/i, /^oct$/i, /^nov$/i, /^dec$/i], + monthsShort: "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"), weekdays: "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"), weekdaysShort: "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"), weekdaysMin: "Su_Mo_Tu_We_Th_Fr_Sa".split("_"), @@ -1138,7 +1145,7 @@ ordinalParse: /\d{1,2}(st|nd|rd|th)/, ordinal: function(a) { var b = a % 10, - c = 1 === ~~(a % 100 / 10) ? "th" : 1 === b ? "st" : 2 === b ? "nd" : 3 === b ? "rd" : "th"; + c = 1 === ~~(a % 100 / 10) ? "th" : 1 === b ? "st" : 2 === b ? "nd" : 3 === b ? "rd" : "th"; return a + c }, week: { @@ -1170,10 +1177,7 @@ "use strict"; var a = (b.defineLocale || b.lang).call(b, "en-ca", { months: "January_February_March_April_May_June_July_August_September_October_November_December".split("_"), - monthsShort: "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sept_Oct_Nov_Dec".split("_"), - monthsParse: [/^jan/i, /^feb/i, /^mar/i, /^apr/i, /^may/i, /^jun/i, /^jul/i, /^aug/i, /^sep/i, /^oct/i, /^nov/i, /^dec/i], - longMonthsParse: [/^january$/i, /^february$/i, /^march$/i, /^april$/i, /^may$/i, /^june$/i, /^july$/i, /^august$/i, /^september$/i, /^october$/i, /^november$/i, /^december$/i], - shortMonthsParse: [/^jan$/i, /^feb$/i, /^mar$/i, /^apr$/i, /^may$/i, /^jun$/i, /^jul$/i, /^aug/i, /^sept?$/i, /^oct$/i, /^nov$/i, /^dec$/i], + monthsShort: "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"), weekdays: "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"), weekdaysShort: "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"), weekdaysMin: "Su_Mo_Tu_We_Th_Fr_Sa".split("_"), @@ -1181,9 +1185,9 @@ LT: "h:mm A", LTS: "h:mm:ss A", L: "YYYY-MM-DD", - LL: "D MMMM, YYYY", - LLL: "D MMMM, YYYY h:mm A", - LLLL: "dddd, D MMMM, YYYY h:mm A" + LL: "MMMM D, YYYY", + LLL: "MMMM D, YYYY h:mm A", + LLLL: "dddd, MMMM D, YYYY h:mm A" }, calendar: { sameDay: "[Today at] LT", @@ -1211,7 +1215,7 @@ ordinalParse: /\d{1,2}(st|nd|rd|th)/, ordinal: function(a) { var b = a % 10, - c = 1 === ~~(a % 100 / 10) ? "th" : 1 === b ? "st" : 2 === b ? "nd" : 3 === b ? "rd" : "th"; + c = 1 === ~~(a % 100 / 10) ? "th" : 1 === b ? "st" : 2 === b ? "nd" : 3 === b ? "rd" : "th"; return a + c } }); @@ -1223,10 +1227,7 @@ "use strict"; var a = (b.defineLocale || b.lang).call(b, "en-gb", { months: "January_February_March_April_May_June_July_August_September_October_November_December".split("_"), - monthsShort: "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sept_Oct_Nov_Dec".split("_"), - monthsParse: [/^jan/i, /^feb/i, /^mar/i, /^apr/i, /^may/i, /^jun/i, /^jul/i, /^aug/i, /^sep/i, /^oct/i, /^nov/i, /^dec/i], - longMonthsParse: [/^january$/i, /^february$/i, /^march$/i, /^april$/i, /^may$/i, /^june$/i, /^july$/i, /^august$/i, /^september$/i, /^october$/i, /^november$/i, /^december$/i], - shortMonthsParse: [/^jan$/i, /^feb$/i, /^mar$/i, /^apr$/i, /^may$/i, /^jun$/i, /^jul$/i, /^aug/i, /^sept?$/i, /^oct$/i, /^nov$/i, /^dec$/i], + monthsShort: "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"), weekdays: "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"), weekdaysShort: "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"), weekdaysMin: "Su_Mo_Tu_We_Th_Fr_Sa".split("_"), @@ -1264,7 +1265,7 @@ ordinalParse: /\d{1,2}(st|nd|rd|th)/, ordinal: function(a) { var b = a % 10, - c = 1 === ~~(a % 100 / 10) ? "th" : 1 === b ? "st" : 2 === b ? "nd" : 3 === b ? "rd" : "th"; + c = 1 === ~~(a % 100 / 10) ? "th" : 1 === b ? "st" : 2 === b ? "nd" : 3 === b ? "rd" : "th"; return a + c }, week: { @@ -1297,9 +1298,6 @@ var a = (b.defineLocale || b.lang).call(b, "en-ie", { months: "January_February_March_April_May_June_July_August_September_October_November_December".split("_"), monthsShort: "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"), - monthsParse: [/^jan/i, /^feb/i, /^mar/i, /^apr/i, /^may/i, /^jun/i, /^jul/i, /^aug/i, /^sep/i, /^oct/i, /^nov/i, /^dec/i], - longMonthsParse: [/^january$/i, /^february$/i, /^march$/i, /^april$/i, /^may$/i, /^june$/i, /^july$/i, /^august$/i, /^september$/i, /^october$/i, /^november$/i, /^december$/i], - shortMonthsParse: [/^jan$/i, /^feb$/i, /^mar$/i, /^apr$/i, /^may$/i, /^jun$/i, /^jul$/i, /^aug/i, /^sept?$/i, /^oct$/i, /^nov$/i, /^dec$/i], weekdays: "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"), weekdaysShort: "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"), weekdaysMin: "Su_Mo_Tu_We_Th_Fr_Sa".split("_"), @@ -1337,7 +1335,7 @@ ordinalParse: /\d{1,2}(st|nd|rd|th)/, ordinal: function(a) { var b = a % 10, - c = 1 === ~~(a % 100 / 10) ? "th" : 1 === b ? "st" : 2 === b ? "nd" : 3 === b ? "rd" : "th"; + c = 1 === ~~(a % 100 / 10) ? "th" : 1 === b ? "st" : 2 === b ? "nd" : 3 === b ? "rd" : "th"; return a + c }, week: { @@ -1353,10 +1351,7 @@ "use strict"; var a = (b.defineLocale || b.lang).call(b, "en-nz", { months: "January_February_March_April_May_June_July_August_September_October_November_December".split("_"), - monthsShort: "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sept_Oct_Nov_Dec".split("_"), - monthsParse: [/^jan/i, /^feb/i, /^mar/i, /^apr/i, /^may/i, /^jun/i, /^jul/i, /^aug/i, /^sep/i, /^oct/i, /^nov/i, /^dec/i], - longMonthsParse: [/^january$/i, /^february$/i, /^march$/i, /^april$/i, /^may$/i, /^june$/i, /^july$/i, /^august$/i, /^september$/i, /^october$/i, /^november$/i, /^december$/i], - shortMonthsParse: [/^jan$/i, /^feb$/i, /^mar$/i, /^apr$/i, /^may$/i, /^jun$/i, /^jul$/i, /^aug/i, /^sept?$/i, /^oct$/i, /^nov$/i, /^dec$/i], + monthsShort: "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"), weekdays: "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"), weekdaysShort: "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"), weekdaysMin: "Su_Mo_Tu_We_Th_Fr_Sa".split("_"), @@ -1394,7 +1389,7 @@ ordinalParse: /\d{1,2}(st|nd|rd|th)/, ordinal: function(a) { var b = a % 10, - c = 1 === ~~(a % 100 / 10) ? "th" : 1 === b ? "st" : 2 === b ? "nd" : 3 === b ? "rd" : "th"; + c = 1 === ~~(a % 100 / 10) ? "th" : 1 === b ? "st" : 2 === b ? "nd" : 3 === b ? "rd" : "th"; return a + c }, week: { @@ -1425,63 +1420,65 @@ ! function() { "use strict"; var a = "ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.".split("_"), - c = "ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic".split("_"), - d = (b.defineLocale || b.lang).call(b, "es", { - months: "enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre".split("_"), - monthsShort: function(b, d) { - return /-MMM-/.test(d) ? c[b.month()] : a[b.month()] + c = "ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic".split("_"), + d = (b.defineLocale || b.lang).call(b, "es", { + months: "enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre".split("_"), + monthsShort: function(b, d) { + return /-MMM-/.test(d) ? c[b.month()] : a[b.month()] + }, + monthsParseExact: !0, + weekdays: "domingo_lunes_martes_miércoles_jueves_viernes_sábado".split("_"), + weekdaysShort: "dom._lun._mar._mié._jue._vie._sáb.".split("_"), + weekdaysMin: "do_lu_ma_mi_ju_vi_sá".split("_"), + weekdaysParseExact: !0, + longDateFormat: { + LT: "H:mm", + LTS: "H:mm:ss", + L: "DD/MM/YYYY", + LL: "D [de] MMMM [de] YYYY", + LLL: "D [de] MMMM [de] YYYY H:mm", + LLLL: "dddd, D [de] MMMM [de] YYYY H:mm" + }, + calendar: { + sameDay: function() { + return "[hoy a la" + (1 !== this.hours() ? "s" : "") + "] LT" + }, + nextDay: function() { + return "[mañana a la" + (1 !== this.hours() ? "s" : "") + "] LT" }, - weekdays: "domingo_lunes_martes_miércoles_jueves_viernes_sábado".split("_"), - weekdaysShort: "dom._lun._mar._mié._jue._vie._sáb.".split("_"), - weekdaysMin: "do_lu_ma_mi_ju_vi_sá".split("_"), - longDateFormat: { - LT: "H:mm", - LTS: "H:mm:ss", - L: "DD/MM/YYYY", - LL: "D [de] MMMM [de] YYYY", - LLL: "D [de] MMMM [de] YYYY H:mm", - LLLL: "dddd, D [de] MMMM [de] YYYY H:mm" + nextWeek: function() { + return "dddd [a la" + (1 !== this.hours() ? "s" : "") + "] LT" }, - calendar: { - sameDay: function() { - return "[hoy a la" + (1 !== this.hours() ? "s" : "") + "] LT" - }, - nextDay: function() { - return "[mañana a la" + (1 !== this.hours() ? "s" : "") + "] LT" - }, - nextWeek: function() { - return "dddd [a la" + (1 !== this.hours() ? "s" : "") + "] LT" - }, - lastDay: function() { - return "[ayer a la" + (1 !== this.hours() ? "s" : "") + "] LT" - }, - lastWeek: function() { - return "[el] dddd [pasado a la" + (1 !== this.hours() ? "s" : "") + "] LT" - }, - sameElse: "L" + lastDay: function() { + return "[ayer a la" + (1 !== this.hours() ? "s" : "") + "] LT" }, - relativeTime: { - future: "en %s", - past: "hace %s", - s: "unos segundos", - m: "un minuto", - mm: "%d minutos", - h: "una hora", - hh: "%d horas", - d: "un día", - dd: "%d días", - M: "un mes", - MM: "%d meses", - y: "un año", - yy: "%d años" + lastWeek: function() { + return "[el] dddd [pasado a la" + (1 !== this.hours() ? "s" : "") + "] LT" }, - ordinalParse: /\d{1,2}º/, - ordinal: "%dº", - week: { - dow: 1, - doy: 4 - } - }); + sameElse: "L" + }, + relativeTime: { + future: "en %s", + past: "hace %s", + s: "unos segundos", + m: "un minuto", + mm: "%d minutos", + h: "una hora", + hh: "%d horas", + d: "un día", + dd: "%d días", + M: "un mes", + MM: "%d meses", + y: "un año", + yy: "%d años" + }, + ordinalParse: /\d{1,2}º/, + ordinal: "%dº", + week: { + dow: 1, + doy: 4 + } + }); return d }(), a.fullCalendar.datepickerLang("es", "es", { closeText: "Cerrar", @@ -1514,90 +1511,91 @@ ! function() { "use strict"; var a = { - 1: "۱", - 2: "۲", - 3: "۳", - 4: "۴", - 5: "۵", - 6: "۶", - 7: "۷", - 8: "۸", - 9: "۹", - 0: "۰" - }, - c = { - "۱": "1", - "۲": "2", - "۳": "3", - "۴": "4", - "۵": "5", - "۶": "6", - "۷": "7", - "۸": "8", - "۹": "9", - "۰": "0" - }, - d = (b.defineLocale || b.lang).call(b, "fa", { - months: "ژانویه_فوریه_مارس_آوریل_مه_ژوئن_ژوئیه_اوت_سپتامبر_اکتبر_نوامبر_دسامبر".split("_"), - monthsShort: "ژانویه_فوریه_مارس_آوریل_مه_ژوئن_ژوئیه_اوت_سپتامبر_اکتبر_نوامبر_دسامبر".split("_"), - weekdays: "یک‌شنبه_دوشنبه_سه‌شنبه_چهارشنبه_پنج‌شنبه_جمعه_شنبه".split("_"), - weekdaysShort: "یک‌شنبه_دوشنبه_سه‌شنبه_چهارشنبه_پنج‌شنبه_جمعه_شنبه".split("_"), - weekdaysMin: "ی_د_س_چ_پ_ج_ش".split("_"), - longDateFormat: { - LT: "HH:mm", - LTS: "HH:mm:ss", - L: "DD/MM/YYYY", - LL: "D MMMM YYYY", - LLL: "D MMMM YYYY HH:mm", - LLLL: "dddd, D MMMM YYYY HH:mm" - }, - meridiemParse: /قبل از ظهر|بعد از ظهر/, - isPM: function(a) { - return /بعد از ظهر/.test(a) - }, - meridiem: function(a, b, c) { - return 12 > a ? "قبل از ظهر" : "بعد از ظهر" - }, - calendar: { - sameDay: "[امروز ساعت] LT", - nextDay: "[فردا ساعت] LT", - nextWeek: "dddd [ساعت] LT", - lastDay: "[دیروز ساعت] LT", - lastWeek: "dddd [پیش] [ساعت] LT", - sameElse: "L" - }, - relativeTime: { - future: "در %s", - past: "%s پیش", - s: "چندین ثانیه", - m: "یک دقیقه", - mm: "%d دقیقه", - h: "یک ساعت", - hh: "%d ساعت", - d: "یک روز", - dd: "%d روز", - M: "یک ماه", - MM: "%d ماه", - y: "یک سال", - yy: "%d سال" - }, - preparse: function(a) { - return a.replace(/[۰-۹]/g, function(a) { - return c[a] - }).replace(/،/g, ",") - }, - postformat: function(b) { - return b.replace(/\d/g, function(b) { - return a[b] - }).replace(/,/g, "،") - }, - ordinalParse: /\d{1,2}م/, - ordinal: "%dم", - week: { - dow: 6, - doy: 12 - } - }); + 1: "۱", + 2: "۲", + 3: "۳", + 4: "۴", + 5: "۵", + 6: "۶", + 7: "۷", + 8: "۸", + 9: "۹", + 0: "۰" + }, + c = { + "۱": "1", + "۲": "2", + "۳": "3", + "۴": "4", + "۵": "5", + "۶": "6", + "۷": "7", + "۸": "8", + "۹": "9", + "۰": "0" + }, + d = (b.defineLocale || b.lang).call(b, "fa", { + months: "ژانویه_فوریه_مارس_آوریل_مه_ژوئن_ژوئیه_اوت_سپتامبر_اکتبر_نوامبر_دسامبر".split("_"), + monthsShort: "ژانویه_فوریه_مارس_آوریل_مه_ژوئن_ژوئیه_اوت_سپتامبر_اکتبر_نوامبر_دسامبر".split("_"), + weekdays: "یک‌شنبه_دوشنبه_سه‌شنبه_چهارشنبه_پنج‌شنبه_جمعه_شنبه".split("_"), + weekdaysShort: "یک‌شنبه_دوشنبه_سه‌شنبه_چهارشنبه_پنج‌شنبه_جمعه_شنبه".split("_"), + weekdaysMin: "ی_د_س_چ_پ_ج_ش".split("_"), + weekdaysParseExact: !0, + longDateFormat: { + LT: "HH:mm", + LTS: "HH:mm:ss", + L: "DD/MM/YYYY", + LL: "D MMMM YYYY", + LLL: "D MMMM YYYY HH:mm", + LLLL: "dddd, D MMMM YYYY HH:mm" + }, + meridiemParse: /قبل از ظهر|بعد از ظهر/, + isPM: function(a) { + return /بعد از ظهر/.test(a) + }, + meridiem: function(a, b, c) { + return 12 > a ? "قبل از ظهر" : "بعد از ظهر" + }, + calendar: { + sameDay: "[امروز ساعت] LT", + nextDay: "[فردا ساعت] LT", + nextWeek: "dddd [ساعت] LT", + lastDay: "[دیروز ساعت] LT", + lastWeek: "dddd [پیش] [ساعت] LT", + sameElse: "L" + }, + relativeTime: { + future: "در %s", + past: "%s پیش", + s: "چندین ثانیه", + m: "یک دقیقه", + mm: "%d دقیقه", + h: "یک ساعت", + hh: "%d ساعت", + d: "یک روز", + dd: "%d روز", + M: "یک ماه", + MM: "%d ماه", + y: "یک سال", + yy: "%d سال" + }, + preparse: function(a) { + return a.replace(/[۰-۹]/g, function(a) { + return c[a] + }).replace(/،/g, ",") + }, + postformat: function(b) { + return b.replace(/\d/g, function(b) { + return a[b] + }).replace(/,/g, "،") + }, + ordinalParse: /\d{1,2}م/, + ordinal: "%dم", + week: { + dow: 6, + doy: 12 + } + }); return d }(), a.fullCalendar.datepickerLang("fa", "fa", { closeText: "بستن", @@ -1636,31 +1634,31 @@ var f = ""; switch (d) { case "s": - return e ? "muutaman sekunnin" : "muutama sekunti"; + return e ? "muutaman sekunnin" : "muutama sekunti"; case "m": - return e ? "minuutin" : "minuutti"; + return e ? "minuutin" : "minuutti"; case "mm": - f = e ? "minuutin" : "minuuttia"; - break; + f = e ? "minuutin" : "minuuttia"; + break; case "h": - return e ? "tunnin" : "tunti"; + return e ? "tunnin" : "tunti"; case "hh": - f = e ? "tunnin" : "tuntia"; - break; + f = e ? "tunnin" : "tuntia"; + break; case "d": - return e ? "päivän" : "päivä"; + return e ? "päivän" : "päivä"; case "dd": - f = e ? "päivän" : "päivää"; - break; + f = e ? "päivän" : "päivää"; + break; case "M": - return e ? "kuukauden" : "kuukausi"; + return e ? "kuukauden" : "kuukausi"; case "MM": - f = e ? "kuukauden" : "kuukautta"; - break; + f = e ? "kuukauden" : "kuukautta"; + break; case "y": - return e ? "vuoden" : "vuosi"; + return e ? "vuoden" : "vuosi"; case "yy": - f = e ? "vuoden" : "vuotta" + f = e ? "vuoden" : "vuotta" } return f = c(a, e) + " " + f } @@ -1669,55 +1667,55 @@ return 10 > a ? b ? e[a] : d[a] : a } var d = "nolla yksi kaksi kolme neljä viisi kuusi seitsemän kahdeksan yhdeksän".split(" "), - e = ["nolla", "yhden", "kahden", "kolmen", "neljän", "viiden", "kuuden", d[7], d[8], d[9]], - f = (b.defineLocale || b.lang).call(b, "fi", { - months: "tammikuu_helmikuu_maaliskuu_huhtikuu_toukokuu_kesäkuu_heinäkuu_elokuu_syyskuu_lokakuu_marraskuu_joulukuu".split("_"), - monthsShort: "tammi_helmi_maalis_huhti_touko_kesä_heinä_elo_syys_loka_marras_joulu".split("_"), - weekdays: "sunnuntai_maanantai_tiistai_keskiviikko_torstai_perjantai_lauantai".split("_"), - weekdaysShort: "su_ma_ti_ke_to_pe_la".split("_"), - weekdaysMin: "su_ma_ti_ke_to_pe_la".split("_"), - longDateFormat: { - LT: "HH.mm", - LTS: "HH.mm.ss", - L: "DD.MM.YYYY", - LL: "Do MMMM[ta] YYYY", - LLL: "Do MMMM[ta] YYYY, [klo] HH.mm", - LLLL: "dddd, Do MMMM[ta] YYYY, [klo] HH.mm", - l: "D.M.YYYY", - ll: "Do MMM YYYY", - lll: "Do MMM YYYY, [klo] HH.mm", - llll: "ddd, Do MMM YYYY, [klo] HH.mm" - }, - calendar: { - sameDay: "[tänään] [klo] LT", - nextDay: "[huomenna] [klo] LT", - nextWeek: "dddd [klo] LT", - lastDay: "[eilen] [klo] LT", - lastWeek: "[viime] dddd[na] [klo] LT", - sameElse: "L" - }, - relativeTime: { - future: "%s päästä", - past: "%s sitten", - s: a, - m: a, - mm: a, - h: a, - hh: a, - d: a, - dd: a, - M: a, - MM: a, - y: a, - yy: a - }, - ordinalParse: /\d{1,2}\./, - ordinal: "%d.", - week: { - dow: 1, - doy: 4 - } - }); + e = ["nolla", "yhden", "kahden", "kolmen", "neljän", "viiden", "kuuden", d[7], d[8], d[9]], + f = (b.defineLocale || b.lang).call(b, "fi", { + months: "tammikuu_helmikuu_maaliskuu_huhtikuu_toukokuu_kesäkuu_heinäkuu_elokuu_syyskuu_lokakuu_marraskuu_joulukuu".split("_"), + monthsShort: "tammi_helmi_maalis_huhti_touko_kesä_heinä_elo_syys_loka_marras_joulu".split("_"), + weekdays: "sunnuntai_maanantai_tiistai_keskiviikko_torstai_perjantai_lauantai".split("_"), + weekdaysShort: "su_ma_ti_ke_to_pe_la".split("_"), + weekdaysMin: "su_ma_ti_ke_to_pe_la".split("_"), + longDateFormat: { + LT: "HH.mm", + LTS: "HH.mm.ss", + L: "DD.MM.YYYY", + LL: "Do MMMM[ta] YYYY", + LLL: "Do MMMM[ta] YYYY, [klo] HH.mm", + LLLL: "dddd, Do MMMM[ta] YYYY, [klo] HH.mm", + l: "D.M.YYYY", + ll: "Do MMM YYYY", + lll: "Do MMM YYYY, [klo] HH.mm", + llll: "ddd, Do MMM YYYY, [klo] HH.mm" + }, + calendar: { + sameDay: "[tänään] [klo] LT", + nextDay: "[huomenna] [klo] LT", + nextWeek: "dddd [klo] LT", + lastDay: "[eilen] [klo] LT", + lastWeek: "[viime] dddd[na] [klo] LT", + sameElse: "L" + }, + relativeTime: { + future: "%s päästä", + past: "%s sitten", + s: a, + m: a, + mm: a, + h: a, + hh: a, + d: a, + dd: a, + M: a, + MM: a, + y: a, + yy: a + }, + ordinalParse: /\d{1,2}\./, + ordinal: "%d.", + week: { + dow: 1, + doy: 4 + } + }); return f }(), a.fullCalendar.datepickerLang("fi", "fi", { closeText: "Sulje", @@ -1752,9 +1750,11 @@ var a = (b.defineLocale || b.lang).call(b, "fr-ca", { months: "janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre".split("_"), monthsShort: "janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.".split("_"), + monthsParseExact: !0, weekdays: "dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"), weekdaysShort: "dim._lun._mar._mer._jeu._ven._sam.".split("_"), weekdaysMin: "Di_Lu_Ma_Me_Je_Ve_Sa".split("_"), + weekdaysParseExact: !0, longDateFormat: { LT: "HH:mm", LTS: "HH:mm:ss", @@ -1825,9 +1825,11 @@ var a = (b.defineLocale || b.lang).call(b, "fr-ch", { months: "janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre".split("_"), monthsShort: "janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.".split("_"), + monthsParseExact: !0, weekdays: "dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"), weekdaysShort: "dim._lun._mar._mer._jeu._ven._sam.".split("_"), weekdaysMin: "Di_Lu_Ma_Me_Je_Ve_Sa".split("_"), + weekdaysParseExact: !0, longDateFormat: { LT: "HH:mm", LTS: "HH:mm:ss", @@ -1902,9 +1904,11 @@ var a = (b.defineLocale || b.lang).call(b, "fr", { months: "janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre".split("_"), monthsShort: "janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.".split("_"), + monthsParseExact: !0, weekdays: "dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"), weekdaysShort: "dim._lun._mar._mer._jeu._ven._sam.".split("_"), weekdaysMin: "Di_Lu_Ma_Me_Je_Ve_Sa".split("_"), + weekdaysParseExact: !0, longDateFormat: { LT: "HH:mm", LTS: "HH:mm:ss", @@ -2024,9 +2028,16 @@ yy: function(a) { return 2 === a ? "שנתיים" : a % 10 === 0 && 10 !== a ? a + " שנה" : a + " שנים" } - } - }); - return a + }, + meridiemParse: /אחה"צ|לפנה"צ|אחרי הצהריים|לפני הצהריים|לפנות בוקר|בבוקר|בערב/i, + isPM: function(a) { + return /^(אחה"צ|אחרי הצהריים|בערב)$/.test(a) + }, + meridiem: function(a, b, c) { + return 5 > a ? "לפנות בוקר" : 10 > a ? "בבוקר" : 12 > a ? c ? 'לפנה"צ' : "לפני הצהריים" : 18 > a ? c ? 'אחה"צ' : "אחרי הצהריים" : "בערב" + } + }); + return a }(), a.fullCalendar.datepickerLang("he", "he", { closeText: "סגור", prevText: "<הקודם", @@ -2059,88 +2070,89 @@ ! function() { "use strict"; var a = { - 1: "१", - 2: "२", - 3: "३", - 4: "४", - 5: "५", - 6: "६", - 7: "७", - 8: "८", - 9: "९", - 0: "०" - }, - c = { - "१": "1", - "२": "2", - "३": "3", - "४": "4", - "५": "5", - "६": "6", - "७": "7", - "८": "8", - "९": "9", - "०": "0" - }, - d = (b.defineLocale || b.lang).call(b, "hi", { - months: "जनवरी_फ़रवरी_मार्च_अप्रैल_मई_जून_जुलाई_अगस्त_सितम्बर_अक्टूबर_नवम्बर_दिसम्बर".split("_"), - monthsShort: "जन._फ़र._मार्च_अप्रै._मई_जून_जुल._अग._सित._अक्टू._नव._दिस.".split("_"), - weekdays: "रविवार_सोमवार_मंगलवार_बुधवार_गुरूवार_शुक्रवार_शनिवार".split("_"), - weekdaysShort: "रवि_सोम_मंगल_बुध_गुरू_शुक्र_शनि".split("_"), - weekdaysMin: "र_सो_मं_बु_गु_शु_श".split("_"), - longDateFormat: { - LT: "A h:mm बजे", - LTS: "A h:mm:ss बजे", - L: "DD/MM/YYYY", - LL: "D MMMM YYYY", - LLL: "D MMMM YYYY, A h:mm बजे", - LLLL: "dddd, D MMMM YYYY, A h:mm बजे" - }, - calendar: { - sameDay: "[आज] LT", - nextDay: "[कल] LT", - nextWeek: "dddd, LT", - lastDay: "[कल] LT", - lastWeek: "[पिछले] dddd, LT", - sameElse: "L" - }, - relativeTime: { - future: "%s में", - past: "%s पहले", - s: "कुछ ही क्षण", - m: "एक मिनट", - mm: "%d मिनट", - h: "एक घंटा", - hh: "%d घंटे", - d: "एक दिन", - dd: "%d दिन", - M: "एक महीने", - MM: "%d महीने", - y: "एक वर्ष", - yy: "%d वर्ष" - }, - preparse: function(a) { - return a.replace(/[१२३४५६७८९०]/g, function(a) { - return c[a] - }) - }, - postformat: function(b) { - return b.replace(/\d/g, function(b) { - return a[b] - }) - }, - meridiemParse: /रात|सुबह|दोपहर|शाम/, - meridiemHour: function(a, b) { - return 12 === a && (a = 0), "रात" === b ? 4 > a ? a : a + 12 : "सुबह" === b ? a : "दोपहर" === b ? a >= 10 ? a : a + 12 : "शाम" === b ? a + 12 : void 0 - }, - meridiem: function(a, b, c) { - return 4 > a ? "रात" : 10 > a ? "सुबह" : 17 > a ? "दोपहर" : 20 > a ? "शाम" : "रात" - }, - week: { - dow: 0, - doy: 6 - } - }); + 1: "१", + 2: "२", + 3: "३", + 4: "४", + 5: "५", + 6: "६", + 7: "७", + 8: "८", + 9: "९", + 0: "०" + }, + c = { + "१": "1", + "२": "2", + "३": "3", + "४": "4", + "५": "5", + "६": "6", + "७": "7", + "८": "8", + "९": "9", + "०": "0" + }, + d = (b.defineLocale || b.lang).call(b, "hi", { + months: "जनवरी_फ़रवरी_मार्च_अप्रैल_मई_जून_जुलाई_अगस्त_सितम्बर_अक्टूबर_नवम्बर_दिसम्बर".split("_"), + monthsShort: "जन._फ़र._मार्च_अप्रै._मई_जून_जुल._अग._सित._अक्टू._नव._दिस.".split("_"), + monthsParseExact: !0, + weekdays: "रविवार_सोमवार_मंगलवार_बुधवार_गुरूवार_शुक्रवार_शनिवार".split("_"), + weekdaysShort: "रवि_सोम_मंगल_बुध_गुरू_शुक्र_शनि".split("_"), + weekdaysMin: "र_सो_मं_बु_गु_शु_श".split("_"), + longDateFormat: { + LT: "A h:mm बजे", + LTS: "A h:mm:ss बजे", + L: "DD/MM/YYYY", + LL: "D MMMM YYYY", + LLL: "D MMMM YYYY, A h:mm बजे", + LLLL: "dddd, D MMMM YYYY, A h:mm बजे" + }, + calendar: { + sameDay: "[आज] LT", + nextDay: "[कल] LT", + nextWeek: "dddd, LT", + lastDay: "[कल] LT", + lastWeek: "[पिछले] dddd, LT", + sameElse: "L" + }, + relativeTime: { + future: "%s में", + past: "%s पहले", + s: "कुछ ही क्षण", + m: "एक मिनट", + mm: "%d मिनट", + h: "एक घंटा", + hh: "%d घंटे", + d: "एक दिन", + dd: "%d दिन", + M: "एक महीने", + MM: "%d महीने", + y: "एक वर्ष", + yy: "%d वर्ष" + }, + preparse: function(a) { + return a.replace(/[१२३४५६७८९०]/g, function(a) { + return c[a] + }) + }, + postformat: function(b) { + return b.replace(/\d/g, function(b) { + return a[b] + }) + }, + meridiemParse: /रात|सुबह|दोपहर|शाम/, + meridiemHour: function(a, b) { + return 12 === a && (a = 0), "रात" === b ? 4 > a ? a : a + 12 : "सुबह" === b ? a : "दोपहर" === b ? a >= 10 ? a : a + 12 : "शाम" === b ? a + 12 : void 0 + }, + meridiem: function(a, b, c) { + return 4 > a ? "रात" : 10 > a ? "सुबह" : 17 > a ? "दोपहर" : 20 > a ? "शाम" : "रात" + }, + week: { + dow: 0, + doy: 6 + } + }); return d }(), a.fullCalendar.datepickerLang("hi", "hi", { closeText: "बंद", @@ -2179,19 +2191,19 @@ var d = a + " "; switch (c) { case "m": - return b ? "jedna minuta" : "jedne minute"; + return b ? "jedna minuta" : "jedne minute"; case "mm": - return d += 1 === a ? "minuta" : 2 === a || 3 === a || 4 === a ? "minute" : "minuta"; + return d += 1 === a ? "minuta" : 2 === a || 3 === a || 4 === a ? "minute" : "minuta"; case "h": - return b ? "jedan sat" : "jednog sata"; + return b ? "jedan sat" : "jednog sata"; case "hh": - return d += 1 === a ? "sat" : 2 === a || 3 === a || 4 === a ? "sata" : "sati"; + return d += 1 === a ? "sat" : 2 === a || 3 === a || 4 === a ? "sata" : "sati"; case "dd": - return d += 1 === a ? "dan" : "dana"; + return d += 1 === a ? "dan" : "dana"; case "MM": - return d += 1 === a ? "mjesec" : 2 === a || 3 === a || 4 === a ? "mjeseca" : "mjeseci"; + return d += 1 === a ? "mjesec" : 2 === a || 3 === a || 4 === a ? "mjeseca" : "mjeseci"; case "yy": - return d += 1 === a ? "godina" : 2 === a || 3 === a || 4 === a ? "godine" : "godina" + return d += 1 === a ? "godina" : 2 === a || 3 === a || 4 === a ? "godine" : "godina" } } var c = (b.defineLocale || b.lang).call(b, "hr", { @@ -2200,9 +2212,11 @@ standalone: "siječanj_veljača_ožujak_travanj_svibanj_lipanj_srpanj_kolovoz_rujan_listopad_studeni_prosinac".split("_") }, monthsShort: "sij._velj._ožu._tra._svi._lip._srp._kol._ruj._lis._stu._pro.".split("_"), + monthsParseExact: !0, weekdays: "nedjelja_ponedjeljak_utorak_srijeda_četvrtak_petak_subota".split("_"), weekdaysShort: "ned._pon._uto._sri._čet._pet._sub.".split("_"), weekdaysMin: "ne_po_ut_sr_če_pe_su".split("_"), + weekdaysParseExact: !0, longDateFormat: { LT: "H:mm", LTS: "H:mm:ss", @@ -2217,16 +2231,16 @@ nextWeek: function() { switch (this.day()) { case 0: - return "[u] [nedjelju] [u] LT"; + return "[u] [nedjelju] [u] LT"; case 3: - return "[u] [srijedu] [u] LT"; + return "[u] [srijedu] [u] LT"; case 6: - return "[u] [subotu] [u] LT"; + return "[u] [subotu] [u] LT"; case 1: case 2: case 4: case 5: - return "[u] dddd [u] LT" + return "[u] dddd [u] LT" } }, lastDay: "[jučer u] LT", @@ -2234,14 +2248,14 @@ switch (this.day()) { case 0: case 3: - return "[prošlu] dddd [u] LT"; + return "[prošlu] dddd [u] LT"; case 6: - return "[prošle] [subote] [u] LT"; + return "[prošle] [subote] [u] LT"; case 1: case 2: case 4: case 5: - return "[prošli] dddd [u] LT" + return "[prošli] dddd [u] LT" } }, sameElse: "L" @@ -2306,27 +2320,27 @@ var e = a; switch (c) { case "s": - return d || b ? "néhány másodperc" : "néhány másodperce"; + return d || b ? "néhány másodperc" : "néhány másodperce"; case "m": - return "egy" + (d || b ? " perc" : " perce"); + return "egy" + (d || b ? " perc" : " perce"); case "mm": - return e + (d || b ? " perc" : " perce"); + return e + (d || b ? " perc" : " perce"); case "h": - return "egy" + (d || b ? " óra" : " órája"); + return "egy" + (d || b ? " óra" : " órája"); case "hh": - return e + (d || b ? " óra" : " órája"); + return e + (d || b ? " óra" : " órája"); case "d": - return "egy" + (d || b ? " nap" : " napja"); + return "egy" + (d || b ? " nap" : " napja"); case "dd": - return e + (d || b ? " nap" : " napja"); + return e + (d || b ? " nap" : " napja"); case "M": - return "egy" + (d || b ? " hónap" : " hónapja"); + return "egy" + (d || b ? " hónap" : " hónapja"); case "MM": - return e + (d || b ? " hónap" : " hónapja"); + return e + (d || b ? " hónap" : " hónapja"); case "y": - return "egy" + (d || b ? " év" : " éve"); + return "egy" + (d || b ? " év" : " éve"); case "yy": - return e + (d || b ? " év" : " éve") + return e + (d || b ? " év" : " éve") } return "" } @@ -2335,61 +2349,61 @@ return (a ? "" : "[múlt] ") + "[" + d[this.day()] + "] LT[-kor]" } var d = "vasárnap hétfőn kedden szerdán csütörtökön pénteken szombaton".split(" "), - e = (b.defineLocale || b.lang).call(b, "hu", { - months: "január_február_március_április_május_június_július_augusztus_szeptember_október_november_december".split("_"), - monthsShort: "jan_feb_márc_ápr_máj_jún_júl_aug_szept_okt_nov_dec".split("_"), - weekdays: "vasárnap_hétfő_kedd_szerda_csütörtök_péntek_szombat".split("_"), - weekdaysShort: "vas_hét_kedd_sze_csüt_pén_szo".split("_"), - weekdaysMin: "v_h_k_sze_cs_p_szo".split("_"), - longDateFormat: { - LT: "H:mm", - LTS: "H:mm:ss", - L: "YYYY.MM.DD.", - LL: "YYYY. MMMM D.", - LLL: "YYYY. MMMM D. H:mm", - LLLL: "YYYY. MMMM D., dddd H:mm" - }, - meridiemParse: /de|du/i, - isPM: function(a) { - return "u" === a.charAt(1).toLowerCase() - }, - meridiem: function(a, b, c) { - return 12 > a ? c === !0 ? "de" : "DE" : c === !0 ? "du" : "DU" - }, - calendar: { - sameDay: "[ma] LT[-kor]", - nextDay: "[holnap] LT[-kor]", - nextWeek: function() { - return c.call(this, !0) - }, - lastDay: "[tegnap] LT[-kor]", - lastWeek: function() { - return c.call(this, !1) - }, - sameElse: "L" + e = (b.defineLocale || b.lang).call(b, "hu", { + months: "január_február_március_április_május_június_július_augusztus_szeptember_október_november_december".split("_"), + monthsShort: "jan_feb_márc_ápr_máj_jún_júl_aug_szept_okt_nov_dec".split("_"), + weekdays: "vasárnap_hétfő_kedd_szerda_csütörtök_péntek_szombat".split("_"), + weekdaysShort: "vas_hét_kedd_sze_csüt_pén_szo".split("_"), + weekdaysMin: "v_h_k_sze_cs_p_szo".split("_"), + longDateFormat: { + LT: "H:mm", + LTS: "H:mm:ss", + L: "YYYY.MM.DD.", + LL: "YYYY. MMMM D.", + LLL: "YYYY. MMMM D. H:mm", + LLLL: "YYYY. MMMM D., dddd H:mm" + }, + meridiemParse: /de|du/i, + isPM: function(a) { + return "u" === a.charAt(1).toLowerCase() + }, + meridiem: function(a, b, c) { + return 12 > a ? c === !0 ? "de" : "DE" : c === !0 ? "du" : "DU" + }, + calendar: { + sameDay: "[ma] LT[-kor]", + nextDay: "[holnap] LT[-kor]", + nextWeek: function() { + return c.call(this, !0) }, - relativeTime: { - future: "%s múlva", - past: "%s", - s: a, - m: a, - mm: a, - h: a, - hh: a, - d: a, - dd: a, - M: a, - MM: a, - y: a, - yy: a + lastDay: "[tegnap] LT[-kor]", + lastWeek: function() { + return c.call(this, !1) }, - ordinalParse: /\d{1,2}\./, - ordinal: "%d.", - week: { - dow: 1, - doy: 7 - } - }); + sameElse: "L" + }, + relativeTime: { + future: "%s múlva", + past: "%s", + s: a, + m: a, + mm: a, + h: a, + hh: a, + d: a, + dd: a, + M: a, + MM: a, + y: a, + yy: a + }, + ordinalParse: /\d{1,2}\./, + ordinal: "%d.", + week: { + dow: 1, + doy: 7 + } + }); return e }(), a.fullCalendar.datepickerLang("hu", "hu", { closeText: "bezár", @@ -2503,32 +2517,32 @@ "use strict"; function a(a) { - return a % 100 === 11 ? !0 : a % 10 === 1 ? !1 : !0 + return a % 100 === 11 ? !0 : a % 10 !== 1 } function c(b, c, d, e) { var f = b + " "; switch (d) { case "s": - return c || e ? "nokkrar sekúndur" : "nokkrum sekúndum"; + return c || e ? "nokkrar sekúndur" : "nokkrum sekúndum"; case "m": - return c ? "mínúta" : "mínútu"; + return c ? "mínúta" : "mínútu"; case "mm": - return a(b) ? f + (c || e ? "mínútur" : "mínútum") : c ? f + "mínúta" : f + "mínútu"; + return a(b) ? f + (c || e ? "mínútur" : "mínútum") : c ? f + "mínúta" : f + "mínútu"; case "hh": - return a(b) ? f + (c || e ? "klukkustundir" : "klukkustundum") : f + "klukkustund"; + return a(b) ? f + (c || e ? "klukkustundir" : "klukkustundum") : f + "klukkustund"; case "d": - return c ? "dagur" : e ? "dag" : "degi"; + return c ? "dagur" : e ? "dag" : "degi"; case "dd": - return a(b) ? c ? f + "dagar" : f + (e ? "daga" : "dögum") : c ? f + "dagur" : f + (e ? "dag" : "degi"); + return a(b) ? c ? f + "dagar" : f + (e ? "daga" : "dögum") : c ? f + "dagur" : f + (e ? "dag" : "degi"); case "M": - return c ? "mánuður" : e ? "mánuð" : "mánuði"; + return c ? "mánuður" : e ? "mánuð" : "mánuði"; case "MM": - return a(b) ? c ? f + "mánuðir" : f + (e ? "mánuði" : "mánuðum") : c ? f + "mánuður" : f + (e ? "mánuð" : "mánuði"); + return a(b) ? c ? f + "mánuðir" : f + (e ? "mánuði" : "mánuðum") : c ? f + "mánuður" : f + (e ? "mánuð" : "mánuði"); case "y": - return c || e ? "ár" : "ári"; + return c || e ? "ár" : "ári"; case "yy": - return a(b) ? f + (c || e ? "ár" : "árum") : f + (c || e ? "ár" : "ári") + return a(b) ? f + (c || e ? "ár" : "árum") : f + (c || e ? "ár" : "ári") } } var d = (b.defineLocale || b.lang).call(b, "is", { @@ -2540,7 +2554,7 @@ longDateFormat: { LT: "H:mm", LTS: "H:mm:ss", - L: "DD/MM/YYYY", + L: "DD.MM.YYYY", LL: "D. MMMM YYYY", LLL: "D. MMMM YYYY [kl.] H:mm", LLLL: "dddd, D. MMMM YYYY [kl.] H:mm" @@ -2628,9 +2642,9 @@ lastWeek: function() { switch (this.day()) { case 0: - return "[la scorsa] dddd [alle] LT"; + return "[la scorsa] dddd [alle] LT"; default: - return "[lo scorso] dddd [alle] LT" + return "[lo scorso] dddd [alle] LT" } }, sameElse: "L" @@ -2721,6 +2735,17 @@ lastWeek: "[前週]dddd LT", sameElse: "L" }, + ordinalParse: /\d{1,2}日/, + ordinal: function(a, b) { + switch (b) { + case "d": + case "D": + case "DDD": + return a + "日"; + default: + return a + } + }, relativeTime: { future: "%s後", past: "%s前", @@ -2795,17 +2820,17 @@ relativeTime: { future: "%s 후", past: "%s 전", - s: "몇초", + s: "몇 초", ss: "%d초", m: "일분", mm: "%d분", - h: "한시간", + h: "한 시간", hh: "%d시간", d: "하루", dd: "%d일", - M: "한달", + M: "한 달", MM: "%d달", - y: "일년", + y: "일 년", yy: "%d년" }, ordinalParse: /\d{1,2}일/, @@ -2871,74 +2896,75 @@ return 1 === a ? h + c(a, b, f[0], g) : b ? h + (d(a) ? e(f)[1] : e(f)[0]) : g ? h + e(f)[1] : h + (d(a) ? e(f)[1] : e(f)[2]) } var g = { - m: "minutė_minutės_minutę", - mm: "minutės_minučių_minutes", - h: "valanda_valandos_valandą", - hh: "valandos_valandų_valandas", - d: "diena_dienos_dieną", - dd: "dienos_dienų_dienas", - M: "mėnuo_mėnesio_mėnesį", - MM: "mėnesiai_mėnesių_mėnesius", - y: "metai_metų_metus", - yy: "metai_metų_metus" - }, - h = (b.defineLocale || b.lang).call(b, "lt", { - months: { - format: "sausio_vasario_kovo_balandžio_gegužės_birželio_liepos_rugpjūčio_rugsėjo_spalio_lapkričio_gruodžio".split("_"), - standalone: "sausis_vasaris_kovas_balandis_gegužė_birželis_liepa_rugpjūtis_rugsėjis_spalis_lapkritis_gruodis".split("_") - }, - monthsShort: "sau_vas_kov_bal_geg_bir_lie_rgp_rgs_spa_lap_grd".split("_"), - weekdays: { - format: "sekmadienį_pirmadienį_antradienį_trečiadienį_ketvirtadienį_penktadienį_šeštadienį".split("_"), - standalone: "sekmadienis_pirmadienis_antradienis_trečiadienis_ketvirtadienis_penktadienis_šeštadienis".split("_"), - isFormat: /dddd HH:mm/ - }, - weekdaysShort: "Sek_Pir_Ant_Tre_Ket_Pen_Šeš".split("_"), - weekdaysMin: "S_P_A_T_K_Pn_Š".split("_"), - longDateFormat: { - LT: "HH:mm", - LTS: "HH:mm:ss", - L: "YYYY-MM-DD", - LL: "YYYY [m.] MMMM D [d.]", - LLL: "YYYY [m.] MMMM D [d.], HH:mm [val.]", - LLLL: "YYYY [m.] MMMM D [d.], dddd, HH:mm [val.]", - l: "YYYY-MM-DD", - ll: "YYYY [m.] MMMM D [d.]", - lll: "YYYY [m.] MMMM D [d.], HH:mm [val.]", - llll: "YYYY [m.] MMMM D [d.], ddd, HH:mm [val.]" - }, - calendar: { - sameDay: "[Šiandien] LT", - nextDay: "[Rytoj] LT", - nextWeek: "dddd LT", - lastDay: "[Vakar] LT", - lastWeek: "[Praėjusį] dddd LT", - sameElse: "L" - }, - relativeTime: { - future: "po %s", - past: "prieš %s", - s: a, - m: c, - mm: f, - h: c, - hh: f, - d: c, - dd: f, - M: c, - MM: f, - y: c, - yy: f - }, - ordinalParse: /\d{1,2}-oji/, - ordinal: function(a) { - return a + "-oji" - }, - week: { - dow: 1, - doy: 4 - } - }); + m: "minutė_minutės_minutę", + mm: "minutės_minučių_minutes", + h: "valanda_valandos_valandą", + hh: "valandos_valandų_valandas", + d: "diena_dienos_dieną", + dd: "dienos_dienų_dienas", + M: "mėnuo_mėnesio_mėnesį", + MM: "mėnesiai_mėnesių_mėnesius", + y: "metai_metų_metus", + yy: "metai_metų_metus" + }, + h = (b.defineLocale || b.lang).call(b, "lt", { + months: { + format: "sausio_vasario_kovo_balandžio_gegužės_birželio_liepos_rugpjūčio_rugsėjo_spalio_lapkričio_gruodžio".split("_"), + standalone: "sausis_vasaris_kovas_balandis_gegužė_birželis_liepa_rugpjūtis_rugsėjis_spalis_lapkritis_gruodis".split("_") + }, + monthsShort: "sau_vas_kov_bal_geg_bir_lie_rgp_rgs_spa_lap_grd".split("_"), + weekdays: { + format: "sekmadienį_pirmadienį_antradienį_trečiadienį_ketvirtadienį_penktadienį_šeštadienį".split("_"), + standalone: "sekmadienis_pirmadienis_antradienis_trečiadienis_ketvirtadienis_penktadienis_šeštadienis".split("_"), + isFormat: /dddd HH:mm/ + }, + weekdaysShort: "Sek_Pir_Ant_Tre_Ket_Pen_Šeš".split("_"), + weekdaysMin: "S_P_A_T_K_Pn_Š".split("_"), + weekdaysParseExact: !0, + longDateFormat: { + LT: "HH:mm", + LTS: "HH:mm:ss", + L: "YYYY-MM-DD", + LL: "YYYY [m.] MMMM D [d.]", + LLL: "YYYY [m.] MMMM D [d.], HH:mm [val.]", + LLLL: "YYYY [m.] MMMM D [d.], dddd, HH:mm [val.]", + l: "YYYY-MM-DD", + ll: "YYYY [m.] MMMM D [d.]", + lll: "YYYY [m.] MMMM D [d.], HH:mm [val.]", + llll: "YYYY [m.] MMMM D [d.], ddd, HH:mm [val.]" + }, + calendar: { + sameDay: "[Šiandien] LT", + nextDay: "[Rytoj] LT", + nextWeek: "dddd LT", + lastDay: "[Vakar] LT", + lastWeek: "[Praėjusį] dddd LT", + sameElse: "L" + }, + relativeTime: { + future: "po %s", + past: "prieš %s", + s: a, + m: c, + mm: f, + h: c, + hh: f, + d: c, + dd: f, + M: c, + MM: f, + y: c, + yy: f + }, + ordinalParse: /\d{1,2}-oji/, + ordinal: function(a) { + return a + "-oji" + }, + week: { + dow: 1, + doy: 4 + } + }); return h }(), a.fullCalendar.datepickerLang("lt", "lt", { closeText: "Uždaryti", @@ -2987,61 +3013,62 @@ return b ? "dažas sekundes" : "dažām sekundēm" } var f = { - m: "minūtes_minūtēm_minūte_minūtes".split("_"), - mm: "minūtes_minūtēm_minūte_minūtes".split("_"), - h: "stundas_stundām_stunda_stundas".split("_"), - hh: "stundas_stundām_stunda_stundas".split("_"), - d: "dienas_dienām_diena_dienas".split("_"), - dd: "dienas_dienām_diena_dienas".split("_"), - M: "mēneša_mēnešiem_mēnesis_mēneši".split("_"), - MM: "mēneša_mēnešiem_mēnesis_mēneši".split("_"), - y: "gada_gadiem_gads_gadi".split("_"), - yy: "gada_gadiem_gads_gadi".split("_") - }, - g = (b.defineLocale || b.lang).call(b, "lv", { - months: "janvāris_februāris_marts_aprīlis_maijs_jūnijs_jūlijs_augusts_septembris_oktobris_novembris_decembris".split("_"), - monthsShort: "jan_feb_mar_apr_mai_jūn_jūl_aug_sep_okt_nov_dec".split("_"), - weekdays: "svētdiena_pirmdiena_otrdiena_trešdiena_ceturtdiena_piektdiena_sestdiena".split("_"), - weekdaysShort: "Sv_P_O_T_C_Pk_S".split("_"), - weekdaysMin: "Sv_P_O_T_C_Pk_S".split("_"), - longDateFormat: { - LT: "HH:mm", - LTS: "HH:mm:ss", - L: "DD.MM.YYYY.", - LL: "YYYY. [gada] D. MMMM", - LLL: "YYYY. [gada] D. MMMM, HH:mm", - LLLL: "YYYY. [gada] D. MMMM, dddd, HH:mm" - }, - calendar: { - sameDay: "[Šodien pulksten] LT", - nextDay: "[Rīt pulksten] LT", - nextWeek: "dddd [pulksten] LT", - lastDay: "[Vakar pulksten] LT", - lastWeek: "[Pagājušā] dddd [pulksten] LT", - sameElse: "L" - }, - relativeTime: { - future: "pēc %s", - past: "pirms %s", - s: e, - m: d, - mm: c, - h: d, - hh: c, - d: d, - dd: c, - M: d, - MM: c, - y: d, - yy: c - }, - ordinalParse: /\d{1,2}\./, - ordinal: "%d.", - week: { - dow: 1, - doy: 4 - } - }); + m: "minūtes_minūtēm_minūte_minūtes".split("_"), + mm: "minūtes_minūtēm_minūte_minūtes".split("_"), + h: "stundas_stundām_stunda_stundas".split("_"), + hh: "stundas_stundām_stunda_stundas".split("_"), + d: "dienas_dienām_diena_dienas".split("_"), + dd: "dienas_dienām_diena_dienas".split("_"), + M: "mēneša_mēnešiem_mēnesis_mēneši".split("_"), + MM: "mēneša_mēnešiem_mēnesis_mēneši".split("_"), + y: "gada_gadiem_gads_gadi".split("_"), + yy: "gada_gadiem_gads_gadi".split("_") + }, + g = (b.defineLocale || b.lang).call(b, "lv", { + months: "janvāris_februāris_marts_aprīlis_maijs_jūnijs_jūlijs_augusts_septembris_oktobris_novembris_decembris".split("_"), + monthsShort: "jan_feb_mar_apr_mai_jūn_jūl_aug_sep_okt_nov_dec".split("_"), + weekdays: "svētdiena_pirmdiena_otrdiena_trešdiena_ceturtdiena_piektdiena_sestdiena".split("_"), + weekdaysShort: "Sv_P_O_T_C_Pk_S".split("_"), + weekdaysMin: "Sv_P_O_T_C_Pk_S".split("_"), + weekdaysParseExact: !0, + longDateFormat: { + LT: "HH:mm", + LTS: "HH:mm:ss", + L: "DD.MM.YYYY.", + LL: "YYYY. [gada] D. MMMM", + LLL: "YYYY. [gada] D. MMMM, HH:mm", + LLLL: "YYYY. [gada] D. MMMM, dddd, HH:mm" + }, + calendar: { + sameDay: "[Šodien pulksten] LT", + nextDay: "[Rīt pulksten] LT", + nextWeek: "dddd [pulksten] LT", + lastDay: "[Vakar pulksten] LT", + lastWeek: "[Pagājušā] dddd [pulksten] LT", + sameElse: "L" + }, + relativeTime: { + future: "pēc %s", + past: "pirms %s", + s: e, + m: d, + mm: c, + h: d, + hh: c, + d: d, + dd: c, + M: d, + MM: c, + y: d, + yy: c + }, + ordinalParse: /\d{1,2}\./, + ordinal: "%d.", + week: { + dow: 1, + doy: 4 + } + }); return g }(), a.fullCalendar.datepickerLang("lv", "lv", { closeText: "Aizvērt", @@ -3078,9 +3105,11 @@ var a = (b.defineLocale || b.lang).call(b, "nb", { months: "januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember".split("_"), monthsShort: "jan._feb._mars_april_mai_juni_juli_aug._sep._okt._nov._des.".split("_"), + monthsParseExact: !0, weekdays: "søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag".split("_"), weekdaysShort: "sø._ma._ti._on._to._fr._lø.".split("_"), weekdaysMin: "sø_ma_ti_on_to_fr_lø".split("_"), + weekdaysParseExact: !0, longDateFormat: { LT: "HH:mm", LTS: "HH:mm:ss", @@ -3099,7 +3128,7 @@ }, relativeTime: { future: "om %s", - past: "for %s siden", + past: "%s siden", s: "noen sekunder", m: "ett minutt", mm: "%d minutter", @@ -3151,55 +3180,57 @@ ! function() { "use strict"; var a = "jan._feb._mrt._apr._mei_jun._jul._aug._sep._okt._nov._dec.".split("_"), - c = "jan_feb_mrt_apr_mei_jun_jul_aug_sep_okt_nov_dec".split("_"), - d = (b.defineLocale || b.lang).call(b, "nl", { - months: "januari_februari_maart_april_mei_juni_juli_augustus_september_oktober_november_december".split("_"), - monthsShort: function(b, d) { - return /-MMM-/.test(d) ? c[b.month()] : a[b.month()] - }, - weekdays: "zondag_maandag_dinsdag_woensdag_donderdag_vrijdag_zaterdag".split("_"), - weekdaysShort: "zo._ma._di._wo._do._vr._za.".split("_"), - weekdaysMin: "Zo_Ma_Di_Wo_Do_Vr_Za".split("_"), - longDateFormat: { - LT: "HH:mm", - LTS: "HH:mm:ss", - L: "DD-MM-YYYY", - LL: "D MMMM YYYY", - LLL: "D MMMM YYYY HH:mm", - LLLL: "dddd D MMMM YYYY HH:mm" - }, - calendar: { - sameDay: "[vandaag om] LT", - nextDay: "[morgen om] LT", - nextWeek: "dddd [om] LT", - lastDay: "[gisteren om] LT", - lastWeek: "[afgelopen] dddd [om] LT", - sameElse: "L" - }, - relativeTime: { - future: "over %s", - past: "%s geleden", - s: "een paar seconden", - m: "één minuut", - mm: "%d minuten", - h: "één uur", - hh: "%d uur", - d: "één dag", - dd: "%d dagen", - M: "één maand", - MM: "%d maanden", - y: "één jaar", - yy: "%d jaar" - }, - ordinalParse: /\d{1,2}(ste|de)/, - ordinal: function(a) { - return a + (1 === a || 8 === a || a >= 20 ? "ste" : "de") - }, - week: { - dow: 1, - doy: 4 - } - }); + c = "jan_feb_mrt_apr_mei_jun_jul_aug_sep_okt_nov_dec".split("_"), + d = (b.defineLocale || b.lang).call(b, "nl", { + months: "januari_februari_maart_april_mei_juni_juli_augustus_september_oktober_november_december".split("_"), + monthsShort: function(b, d) { + return /-MMM-/.test(d) ? c[b.month()] : a[b.month()] + }, + monthsParseExact: !0, + weekdays: "zondag_maandag_dinsdag_woensdag_donderdag_vrijdag_zaterdag".split("_"), + weekdaysShort: "zo._ma._di._wo._do._vr._za.".split("_"), + weekdaysMin: "Zo_Ma_Di_Wo_Do_Vr_Za".split("_"), + weekdaysParseExact: !0, + longDateFormat: { + LT: "HH:mm", + LTS: "HH:mm:ss", + L: "DD-MM-YYYY", + LL: "D MMMM YYYY", + LLL: "D MMMM YYYY HH:mm", + LLLL: "dddd D MMMM YYYY HH:mm" + }, + calendar: { + sameDay: "[vandaag om] LT", + nextDay: "[morgen om] LT", + nextWeek: "dddd [om] LT", + lastDay: "[gisteren om] LT", + lastWeek: "[afgelopen] dddd [om] LT", + sameElse: "L" + }, + relativeTime: { + future: "over %s", + past: "%s geleden", + s: "een paar seconden", + m: "één minuut", + mm: "%d minuten", + h: "één uur", + hh: "%d uur", + d: "één dag", + dd: "%d dagen", + M: "één maand", + MM: "%d maanden", + y: "één jaar", + yy: "%d jaar" + }, + ordinalParse: /\d{1,2}(ste|de)/, + ordinal: function(a) { + return a + (1 === a || 8 === a || a >= 20 ? "ste" : "de") + }, + week: { + dow: 1, + doy: 4 + } + }); return d }(), a.fullCalendar.datepickerLang("nl", "nl", { closeText: "Sluiten", @@ -3240,78 +3271,78 @@ var e = b + " "; switch (d) { case "m": - return c ? "minuta" : "minutę"; + return c ? "minuta" : "minutę"; case "mm": - return e + (a(b) ? "minuty" : "minut"); + return e + (a(b) ? "minuty" : "minut"); case "h": - return c ? "godzina" : "godzinę"; + return c ? "godzina" : "godzinę"; case "hh": - return e + (a(b) ? "godziny" : "godzin"); + return e + (a(b) ? "godziny" : "godzin"); case "MM": - return e + (a(b) ? "miesiące" : "miesięcy"); + return e + (a(b) ? "miesiące" : "miesięcy"); case "yy": - return e + (a(b) ? "lata" : "lat") + return e + (a(b) ? "lata" : "lat") } } var d = "styczeń_luty_marzec_kwiecień_maj_czerwiec_lipiec_sierpień_wrzesień_październik_listopad_grudzień".split("_"), - e = "stycznia_lutego_marca_kwietnia_maja_czerwca_lipca_sierpnia_września_października_listopada_grudnia".split("_"), - f = (b.defineLocale || b.lang).call(b, "pl", { - months: function(a, b) { - return "" === b ? "(" + e[a.month()] + "|" + d[a.month()] + ")" : /D MMMM/.test(b) ? e[a.month()] : d[a.month()] - }, - monthsShort: "sty_lut_mar_kwi_maj_cze_lip_sie_wrz_paź_lis_gru".split("_"), - weekdays: "niedziela_poniedziałek_wtorek_środa_czwartek_piątek_sobota".split("_"), - weekdaysShort: "nie_pon_wt_śr_czw_pt_sb".split("_"), - weekdaysMin: "Nd_Pn_Wt_Śr_Cz_Pt_So".split("_"), - longDateFormat: { - LT: "HH:mm", - LTS: "HH:mm:ss", - L: "DD.MM.YYYY", - LL: "D MMMM YYYY", - LLL: "D MMMM YYYY HH:mm", - LLLL: "dddd, D MMMM YYYY HH:mm" - }, - calendar: { - sameDay: "[Dziś o] LT", - nextDay: "[Jutro o] LT", - nextWeek: "[W] dddd [o] LT", - lastDay: "[Wczoraj o] LT", - lastWeek: function() { - switch (this.day()) { - case 0: - return "[W zeszłą niedzielę o] LT"; - case 3: - return "[W zeszłą środę o] LT"; - case 6: - return "[W zeszłą sobotę o] LT"; - default: - return "[W zeszły] dddd [o] LT" - } - }, - sameElse: "L" - }, - relativeTime: { - future: "za %s", - past: "%s temu", - s: "kilka sekund", - m: c, - mm: c, - h: c, - hh: c, - d: "1 dzień", - dd: "%d dni", - M: "miesiąc", - MM: c, - y: "rok", - yy: c + e = "stycznia_lutego_marca_kwietnia_maja_czerwca_lipca_sierpnia_września_października_listopada_grudnia".split("_"), + f = (b.defineLocale || b.lang).call(b, "pl", { + months: function(a, b) { + return "" === b ? "(" + e[a.month()] + "|" + d[a.month()] + ")" : /D MMMM/.test(b) ? e[a.month()] : d[a.month()] + }, + monthsShort: "sty_lut_mar_kwi_maj_cze_lip_sie_wrz_paź_lis_gru".split("_"), + weekdays: "niedziela_poniedziałek_wtorek_środa_czwartek_piątek_sobota".split("_"), + weekdaysShort: "nie_pon_wt_śr_czw_pt_sb".split("_"), + weekdaysMin: "Nd_Pn_Wt_Śr_Cz_Pt_So".split("_"), + longDateFormat: { + LT: "HH:mm", + LTS: "HH:mm:ss", + L: "DD.MM.YYYY", + LL: "D MMMM YYYY", + LLL: "D MMMM YYYY HH:mm", + LLLL: "dddd, D MMMM YYYY HH:mm" + }, + calendar: { + sameDay: "[Dziś o] LT", + nextDay: "[Jutro o] LT", + nextWeek: "[W] dddd [o] LT", + lastDay: "[Wczoraj o] LT", + lastWeek: function() { + switch (this.day()) { + case 0: + return "[W zeszłą niedzielę o] LT"; + case 3: + return "[W zeszłą środę o] LT"; + case 6: + return "[W zeszłą sobotę o] LT"; + default: + return "[W zeszły] dddd [o] LT" + } }, - ordinalParse: /\d{1,2}\./, - ordinal: "%d.", - week: { - dow: 1, - doy: 4 - } - }); + sameElse: "L" + }, + relativeTime: { + future: "za %s", + past: "%s temu", + s: "kilka sekund", + m: c, + mm: c, + h: c, + hh: c, + d: "1 dzień", + dd: "%d dni", + M: "miesiąc", + MM: c, + y: "rok", + yy: c + }, + ordinalParse: /\d{1,2}\./, + ordinal: "%d.", + week: { + dow: 1, + doy: 4 + } + }); return f }(), a.fullCalendar.datepickerLang("pl", "pl", { closeText: "Zamknij", @@ -3346,9 +3377,10 @@ var a = (b.defineLocale || b.lang).call(b, "pt-br", { months: "Janeiro_Fevereiro_Março_Abril_Maio_Junho_Julho_Agosto_Setembro_Outubro_Novembro_Dezembro".split("_"), monthsShort: "Jan_Fev_Mar_Abr_Mai_Jun_Jul_Ago_Set_Out_Nov_Dez".split("_"), - weekdays: "Domingo_Segunda-Feira_Terça-Feira_Quarta-Feira_Quinta-Feira_Sexta-Feira_Sábado".split("_"), + weekdays: "Domingo_Segunda-feira_Terça-feira_Quarta-feira_Quinta-feira_Sexta-feira_Sábado".split("_"), weekdaysShort: "Dom_Seg_Ter_Qua_Qui_Sex_Sáb".split("_"), weekdaysMin: "Dom_2ª_3ª_4ª_5ª_6ª_Sáb".split("_"), + weekdaysParseExact: !0, longDateFormat: { LT: "HH:mm", LTS: "HH:mm:ss", @@ -3424,6 +3456,7 @@ weekdays: "Domingo_Segunda-Feira_Terça-Feira_Quarta-Feira_Quinta-Feira_Sexta-Feira_Sábado".split("_"), weekdaysShort: "Dom_Seg_Ter_Qua_Qui_Sex_Sáb".split("_"), weekdaysMin: "Dom_2ª_3ª_4ª_5ª_6ª_Sáb".split("_"), + weekdaysParseExact: !0, longDateFormat: { LT: "HH:mm", LTS: "HH:mm:ss", @@ -3498,18 +3531,19 @@ function a(a, b, c) { var d = { - mm: "minute", - hh: "ore", - dd: "zile", - MM: "luni", - yy: "ani" - }, - e = " "; + mm: "minute", + hh: "ore", + dd: "zile", + MM: "luni", + yy: "ani" + }, + e = " "; return (a % 100 >= 20 || a >= 100 && a % 100 === 0) && (e = " de "), a + e + d[c] } var c = (b.defineLocale || b.lang).call(b, "ro", { months: "ianuarie_februarie_martie_aprilie_mai_iunie_iulie_august_septembrie_octombrie_noiembrie_decembrie".split("_"), monthsShort: "ian._febr._mart._apr._mai_iun._iul._aug._sept._oct._nov._dec.".split("_"), + monthsParseExact: !0, weekdays: "duminică_luni_marți_miercuri_joi_vineri_sâmbătă".split("_"), weekdaysShort: "Dum_Lun_Mar_Mie_Joi_Vin_Sâm".split("_"), weekdaysMin: "Du_Lu_Ma_Mi_Jo_Vi_Sâ".split("_"), @@ -3600,113 +3634,117 @@ }; return "m" === d ? c ? "минута" : "минуту" : b + " " + a(e[d], +b) } - var d = [/^янв/i, /^фев/i, /^мар/i, /^апр/i, /^ма[й|я]/i, /^июн/i, /^июл/i, /^авг/i, /^сен/i, /^окт/i, /^ноя/i, /^дек/i], - e = (b.defineLocale || b.lang).call(b, "ru", { - months: { - format: "Января_Февраля_Марта_Апреля_Мая_Июня_Июля_Августа_Сентября_Октября_Ноября_Декабря".split("_"), - standalone: "Январь_Февраль_Март_Апрель_Май_Июнь_Июль_Август_Сентябрь_Октябрь_Ноябрь_Декабрь".split("_") - }, - monthsShort: { - format: "янв_фев_мар_апр_мая_июня_июля_авг_сен_окт_ноя_дек".split("_"), - standalone: "янв_фев_март_апр_май_июнь_июль_авг_сен_окт_ноя_дек".split("_") - }, - weekdays: { - standalone: "Воскресенье_Понедельник_Вторник_Среда_Четверг_Пятница_Суббота".split("_"), - format: "Воскресенье_Понедельник_Вторник_Среду_Четверг_Пятницу_Субботу".split("_"), - isFormat: /\[ ?[Вв] ?(?:прошлую|следующую|эту)? ?\] ?dddd/ - }, - weekdaysShort: "Вс_Пн_Вт_Ср_Чт_Пт_Сб".split("_"), - weekdaysMin: "Вс_Пн_Вт_Ср_Чт_Пт_Сб".split("_"), - monthsParse: d, - longMonthsParse: d, - shortMonthsParse: d, - longDateFormat: { - LT: "HH:mm", - LTS: "HH:mm:ss", - L: "DD.MM.YYYY", - LL: "D MMMM YYYY г.", - LLL: "D MMMM YYYY г., HH:mm", - LLLL: "dddd, D MMMM YYYY г., HH:mm" - }, - calendar: { - sameDay: "[Сегодня в] LT", - nextDay: "[Завтра в] LT", - lastDay: "[Вчера в] LT", - nextWeek: function(a) { - if (a.week() === this.week()) return 2 === this.day() ? "[Во] dddd [в] LT" : "[В] dddd [в] LT"; - switch (this.day()) { - case 0: - return "[В следующее] dddd [в] LT"; - case 1: - case 2: - case 4: - return "[В следующий] dddd [в] LT"; - case 3: - case 5: - case 6: - return "[В следующую] dddd [в] LT" - } - }, - lastWeek: function(a) { - if (a.week() === this.week()) return 2 === this.day() ? "[Во] dddd [в] LT" : "[В] dddd [в] LT"; - switch (this.day()) { - case 0: - return "[В прошлое] dddd [в] LT"; - case 1: - case 2: - case 4: - return "[В прошлый] dddd [в] LT"; - case 3: - case 5: - case 6: - return "[В прошлую] dddd [в] LT" - } - }, - sameElse: "L" - }, - relativeTime: { - future: "через %s", - past: "%s назад", - s: "несколько секунд", - m: c, - mm: c, - h: "час", - hh: c, - d: "день", - dd: c, - M: "месяц", - MM: c, - y: "год", - yy: c - }, - meridiemParse: /ночи|утра|дня|вечера/i, - isPM: function(a) { - return /^(дня|вечера)$/.test(a) - }, - meridiem: function(a, b, c) { - return 4 > a ? "ночи" : 12 > a ? "утра" : 17 > a ? "дня" : "вечера" + var d = [/^янв/i, /^фев/i, /^мар/i, /^апр/i, /^ма[йя]/i, /^июн/i, /^июл/i, /^авг/i, /^сен/i, /^окт/i, /^ноя/i, /^дек/i], + e = (b.defineLocale || b.lang).call(b, "ru", { + months: { + format: "января_февраля_марта_апреля_мая_июня_июля_августа_сентября_октября_ноября_декабря".split("_"), + standalone: "январь_февраль_март_апрель_май_июнь_июль_август_сентябрь_октябрь_ноябрь_декабрь".split("_") + }, + monthsShort: { + format: "янв._февр._мар._апр._мая_июня_июля_авг._сент._окт._нояб._дек.".split("_"), + standalone: "янв._февр._март_апр._май_июнь_июль_авг._сент._окт._нояб._дек.".split("_") + }, + weekdays: { + standalone: "воскресенье_понедельник_вторник_среда_четверг_пятница_суббота".split("_"), + format: "воскресенье_понедельник_вторник_среду_четверг_пятницу_субботу".split("_"), + isFormat: /\[ ?[Вв] ?(?:прошлую|следующую|эту)? ?\] ?dddd/ + }, + weekdaysShort: "вс_пн_вт_ср_чт_пт_сб".split("_"), + weekdaysMin: "вс_пн_вт_ср_чт_пт_сб".split("_"), + monthsParse: d, + longMonthsParse: d, + shortMonthsParse: d, + monthsRegex: /^(сентябр[яь]|октябр[яь]|декабр[яь]|феврал[яь]|январ[яь]|апрел[яь]|августа?|ноябр[яь]|сент\.|февр\.|нояб\.|июнь|янв.|июль|дек.|авг.|апр.|марта|мар[.т]|окт.|июн[яь]|июл[яь]|ма[яй])/i, + monthsShortRegex: /^(сентябр[яь]|октябр[яь]|декабр[яь]|феврал[яь]|январ[яь]|апрел[яь]|августа?|ноябр[яь]|сент\.|февр\.|нояб\.|июнь|янв.|июль|дек.|авг.|апр.|марта|мар[.т]|окт.|июн[яь]|июл[яь]|ма[яй])/i, + monthsStrictRegex: /^(сентябр[яь]|октябр[яь]|декабр[яь]|феврал[яь]|январ[яь]|апрел[яь]|августа?|ноябр[яь]|марта?|июн[яь]|июл[яь]|ма[яй])/i, + monthsShortStrictRegex: /^(нояб\.|февр\.|сент\.|июль|янв\.|июн[яь]|мар[.т]|авг\.|апр\.|окт\.|дек\.|ма[яй])/i, + longDateFormat: { + LT: "HH:mm", + LTS: "HH:mm:ss", + L: "DD.MM.YYYY", + LL: "D MMMM YYYY г.", + LLL: "D MMMM YYYY г., HH:mm", + LLLL: "dddd, D MMMM YYYY г., HH:mm" + }, + calendar: { + sameDay: "[Сегодня в] LT", + nextDay: "[Завтра в] LT", + lastDay: "[Вчера в] LT", + nextWeek: function(a) { + if (a.week() === this.week()) return 2 === this.day() ? "[Во] dddd [в] LT" : "[В] dddd [в] LT"; + switch (this.day()) { + case 0: + return "[В следующее] dddd [в] LT"; + case 1: + case 2: + case 4: + return "[В следующий] dddd [в] LT"; + case 3: + case 5: + case 6: + return "[В следующую] dddd [в] LT" + } }, - ordinalParse: /\d{1,2}-(й|го|я)/, - ordinal: function(a, b) { - switch (b) { - case "M": - case "d": - case "DDD": - return a + "-й"; - case "D": - return a + "-го"; - case "w": - case "W": - return a + "-я"; - default: - return a + lastWeek: function(a) { + if (a.week() === this.week()) return 2 === this.day() ? "[Во] dddd [в] LT" : "[В] dddd [в] LT"; + switch (this.day()) { + case 0: + return "[В прошлое] dddd [в] LT"; + case 1: + case 2: + case 4: + return "[В прошлый] dddd [в] LT"; + case 3: + case 5: + case 6: + return "[В прошлую] dddd [в] LT" } }, - week: { - dow: 1, - doy: 7 + sameElse: "L" + }, + relativeTime: { + future: "через %s", + past: "%s назад", + s: "несколько секунд", + m: c, + mm: c, + h: "час", + hh: c, + d: "день", + dd: c, + M: "месяц", + MM: c, + y: "год", + yy: c + }, + meridiemParse: /ночи|утра|дня|вечера/i, + isPM: function(a) { + return /^(дня|вечера)$/.test(a) + }, + meridiem: function(a, b, c) { + return 4 > a ? "ночи" : 12 > a ? "утра" : 17 > a ? "дня" : "вечера" + }, + ordinalParse: /\d{1,2}-(й|го|я)/, + ordinal: function(a, b) { + switch (b) { + case "M": + case "d": + case "DDD": + return a + "-й"; + case "D": + return a + "-го"; + case "w": + case "W": + return a + "-я"; + default: + return a } - }); + }, + week: { + dow: 1, + doy: 7 + } + }); return e }(), a.fullCalendar.datepickerLang("ru", "ru", { closeText: "Закрыть", @@ -3749,106 +3787,106 @@ var f = b + " "; switch (d) { case "s": - return c || e ? "pár sekúnd" : "pár sekundami"; + return c || e ? "pár sekúnd" : "pár sekundami"; case "m": - return c ? "minúta" : e ? "minútu" : "minútou"; + return c ? "minúta" : e ? "minútu" : "minútou"; case "mm": - return c || e ? f + (a(b) ? "minúty" : "minút") : f + "minútami"; + return c || e ? f + (a(b) ? "minúty" : "minút") : f + "minútami"; case "h": - return c ? "hodina" : e ? "hodinu" : "hodinou"; + return c ? "hodina" : e ? "hodinu" : "hodinou"; case "hh": - return c || e ? f + (a(b) ? "hodiny" : "hodín") : f + "hodinami"; + return c || e ? f + (a(b) ? "hodiny" : "hodín") : f + "hodinami"; case "d": - return c || e ? "deň" : "dňom"; + return c || e ? "deň" : "dňom"; case "dd": - return c || e ? f + (a(b) ? "dni" : "dní") : f + "dňami"; + return c || e ? f + (a(b) ? "dni" : "dní") : f + "dňami"; case "M": - return c || e ? "mesiac" : "mesiacom"; + return c || e ? "mesiac" : "mesiacom"; case "MM": - return c || e ? f + (a(b) ? "mesiace" : "mesiacov") : f + "mesiacmi"; + return c || e ? f + (a(b) ? "mesiace" : "mesiacov") : f + "mesiacmi"; case "y": - return c || e ? "rok" : "rokom"; + return c || e ? "rok" : "rokom"; case "yy": - return c || e ? f + (a(b) ? "roky" : "rokov") : f + "rokmi" + return c || e ? f + (a(b) ? "roky" : "rokov") : f + "rokmi" } } var d = "január_február_marec_apríl_máj_jún_júl_august_september_október_november_december".split("_"), - e = "jan_feb_mar_apr_máj_jún_júl_aug_sep_okt_nov_dec".split("_"), - f = (b.defineLocale || b.lang).call(b, "sk", { - months: d, - monthsShort: e, - weekdays: "nedeľa_pondelok_utorok_streda_štvrtok_piatok_sobota".split("_"), - weekdaysShort: "ne_po_ut_st_št_pi_so".split("_"), - weekdaysMin: "ne_po_ut_st_št_pi_so".split("_"), - longDateFormat: { - LT: "H:mm", - LTS: "H:mm:ss", - L: "DD.MM.YYYY", - LL: "D. MMMM YYYY", - LLL: "D. MMMM YYYY H:mm", - LLLL: "dddd D. MMMM YYYY H:mm" - }, - calendar: { - sameDay: "[dnes o] LT", - nextDay: "[zajtra o] LT", - nextWeek: function() { - switch (this.day()) { - case 0: - return "[v nedeľu o] LT"; - case 1: - case 2: - return "[v] dddd [o] LT"; - case 3: - return "[v stredu o] LT"; - case 4: - return "[vo štvrtok o] LT"; - case 5: - return "[v piatok o] LT"; - case 6: - return "[v sobotu o] LT" - } - }, - lastDay: "[včera o] LT", - lastWeek: function() { - switch (this.day()) { - case 0: - return "[minulú nedeľu o] LT"; - case 1: - case 2: - return "[minulý] dddd [o] LT"; - case 3: - return "[minulú stredu o] LT"; - case 4: - case 5: - return "[minulý] dddd [o] LT"; - case 6: - return "[minulú sobotu o] LT" - } - }, - sameElse: "L" + e = "jan_feb_mar_apr_máj_jún_júl_aug_sep_okt_nov_dec".split("_"), + f = (b.defineLocale || b.lang).call(b, "sk", { + months: d, + monthsShort: e, + weekdays: "nedeľa_pondelok_utorok_streda_štvrtok_piatok_sobota".split("_"), + weekdaysShort: "ne_po_ut_st_št_pi_so".split("_"), + weekdaysMin: "ne_po_ut_st_št_pi_so".split("_"), + longDateFormat: { + LT: "H:mm", + LTS: "H:mm:ss", + L: "DD.MM.YYYY", + LL: "D. MMMM YYYY", + LLL: "D. MMMM YYYY H:mm", + LLLL: "dddd D. MMMM YYYY H:mm" + }, + calendar: { + sameDay: "[dnes o] LT", + nextDay: "[zajtra o] LT", + nextWeek: function() { + switch (this.day()) { + case 0: + return "[v nedeľu o] LT"; + case 1: + case 2: + return "[v] dddd [o] LT"; + case 3: + return "[v stredu o] LT"; + case 4: + return "[vo štvrtok o] LT"; + case 5: + return "[v piatok o] LT"; + case 6: + return "[v sobotu o] LT" + } }, - relativeTime: { - future: "za %s", - past: "pred %s", - s: c, - m: c, - mm: c, - h: c, - hh: c, - d: c, - dd: c, - M: c, - MM: c, - y: c, - yy: c + lastDay: "[včera o] LT", + lastWeek: function() { + switch (this.day()) { + case 0: + return "[minulú nedeľu o] LT"; + case 1: + case 2: + return "[minulý] dddd [o] LT"; + case 3: + return "[minulú stredu o] LT"; + case 4: + case 5: + return "[minulý] dddd [o] LT"; + case 6: + return "[minulú sobotu o] LT" + } }, - ordinalParse: /\d{1,2}\./, - ordinal: "%d.", - week: { - dow: 1, - doy: 4 - } - }); + sameElse: "L" + }, + relativeTime: { + future: "za %s", + past: "pred %s", + s: c, + m: c, + mm: c, + h: c, + hh: c, + d: c, + dd: c, + M: c, + MM: c, + y: c, + yy: c + }, + ordinalParse: /\d{1,2}\./, + ordinal: "%d.", + week: { + dow: 1, + doy: 4 + } + }); return f }(), a.fullCalendar.datepickerLang("sk", "sk", { closeText: "Zavrieť", @@ -3887,35 +3925,37 @@ var e = a + " "; switch (c) { case "s": - return b || d ? "nekaj sekund" : "nekaj sekundami"; + return b || d ? "nekaj sekund" : "nekaj sekundami"; case "m": - return b ? "ena minuta" : "eno minuto"; + return b ? "ena minuta" : "eno minuto"; case "mm": - return e += 1 === a ? b ? "minuta" : "minuto" : 2 === a ? b || d ? "minuti" : "minutama" : 5 > a ? b || d ? "minute" : "minutami" : b || d ? "minut" : "minutami"; + return e += 1 === a ? b ? "minuta" : "minuto" : 2 === a ? b || d ? "minuti" : "minutama" : 5 > a ? b || d ? "minute" : "minutami" : b || d ? "minut" : "minutami"; case "h": - return b ? "ena ura" : "eno uro"; + return b ? "ena ura" : "eno uro"; case "hh": - return e += 1 === a ? b ? "ura" : "uro" : 2 === a ? b || d ? "uri" : "urama" : 5 > a ? b || d ? "ure" : "urami" : b || d ? "ur" : "urami"; + return e += 1 === a ? b ? "ura" : "uro" : 2 === a ? b || d ? "uri" : "urama" : 5 > a ? b || d ? "ure" : "urami" : b || d ? "ur" : "urami"; case "d": - return b || d ? "en dan" : "enim dnem"; + return b || d ? "en dan" : "enim dnem"; case "dd": - return e += 1 === a ? b || d ? "dan" : "dnem" : 2 === a ? b || d ? "dni" : "dnevoma" : b || d ? "dni" : "dnevi"; + return e += 1 === a ? b || d ? "dan" : "dnem" : 2 === a ? b || d ? "dni" : "dnevoma" : b || d ? "dni" : "dnevi"; case "M": - return b || d ? "en mesec" : "enim mesecem"; + return b || d ? "en mesec" : "enim mesecem"; case "MM": - return e += 1 === a ? b || d ? "mesec" : "mesecem" : 2 === a ? b || d ? "meseca" : "mesecema" : 5 > a ? b || d ? "mesece" : "meseci" : b || d ? "mesecev" : "meseci"; + return e += 1 === a ? b || d ? "mesec" : "mesecem" : 2 === a ? b || d ? "meseca" : "mesecema" : 5 > a ? b || d ? "mesece" : "meseci" : b || d ? "mesecev" : "meseci"; case "y": - return b || d ? "eno leto" : "enim letom"; + return b || d ? "eno leto" : "enim letom"; case "yy": - return e += 1 === a ? b || d ? "leto" : "letom" : 2 === a ? b || d ? "leti" : "letoma" : 5 > a ? b || d ? "leta" : "leti" : b || d ? "let" : "leti" + return e += 1 === a ? b || d ? "leto" : "letom" : 2 === a ? b || d ? "leti" : "letoma" : 5 > a ? b || d ? "leta" : "leti" : b || d ? "let" : "leti" } } var c = (b.defineLocale || b.lang).call(b, "sl", { months: "januar_februar_marec_april_maj_junij_julij_avgust_september_oktober_november_december".split("_"), monthsShort: "jan._feb._mar._apr._maj._jun._jul._avg._sep._okt._nov._dec.".split("_"), + monthsParseExact: !0, weekdays: "nedelja_ponedeljek_torek_sreda_četrtek_petek_sobota".split("_"), weekdaysShort: "ned._pon._tor._sre._čet._pet._sob.".split("_"), weekdaysMin: "ne_po_to_sr_če_pe_so".split("_"), + weekdaysParseExact: !0, longDateFormat: { LT: "H:mm", LTS: "H:mm:ss", @@ -3930,32 +3970,32 @@ nextWeek: function() { switch (this.day()) { case 0: - return "[v] [nedeljo] [ob] LT"; + return "[v] [nedeljo] [ob] LT"; case 3: - return "[v] [sredo] [ob] LT"; + return "[v] [sredo] [ob] LT"; case 6: - return "[v] [soboto] [ob] LT"; + return "[v] [soboto] [ob] LT"; case 1: case 2: case 4: case 5: - return "[v] dddd [ob] LT" + return "[v] dddd [ob] LT" } }, lastDay: "[včeraj ob] LT", lastWeek: function() { switch (this.day()) { case 0: - return "[prejšnjo] [nedeljo] [ob] LT"; + return "[prejšnjo] [nedeljo] [ob] LT"; case 3: - return "[prejšnjo] [sredo] [ob] LT"; + return "[prejšnjo] [sredo] [ob] LT"; case 6: - return "[prejšnjo] [soboto] [ob] LT"; + return "[prejšnjo] [soboto] [ob] LT"; case 1: case 2: case 4: case 5: - return "[prejšnji] dddd [ob] LT" + return "[prejšnji] dddd [ob] LT" } }, sameElse: "L" @@ -4014,84 +4054,86 @@ ! function() { "use strict"; var a = { - words: { - m: ["један минут", "једне минуте"], - mm: ["минут", "минуте", "минута"], - h: ["један сат", "једног сата"], - hh: ["сат", "сата", "сати"], - dd: ["дан", "дана", "дана"], - MM: ["месец", "месеца", "месеци"], - yy: ["година", "године", "година"] - }, - correctGrammaticalCase: function(a, b) { - return 1 === a ? b[0] : a >= 2 && 4 >= a ? b[1] : b[2] - }, - translate: function(b, c, d) { - var e = a.words[d]; - return 1 === d.length ? c ? e[0] : e[1] : b + " " + a.correctGrammaticalCase(b, e) - } + words: { + m: ["један минут", "једне минуте"], + mm: ["минут", "минуте", "минута"], + h: ["један сат", "једног сата"], + hh: ["сат", "сата", "сати"], + dd: ["дан", "дана", "дана"], + MM: ["месец", "месеца", "месеци"], + yy: ["година", "године", "година"] + }, + correctGrammaticalCase: function(a, b) { + return 1 === a ? b[0] : a >= 2 && 4 >= a ? b[1] : b[2] + }, + translate: function(b, c, d) { + var e = a.words[d]; + return 1 === d.length ? c ? e[0] : e[1] : b + " " + a.correctGrammaticalCase(b, e) + } + }, + c = (b.defineLocale || b.lang).call(b, "sr-cyrl", { + months: "јануар_фебруар_март_април_мај_јун_јул_август_септембар_октобар_новембар_децембар".split("_"), + monthsShort: "јан._феб._мар._апр._мај_јун_јул_авг._сеп._окт._нов._дец.".split("_"), + monthsParseExact: !0, + weekdays: "недеља_понедељак_уторак_среда_четвртак_петак_субота".split("_"), + weekdaysShort: "нед._пон._уто._сре._чет._пет._суб.".split("_"), + weekdaysMin: "не_по_ут_ср_че_пе_су".split("_"), + weekdaysParseExact: !0, + longDateFormat: { + LT: "H:mm", + LTS: "H:mm:ss", + L: "DD. MM. YYYY", + LL: "D. MMMM YYYY", + LLL: "D. MMMM YYYY H:mm", + LLLL: "dddd, D. MMMM YYYY H:mm" }, - c = (b.defineLocale || b.lang).call(b, "sr-cyrl", { - months: ["јануар", "фебруар", "март", "април", "мај", "јун", "јул", "август", "септембар", "октобар", "новембар", "децембар"], - monthsShort: ["јан.", "феб.", "мар.", "апр.", "мај", "јун", "јул", "авг.", "сеп.", "окт.", "нов.", "дец."], - weekdays: ["недеља", "понедељак", "уторак", "среда", "четвртак", "петак", "субота"], - weekdaysShort: ["нед.", "пон.", "уто.", "сре.", "чет.", "пет.", "суб."], - weekdaysMin: ["не", "по", "ут", "ср", "че", "пе", "су"], - longDateFormat: { - LT: "H:mm", - LTS: "H:mm:ss", - L: "DD. MM. YYYY", - LL: "D. MMMM YYYY", - LLL: "D. MMMM YYYY H:mm", - LLLL: "dddd, D. MMMM YYYY H:mm" - }, - calendar: { - sameDay: "[данас у] LT", - nextDay: "[сутра у] LT", - nextWeek: function() { - switch (this.day()) { - case 0: - return "[у] [недељу] [у] LT"; - case 3: - return "[у] [среду] [у] LT"; - case 6: - return "[у] [суботу] [у] LT"; - case 1: - case 2: - case 4: - case 5: - return "[у] dddd [у] LT" - } - }, - lastDay: "[јуче у] LT", - lastWeek: function() { - var a = ["[прошле] [недеље] [у] LT", "[прошлог] [понедељка] [у] LT", "[прошлог] [уторка] [у] LT", "[прошле] [среде] [у] LT", "[прошлог] [четвртка] [у] LT", "[прошлог] [петка] [у] LT", "[прошле] [суботе] [у] LT"]; - return a[this.day()] - }, - sameElse: "L" + calendar: { + sameDay: "[данас у] LT", + nextDay: "[сутра у] LT", + nextWeek: function() { + switch (this.day()) { + case 0: + return "[у] [недељу] [у] LT"; + case 3: + return "[у] [среду] [у] LT"; + case 6: + return "[у] [суботу] [у] LT"; + case 1: + case 2: + case 4: + case 5: + return "[у] dddd [у] LT" + } }, - relativeTime: { - future: "за %s", - past: "пре %s", - s: "неколико секунди", - m: a.translate, - mm: a.translate, - h: a.translate, - hh: a.translate, - d: "дан", - dd: a.translate, - M: "месец", - MM: a.translate, - y: "годину", - yy: a.translate + lastDay: "[јуче у] LT", + lastWeek: function() { + var a = ["[прошле] [недеље] [у] LT", "[прошлог] [понедељка] [у] LT", "[прошлог] [уторка] [у] LT", "[прошле] [среде] [у] LT", "[прошлог] [четвртка] [у] LT", "[прошлог] [петка] [у] LT", "[прошле] [суботе] [у] LT"]; + return a[this.day()] }, - ordinalParse: /\d{1,2}\./, - ordinal: "%d.", - week: { - dow: 1, - doy: 7 - } - }); + sameElse: "L" + }, + relativeTime: { + future: "за %s", + past: "пре %s", + s: "неколико секунди", + m: a.translate, + mm: a.translate, + h: a.translate, + hh: a.translate, + d: "дан", + dd: a.translate, + M: "месец", + MM: a.translate, + y: "годину", + yy: a.translate + }, + ordinalParse: /\d{1,2}\./, + ordinal: "%d.", + week: { + dow: 1, + doy: 7 + } + }); return c }(), a.fullCalendar.datepickerLang("sr-cyrl", "sr", { closeText: "Затвори", @@ -4126,84 +4168,86 @@ ! function() { "use strict"; var a = { - words: { - m: ["jedan minut", "jedne minute"], - mm: ["minut", "minute", "minuta"], - h: ["jedan sat", "jednog sata"], - hh: ["sat", "sata", "sati"], - dd: ["dan", "dana", "dana"], - MM: ["mesec", "meseca", "meseci"], - yy: ["godina", "godine", "godina"] - }, - correctGrammaticalCase: function(a, b) { - return 1 === a ? b[0] : a >= 2 && 4 >= a ? b[1] : b[2] - }, - translate: function(b, c, d) { - var e = a.words[d]; - return 1 === d.length ? c ? e[0] : e[1] : b + " " + a.correctGrammaticalCase(b, e) - } + words: { + m: ["jedan minut", "jedne minute"], + mm: ["minut", "minute", "minuta"], + h: ["jedan sat", "jednog sata"], + hh: ["sat", "sata", "sati"], + dd: ["dan", "dana", "dana"], + MM: ["mesec", "meseca", "meseci"], + yy: ["godina", "godine", "godina"] + }, + correctGrammaticalCase: function(a, b) { + return 1 === a ? b[0] : a >= 2 && 4 >= a ? b[1] : b[2] + }, + translate: function(b, c, d) { + var e = a.words[d]; + return 1 === d.length ? c ? e[0] : e[1] : b + " " + a.correctGrammaticalCase(b, e) + } + }, + c = (b.defineLocale || b.lang).call(b, "sr", { + months: "januar_februar_mart_april_maj_jun_jul_avgust_septembar_oktobar_novembar_decembar".split("_"), + monthsShort: "jan._feb._mar._apr._maj_jun_jul_avg._sep._okt._nov._dec.".split("_"), + monthsParseExact: !0, + weekdays: "nedelja_ponedeljak_utorak_sreda_četvrtak_petak_subota".split("_"), + weekdaysShort: "ned._pon._uto._sre._čet._pet._sub.".split("_"), + weekdaysMin: "ne_po_ut_sr_če_pe_su".split("_"), + weekdaysParseExact: !0, + longDateFormat: { + LT: "H:mm", + LTS: "H:mm:ss", + L: "DD. MM. YYYY", + LL: "D. MMMM YYYY", + LLL: "D. MMMM YYYY H:mm", + LLLL: "dddd, D. MMMM YYYY H:mm" }, - c = (b.defineLocale || b.lang).call(b, "sr", { - months: ["januar", "februar", "mart", "april", "maj", "jun", "jul", "avgust", "septembar", "oktobar", "novembar", "decembar"], - monthsShort: ["jan.", "feb.", "mar.", "apr.", "maj", "jun", "jul", "avg.", "sep.", "okt.", "nov.", "dec."], - weekdays: ["nedelja", "ponedeljak", "utorak", "sreda", "četvrtak", "petak", "subota"], - weekdaysShort: ["ned.", "pon.", "uto.", "sre.", "čet.", "pet.", "sub."], - weekdaysMin: ["ne", "po", "ut", "sr", "če", "pe", "su"], - longDateFormat: { - LT: "H:mm", - LTS: "H:mm:ss", - L: "DD. MM. YYYY", - LL: "D. MMMM YYYY", - LLL: "D. MMMM YYYY H:mm", - LLLL: "dddd, D. MMMM YYYY H:mm" - }, - calendar: { - sameDay: "[danas u] LT", - nextDay: "[sutra u] LT", - nextWeek: function() { - switch (this.day()) { - case 0: - return "[u] [nedelju] [u] LT"; - case 3: - return "[u] [sredu] [u] LT"; - case 6: - return "[u] [subotu] [u] LT"; - case 1: - case 2: - case 4: - case 5: - return "[u] dddd [u] LT" - } - }, - lastDay: "[juče u] LT", - lastWeek: function() { - var a = ["[prošle] [nedelje] [u] LT", "[prošlog] [ponedeljka] [u] LT", "[prošlog] [utorka] [u] LT", "[prošle] [srede] [u] LT", "[prošlog] [četvrtka] [u] LT", "[prošlog] [petka] [u] LT", "[prošle] [subote] [u] LT"]; - return a[this.day()] - }, - sameElse: "L" + calendar: { + sameDay: "[danas u] LT", + nextDay: "[sutra u] LT", + nextWeek: function() { + switch (this.day()) { + case 0: + return "[u] [nedelju] [u] LT"; + case 3: + return "[u] [sredu] [u] LT"; + case 6: + return "[u] [subotu] [u] LT"; + case 1: + case 2: + case 4: + case 5: + return "[u] dddd [u] LT" + } }, - relativeTime: { - future: "za %s", - past: "pre %s", - s: "nekoliko sekundi", - m: a.translate, - mm: a.translate, - h: a.translate, - hh: a.translate, - d: "dan", - dd: a.translate, - M: "mesec", - MM: a.translate, - y: "godinu", - yy: a.translate + lastDay: "[juče u] LT", + lastWeek: function() { + var a = ["[prošle] [nedelje] [u] LT", "[prošlog] [ponedeljka] [u] LT", "[prošlog] [utorka] [u] LT", "[prošle] [srede] [u] LT", "[prošlog] [četvrtka] [u] LT", "[prošlog] [petka] [u] LT", "[prošle] [subote] [u] LT"]; + return a[this.day()] }, - ordinalParse: /\d{1,2}\./, - ordinal: "%d.", - week: { - dow: 1, - doy: 7 - } - }); + sameElse: "L" + }, + relativeTime: { + future: "za %s", + past: "pre %s", + s: "nekoliko sekundi", + m: a.translate, + mm: a.translate, + h: a.translate, + hh: a.translate, + d: "dan", + dd: a.translate, + M: "mesec", + MM: a.translate, + y: "godinu", + yy: a.translate + }, + ordinalParse: /\d{1,2}\./, + ordinal: "%d.", + week: { + dow: 1, + doy: 7 + } + }); return c }(), a.fullCalendar.datepickerLang("sr", "sr", { closeText: "Затвори", @@ -4248,8 +4292,10 @@ LTS: "HH:mm:ss", L: "YYYY-MM-DD", LL: "D MMMM YYYY", - LLL: "D MMMM YYYY HH:mm", - LLLL: "dddd D MMMM YYYY HH:mm" + LLL: "D MMMM YYYY [kl.] HH:mm", + LLLL: "dddd D MMMM YYYY [kl.] HH:mm", + lll: "D MMM YYYY HH:mm", + llll: "ddd D MMM YYYY HH:mm" }, calendar: { sameDay: "[Idag] LT", @@ -4277,7 +4323,7 @@ ordinalParse: /\d{1,2}(e|a)/, ordinal: function(a) { var b = a % 10, - c = 1 === ~~(a % 100 / 10) ? "e" : 1 === b ? "a" : 2 === b ? "a" : "e"; + c = 1 === ~~(a % 100 / 10) ? "e" : 1 === b ? "a" : 2 === b ? "a" : "e"; return a + c }, week: { @@ -4319,9 +4365,11 @@ var a = (b.defineLocale || b.lang).call(b, "th", { months: "มกราคม_กุมภาพันธ์_มีนาคม_เมษายน_พฤษภาคม_มิถุนายน_กรกฎาคม_สิงหาคม_กันยายน_ตุลาคม_พฤศจิกายน_ธันวาคม".split("_"), monthsShort: "มกรา_กุมภา_มีนา_เมษา_พฤษภา_มิถุนา_กรกฎา_สิงหา_กันยา_ตุลา_พฤศจิกา_ธันวา".split("_"), + monthsParseExact: !0, weekdays: "อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัสบดี_ศุกร์_เสาร์".split("_"), weekdaysShort: "อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัส_ศุกร์_เสาร์".split("_"), weekdaysMin: "อา._จ._อ._พ._พฤ._ศ._ส.".split("_"), + weekdaysParseExact: !0, longDateFormat: { LT: "H นาฬิกา m นาที", LTS: "H นาฬิกา m นาที s วินาที", @@ -4393,75 +4441,75 @@ ! function() { "use strict"; var a = { - 1: "'inci", - 5: "'inci", - 8: "'inci", - 70: "'inci", - 80: "'inci", - 2: "'nci", - 7: "'nci", - 20: "'nci", - 50: "'nci", - 3: "'üncü", - 4: "'üncü", - 100: "'üncü", - 6: "'ncı", - 9: "'uncu", - 10: "'uncu", - 30: "'uncu", - 60: "'ıncı", - 90: "'ıncı" - }, - c = (b.defineLocale || b.lang).call(b, "tr", { - months: "Ocak_Şubat_Mart_Nisan_Mayıs_Haziran_Temmuz_Ağustos_Eylül_Ekim_Kasım_Aralık".split("_"), - monthsShort: "Oca_Şub_Mar_Nis_May_Haz_Tem_Ağu_Eyl_Eki_Kas_Ara".split("_"), - weekdays: "Pazar_Pazartesi_Salı_Çarşamba_Perşembe_Cuma_Cumartesi".split("_"), - weekdaysShort: "Paz_Pts_Sal_Çar_Per_Cum_Cts".split("_"), - weekdaysMin: "Pz_Pt_Sa_Ça_Pe_Cu_Ct".split("_"), - longDateFormat: { - LT: "HH:mm", - LTS: "HH:mm:ss", - L: "DD.MM.YYYY", - LL: "D MMMM YYYY", - LLL: "D MMMM YYYY HH:mm", - LLLL: "dddd, D MMMM YYYY HH:mm" - }, - calendar: { - sameDay: "[bugün saat] LT", - nextDay: "[yarın saat] LT", - nextWeek: "[haftaya] dddd [saat] LT", - lastDay: "[dün] LT", - lastWeek: "[geçen hafta] dddd [saat] LT", - sameElse: "L" - }, - relativeTime: { - future: "%s sonra", - past: "%s önce", - s: "birkaç saniye", - m: "bir dakika", - mm: "%d dakika", - h: "bir saat", - hh: "%d saat", - d: "bir gün", - dd: "%d gün", - M: "bir ay", - MM: "%d ay", - y: "bir yıl", - yy: "%d yıl" - }, - ordinalParse: /\d{1,2}'(inci|nci|üncü|ncı|uncu|ıncı)/, - ordinal: function(b) { - if (0 === b) return b + "'ıncı"; - var c = b % 10, - d = b % 100 - c, - e = b >= 100 ? 100 : null; - return b + (a[c] || a[d] || a[e]) - }, - week: { - dow: 1, - doy: 7 - } - }); + 1: "'inci", + 5: "'inci", + 8: "'inci", + 70: "'inci", + 80: "'inci", + 2: "'nci", + 7: "'nci", + 20: "'nci", + 50: "'nci", + 3: "'üncü", + 4: "'üncü", + 100: "'üncü", + 6: "'ncı", + 9: "'uncu", + 10: "'uncu", + 30: "'uncu", + 60: "'ıncı", + 90: "'ıncı" + }, + c = (b.defineLocale || b.lang).call(b, "tr", { + months: "Ocak_Şubat_Mart_Nisan_Mayıs_Haziran_Temmuz_Ağustos_Eylül_Ekim_Kasım_Aralık".split("_"), + monthsShort: "Oca_Şub_Mar_Nis_May_Haz_Tem_Ağu_Eyl_Eki_Kas_Ara".split("_"), + weekdays: "Pazar_Pazartesi_Salı_Çarşamba_Perşembe_Cuma_Cumartesi".split("_"), + weekdaysShort: "Paz_Pts_Sal_Çar_Per_Cum_Cts".split("_"), + weekdaysMin: "Pz_Pt_Sa_Ça_Pe_Cu_Ct".split("_"), + longDateFormat: { + LT: "HH:mm", + LTS: "HH:mm:ss", + L: "DD.MM.YYYY", + LL: "D MMMM YYYY", + LLL: "D MMMM YYYY HH:mm", + LLLL: "dddd, D MMMM YYYY HH:mm" + }, + calendar: { + sameDay: "[bugün saat] LT", + nextDay: "[yarın saat] LT", + nextWeek: "[haftaya] dddd [saat] LT", + lastDay: "[dün] LT", + lastWeek: "[geçen hafta] dddd [saat] LT", + sameElse: "L" + }, + relativeTime: { + future: "%s sonra", + past: "%s önce", + s: "birkaç saniye", + m: "bir dakika", + mm: "%d dakika", + h: "bir saat", + hh: "%d saat", + d: "bir gün", + dd: "%d gün", + M: "bir ay", + MM: "%d ay", + y: "bir yıl", + yy: "%d yıl" + }, + ordinalParse: /\d{1,2}'(inci|nci|üncü|ncı|uncu|ıncı)/, + ordinal: function(b) { + if (0 === b) return b + "'ıncı"; + var c = b % 10, + d = b % 100 - c, + e = b >= 100 ? 100 : null; + return b + (a[c] || a[d] || a[e]) + }, + week: { + dow: 1, + doy: 7 + } + }); return c }(), a.fullCalendar.datepickerLang("tr", "tr", { closeText: "kapat", @@ -4513,11 +4561,11 @@ function d(a, b) { var c = { - nominative: "неділя_понеділок_вівторок_середа_четвер_п’ятниця_субота".split("_"), - accusative: "неділю_понеділок_вівторок_середу_четвер_п’ятницю_суботу".split("_"), - genitive: "неділі_понеділка_вівторка_середи_четверга_п’ятниці_суботи".split("_") - }, - d = /(\[[ВвУу]\]) ?dddd/.test(b) ? "accusative" : /\[?(?:минулої|наступної)? ?\] ?dddd/.test(b) ? "genitive" : "nominative"; + nominative: "неділя_понеділок_вівторок_середа_четвер_п’ятниця_субота".split("_"), + accusative: "неділю_понеділок_вівторок_середу_четвер_п’ятницю_суботу".split("_"), + genitive: "неділі_понеділка_вівторка_середи_четверга_п’ятниці_суботи".split("_") + }, + d = /(\[[ВвУу]\]) ?dddd/.test(b) ? "accusative" : /\[?(?:минулої|наступної)? ?\] ?dddd/.test(b) ? "genitive" : "nominative"; return c[d][a.day()] } @@ -4554,11 +4602,11 @@ case 3: case 5: case 6: - return e("[Минулої] dddd [").call(this); + return e("[Минулої] dddd [").call(this); case 1: case 2: case 4: - return e("[Минулого] dddd [").call(this) + return e("[Минулого] dddd [").call(this) } }, sameElse: "L" @@ -4593,11 +4641,11 @@ case "DDD": case "w": case "W": - return a + "-й"; + return a + "-й"; case "D": - return a + "-го"; + return a + "-го"; default: - return a + return a } }, week: { @@ -4641,9 +4689,18 @@ var a = (b.defineLocale || b.lang).call(b, "vi", { months: "tháng 1_tháng 2_tháng 3_tháng 4_tháng 5_tháng 6_tháng 7_tháng 8_tháng 9_tháng 10_tháng 11_tháng 12".split("_"), monthsShort: "Th01_Th02_Th03_Th04_Th05_Th06_Th07_Th08_Th09_Th10_Th11_Th12".split("_"), + monthsParseExact: !0, weekdays: "chủ nhật_thứ hai_thứ ba_thứ tư_thứ năm_thứ sáu_thứ bảy".split("_"), weekdaysShort: "CN_T2_T3_T4_T5_T6_T7".split("_"), weekdaysMin: "CN_T2_T3_T4_T5_T6_T7".split("_"), + weekdaysParseExact: !0, + meridiemParse: /sa|ch/i, + isPM: function(a) { + return /^ch$/i.test(a) + }, + meridiem: function(a, b, c) { + return 12 > a ? c ? "sa" : "SA" : c ? "ch" : "CH" + }, longDateFormat: { LT: "HH:mm", LTS: "HH:mm:ss", @@ -4759,7 +4816,7 @@ }, nextWeek: function() { var a, c; - return a = b().startOf("week"), c = this.unix() - a.unix() >= 604800 ? "[下]" : "[本]", 0 === this.minutes() ? c + "dddAh点整" : c + "dddAh点mm" + return a = b().startOf("week"), c = this.diff(a, "days") >= 7 ? "[下]" : "[本]", 0 === this.minutes() ? c + "dddAh点整" : c + "dddAh点mm" }, lastWeek: function() { var a, c; @@ -4773,14 +4830,14 @@ case "d": case "D": case "DDD": - return a + "日"; + return a + "日"; case "M": - return a + "月"; + return a + "月"; case "w": case "W": - return a + "周"; + return a + "周"; default: - return a + return a } }, relativeTime: { @@ -4876,29 +4933,29 @@ case "d": case "D": case "DDD": - return a + "日"; + return a + "日"; case "M": - return a + "月"; + return a + "月"; case "w": case "W": - return a + "週"; + return a + "週"; default: - return a + return a } }, relativeTime: { future: "%s內", past: "%s前", s: "幾秒", - m: "一分鐘", + m: "1分鐘", mm: "%d分鐘", - h: "一小時", + h: "1小時", hh: "%d小時", - d: "一天", + d: "1天", dd: "%d天", - M: "一個月", + M: "1個月", MM: "%d個月", - y: "一年", + y: "1年", yy: "%d年" } }); diff --git a/src/calendar/lib/moment.js b/src/calendar/lib/moment.js index dab6c2e..c616ea7 100644 --- a/src/calendar/lib/moment.js +++ b/src/calendar/lib/moment.js @@ -1,29 +1,30 @@ + //! moment.js -//! version : 2.11.0 +//! version : 2.13.0 //! authors : Tim Wood, Iskren Chernev, Moment.js contributors //! license : MIT //! momentjs.com -;(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - global.moment = factory() -}(this, function () { 'use strict'; +; +(function(global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : global.moment = factory() +}(this, function() { + 'use strict'; var hookCallback; - function utils_hooks__hooks () { + function utils_hooks__hooks() { return hookCallback.apply(null, arguments); } // This is done to register the method called with moment() // without creating circular dependencies. - function setHookCallback (callback) { + function setHookCallback(callback) { hookCallback = callback; } function isArray(input) { - return Object.prototype.toString.call(input) === '[object Array]'; + return input instanceof Array || Object.prototype.toString.call(input) === '[object Array]'; } function isDate(input) { @@ -31,7 +32,8 @@ } function map(arr, fn) { - var res = [], i; + var res = [], + i; for (i = 0; i < arr.length; ++i) { res.push(fn(arr[i], i)); } @@ -60,23 +62,25 @@ return a; } - function create_utc__createUTC (input, format, locale, strict) { + function create_utc__createUTC(input, format, locale, strict) { return createLocalOrUTC(input, format, locale, strict, true).utc(); } function defaultParsingFlags() { // We need to deep clone this object. return { - empty : false, - unusedTokens : [], - unusedInput : [], - overflow : -2, - charsLeftOver : 0, - nullInput : false, - invalidMonth : null, - invalidFormat : false, - userInvalidated : false, - iso : false + empty: false, + unusedTokens: [], + unusedInput: [], + overflow: -2, + charsLeftOver: 0, + nullInput: false, + invalidMonth: null, + invalidFormat: false, + userInvalidated: false, + iso: false, + parsedDateParts: [], + meridiem: null }; } @@ -87,34 +91,44 @@ return m._pf; } + var some; + if (Array.prototype.some) { + some = Array.prototype.some; + } else { + some = function(fun) { + var t = Object(this); + var len = t.length >>> 0; + + for (var i = 0; i < len; i++) { + if (i in t && fun.call(this, t[i], i, t)) { + return true; + } + } + + return false; + }; + } + function valid__isValid(m) { if (m._isValid == null) { var flags = getParsingFlags(m); - m._isValid = !isNaN(m._d.getTime()) && - flags.overflow < 0 && - !flags.empty && - !flags.invalidMonth && - !flags.invalidWeekday && - !flags.nullInput && - !flags.invalidFormat && - !flags.userInvalidated; + var parsedParts = some.call(flags.parsedDateParts, function(i) { + return i != null; + }); + m._isValid = !isNaN(m._d.getTime()) && flags.overflow < 0 && !flags.empty && !flags.invalidMonth && !flags.invalidWeekday && !flags.nullInput && !flags.invalidFormat && !flags.userInvalidated && (!flags.meridiem || (flags.meridiem && parsedParts)); if (m._strict) { - m._isValid = m._isValid && - flags.charsLeftOver === 0 && - flags.unusedTokens.length === 0 && - flags.bigHour === undefined; + m._isValid = m._isValid && flags.charsLeftOver === 0 && flags.unusedTokens.length === 0 && flags.bigHour === undefined; } } return m._isValid; } - function valid__createInvalid (flags) { + function valid__createInvalid(flags) { var m = create_utc__createUTC(NaN); if (flags != null) { extend(getParsingFlags(m), flags); - } - else { + } else { getParsingFlags(m).userInvalidated = true; } @@ -191,11 +205,11 @@ } } - function isMoment (obj) { + function isMoment(obj) { return obj instanceof Moment || (obj != null && obj._isAMomentObject != null); } - function absFloor (number) { + function absFloor(number) { if (number < 0) { return Math.ceil(number); } else { @@ -221,15 +235,112 @@ diffs = 0, i; for (i = 0; i < len; i++) { - if ((dontConvert && array1[i] !== array2[i]) || - (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) { + if ((dontConvert && array1[i] !== array2[i]) || (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) { diffs++; } } return diffs + lengthDiff; } - function Locale() { + function warn(msg) { + if (utils_hooks__hooks.suppressDeprecationWarnings === false && (typeof console !== 'undefined') && console.warn) { + console.warn('Deprecation warning: ' + msg); + } + } + + function deprecate(msg, fn) { + var firstTime = true; + + return extend(function() { + if (utils_hooks__hooks.deprecationHandler != null) { + utils_hooks__hooks.deprecationHandler(null, msg); + } + if (firstTime) { + warn(msg + '\nArguments: ' + Array.prototype.slice.call(arguments).join(', ') + '\n' + (new Error()).stack); + firstTime = false; + } + return fn.apply(this, arguments); + }, fn); + } + + var deprecations = {}; + + function deprecateSimple(name, msg) { + if (utils_hooks__hooks.deprecationHandler != null) { + utils_hooks__hooks.deprecationHandler(name, msg); + } + if (!deprecations[name]) { + warn(msg); + deprecations[name] = true; + } + } + + utils_hooks__hooks.suppressDeprecationWarnings = false; + utils_hooks__hooks.deprecationHandler = null; + + function isFunction(input) { + return input instanceof Function || Object.prototype.toString.call(input) === '[object Function]'; + } + + function isObject(input) { + return Object.prototype.toString.call(input) === '[object Object]'; + } + + function locale_set__set(config) { + var prop, i; + for (i in config) { + prop = config[i]; + if (isFunction(prop)) { + this[i] = prop; + } else { + this['_' + i] = prop; + } + } + this._config = config; + // Lenient ordinal parsing accepts just a number in addition to + // number + (possibly) stuff coming from _ordinalParseLenient. + this._ordinalParseLenient = new RegExp(this._ordinalParse.source + '|' + (/\d{1,2}/).source); + } + + function mergeConfigs(parentConfig, childConfig) { + var res = extend({}, parentConfig), + prop; + for (prop in childConfig) { + if (hasOwnProp(childConfig, prop)) { + if (isObject(parentConfig[prop]) && isObject(childConfig[prop])) { + res[prop] = {}; + extend(res[prop], parentConfig[prop]); + extend(res[prop], childConfig[prop]); + } else if (childConfig[prop] != null) { + res[prop] = childConfig[prop]; + } else { + delete res[prop]; + } + } + } + return res; + } + + function Locale(config) { + if (config != null) { + this.set(config); + } + } + + var keys; + + if (Object.keys) { + keys = Object.keys; + } else { + keys = function(obj) { + var i, res = []; + for (i in obj) { + if (hasOwnProp(obj, i)) { + res.push(i); + } + } + return res; + }; } // internal storage for locale config files @@ -244,7 +355,8 @@ // try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each // substring from most specific to least, but move to the next array item if it's a more specific variant than the current root function chooseLocale(names) { - var i = 0, j, next, locale, split; + var i = 0, + j, next, locale, split; while (i < names.length) { split = normalizeLocale(names[i]).split('-'); @@ -270,15 +382,14 @@ function loadLocale(name) { var oldLocale = null; // TODO: Find a better way to register and load all the locales in Node - if (!locales[name] && !isUndefined(module) && - module && module.exports) { + if (!locales[name] && (typeof module !== 'undefined') && module && module.exports) { try { oldLocale = globalLocale._abbr; require('./locale/' + name); // because defineLocale currently also sets the global locale, we // want to undo that for lazy loaded locales locale_locales__getSetGlobalLocale(oldLocale); - } catch (e) { } + } catch (e) {} } return locales[name]; } @@ -286,13 +397,12 @@ // This function will load locale and then set the global locale. If // no arguments are passed in, it will simply return the current global // locale key. - function locale_locales__getSetGlobalLocale (key, values) { + function locale_locales__getSetGlobalLocale(key, values) { var data; if (key) { if (isUndefined(values)) { data = locale_locales__getLocale(key); - } - else { + } else { data = defineLocale(key, values); } @@ -305,11 +415,21 @@ return globalLocale._abbr; } - function defineLocale (name, values) { - if (values !== null) { - values.abbr = name; - locales[name] = locales[name] || new Locale(); - locales[name].set(values); + function defineLocale(name, config) { + if (config !== null) { + config.abbr = name; + if (locales[name] != null) { + deprecateSimple('defineLocaleOverride', 'use moment.updateLocale(localeName, config) to change ' + 'an existing locale. moment.defineLocale(localeName, ' + 'config) should only be used for creating a new locale'); + config = mergeConfigs(locales[name]._config, config); + } else if (config.parentLocale != null) { + if (locales[config.parentLocale] != null) { + config = mergeConfigs(locales[config.parentLocale]._config, config); + } else { + // treat as if there is no base config + deprecateSimple('parentLocaleUndefined', 'specified parentLocale is not defined yet'); + } + } + locales[name] = new Locale(config); // backwards compat for now: also set the locale locale_locales__getSetGlobalLocale(name); @@ -322,8 +442,33 @@ } } + function updateLocale(name, config) { + if (config != null) { + var locale; + if (locales[name] != null) { + config = mergeConfigs(locales[name]._config, config); + } + locale = new Locale(config); + locale.parentLocale = locales[name]; + locales[name] = locale; + + // backwards compat for now: also set the locale + locale_locales__getSetGlobalLocale(name); + } else { + // pass null for config to unupdate, useful for tests + if (locales[name] != null) { + if (locales[name].parentLocale != null) { + locales[name] = locales[name].parentLocale; + } else if (locales[name] != null) { + delete locales[name]; + } + } + } + return locales[name]; + } + // returns locale data - function locale_locales__getLocale (key) { + function locale_locales__getLocale(key) { var locale; if (key && key._locale && key._locale._abbr) { @@ -346,9 +491,13 @@ return chooseLocale(key); } + function locale_locales__listLocales() { + return keys(locales); + } + var aliases = {}; - function addUnitAlias (unit, shorthand) { + function addUnitAlias(unit, shorthand) { var lowerCase = unit.toLowerCase(); aliases[lowerCase] = aliases[lowerCase + 's'] = aliases[shorthand] = unit; } @@ -359,8 +508,8 @@ function normalizeObjectUnits(inputObject) { var normalizedInput = {}, - normalizedProp, - prop; + normalizedProp, + prop; for (prop in inputObject) { if (hasOwnProp(inputObject, prop)) { @@ -374,12 +523,8 @@ return normalizedInput; } - function isFunction(input) { - return input instanceof Function || Object.prototype.toString.call(input) === '[object Function]'; - } - - function makeGetSet (unit, keepTime) { - return function (value) { + function makeGetSet(unit, keepTime) { + return function(value) { if (value != null) { get_set__set(this, unit, value); utils_hooks__hooks.updateOffset(this, keepTime); @@ -390,12 +535,11 @@ }; } - function get_set__get (mom, unit) { - return mom.isValid() ? - mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]() : NaN; + function get_set__get(mom, unit) { + return mom.isValid() ? mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]() : NaN; } - function get_set__set (mom, unit, value) { + function get_set__set(mom, unit, value) { if (mom.isValid()) { mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); } @@ -403,7 +547,7 @@ // MOMENTS - function getSet (units, value) { + function getSet(units, value) { var unit; if (typeof units === 'object') { for (unit in units) { @@ -422,11 +566,10 @@ var absNumber = '' + Math.abs(number), zerosToFill = targetLength - absNumber.length, sign = number >= 0; - return (sign ? (forceSign ? '+' : '') : '-') + - Math.pow(10, Math.max(0, zerosToFill)).toString().substr(1) + absNumber; + return (sign ? (forceSign ? '+' : '') : '-') + Math.pow(10, Math.max(0, zerosToFill)).toString().substr(1) + absNumber; } - var formattingTokens = /(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g; + var formattingTokens = /(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g; var localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g; @@ -438,10 +581,10 @@ // padded: ['MM', 2] // ordinal: 'Mo' // callback: function () { this.month() + 1 } - function addFormatToken (token, padded, ordinal, callback) { + function addFormatToken(token, padded, ordinal, callback) { var func = callback; if (typeof callback === 'string') { - func = function () { + func = function() { return this[callback](); }; } @@ -449,12 +592,12 @@ formatTokenFunctions[token] = func; } if (padded) { - formatTokenFunctions[padded[0]] = function () { + formatTokenFunctions[padded[0]] = function() { return zeroFill(func.apply(this, arguments), padded[1], padded[2]); }; } if (ordinal) { - formatTokenFunctions[ordinal] = function () { + formatTokenFunctions[ordinal] = function() { return this.localeData().ordinal(func.apply(this, arguments), token); }; } @@ -468,7 +611,8 @@ } function makeFormatFunction(format) { - var array = format.match(formattingTokens), i, length; + var array = format.match(formattingTokens), + i, length; for (i = 0, length = array.length; i < length; i++) { if (formatTokenFunctions[array[i]]) { @@ -478,8 +622,9 @@ } } - return function (mom) { - var output = ''; + return function(mom) { + var output = '', + i; for (i = 0; i < length; i++) { output += array[i] instanceof Function ? array[i].call(mom, format) : array[i]; } @@ -516,40 +661,40 @@ return format; } - var match1 = /\d/; // 0 - 9 - var match2 = /\d\d/; // 00 - 99 - var match3 = /\d{3}/; // 000 - 999 - var match4 = /\d{4}/; // 0000 - 9999 - var match6 = /[+-]?\d{6}/; // -999999 - 999999 - var match1to2 = /\d\d?/; // 0 - 99 - var match3to4 = /\d\d\d\d?/; // 999 - 9999 - var match5to6 = /\d\d\d\d\d\d?/; // 99999 - 999999 - var match1to3 = /\d{1,3}/; // 0 - 999 - var match1to4 = /\d{1,4}/; // 0 - 9999 - var match1to6 = /[+-]?\d{1,6}/; // -999999 - 999999 + var match1 = /\d/; // 0 - 9 + var match2 = /\d\d/; // 00 - 99 + var match3 = /\d{3}/; // 000 - 999 + var match4 = /\d{4}/; // 0000 - 9999 + var match6 = /[+-]?\d{6}/; // -999999 - 999999 + var match1to2 = /\d\d?/; // 0 - 99 + var match3to4 = /\d\d\d\d?/; // 999 - 9999 + var match5to6 = /\d\d\d\d\d\d?/; // 99999 - 999999 + var match1to3 = /\d{1,3}/; // 0 - 999 + var match1to4 = /\d{1,4}/; // 0 - 9999 + var match1to6 = /[+-]?\d{1,6}/; // -999999 - 999999 - var matchUnsigned = /\d+/; // 0 - inf - var matchSigned = /[+-]?\d+/; // -inf - inf + var matchUnsigned = /\d+/; // 0 - inf + var matchSigned = /[+-]?\d+/; // -inf - inf - var matchOffset = /Z|[+-]\d\d:?\d\d/gi; // +00:00 -00:00 +0000 -0000 or Z + var matchOffset = /Z|[+-]\d\d:?\d\d/gi; // +00:00 -00:00 +0000 -0000 or Z var matchShortOffset = /Z|[+-]\d\d(?::?\d\d)?/gi; // +00 -00 +00:00 -00:00 +0000 -0000 or Z var matchTimestamp = /[+-]?\d+(\.\d{1,3})?/; // 123456789 123456789.123 // any word (or two) characters or numbers including two/three word month in arabic. // includes scottish gaelic two word and hyphenated months - var matchWord = /[0-9]*(a[mn]\s?)?['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF\-]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i; + var matchWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i; var regexes = {}; - function addRegexToken (token, regex, strictRegex) { - regexes[token] = isFunction(regex) ? regex : function (isStrict) { + function addRegexToken(token, regex, strictRegex) { + regexes[token] = isFunction(regex) ? regex : function(isStrict, localeData) { return (isStrict && strictRegex) ? strictRegex : regex; }; } - function getParseRegexForToken (token, config) { + function getParseRegexForToken(token, config) { if (!hasOwnProp(regexes, token)) { return new RegExp(unescapeFormat(token)); } @@ -559,20 +704,24 @@ // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript function unescapeFormat(s) { - return s.replace('\\', '').replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) { + return regexEscape(s.replace('\\', '').replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function(matched, p1, p2, p3, p4) { return p1 || p2 || p3 || p4; - }).replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + })); + } + + function regexEscape(s) { + return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); } var tokens = {}; - function addParseToken (token, callback) { + function addParseToken(token, callback) { var i, func = callback; if (typeof token === 'string') { token = [token]; } if (typeof callback === 'number') { - func = function (input, array) { + func = function(input, array) { array[callback] = toInt(input); }; } @@ -581,8 +730,8 @@ } } - function addWeekParseToken (token, callback) { - addParseToken(token, function (input, array, config, token) { + function addWeekParseToken(token, callback) { + addParseToken(token, function(input, array, config, token) { config._w = config._w || {}; callback(input, config._w, config, token); }); @@ -604,21 +753,38 @@ var WEEK = 7; var WEEKDAY = 8; + var indexOf; + + if (Array.prototype.indexOf) { + indexOf = Array.prototype.indexOf; + } else { + indexOf = function(o) { + // I know + var i; + for (i = 0; i < this.length; ++i) { + if (this[i] === o) { + return i; + } + } + return -1; + }; + } + function daysInMonth(year, month) { return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); } // FORMATTING - addFormatToken('M', ['MM', 2], 'Mo', function () { + addFormatToken('M', ['MM', 2], 'Mo', function() { return this.month() + 1; }); - addFormatToken('MMM', 0, 0, function (format) { + addFormatToken('MMM', 0, 0, function(format) { return this.localeData().monthsShort(this, format); }); - addFormatToken('MMMM', 0, 0, function (format) { + addFormatToken('MMMM', 0, 0, function(format) { return this.localeData().months(this, format); }); @@ -628,16 +794,20 @@ // PARSING - addRegexToken('M', match1to2); - addRegexToken('MM', match1to2, match2); - addRegexToken('MMM', matchWord); - addRegexToken('MMMM', matchWord); + addRegexToken('M', match1to2); + addRegexToken('MM', match1to2, match2); + addRegexToken('MMM', function(isStrict, locale) { + return locale.monthsShortRegex(isStrict); + }); + addRegexToken('MMMM', function(isStrict, locale) { + return locale.monthsRegex(isStrict); + }); - addParseToken(['M', 'MM'], function (input, array) { + addParseToken(['M', 'MM'], function(input, array) { array[MONTH] = toInt(input) - 1; }); - addParseToken(['MMM', 'MMMM'], function (input, array, config, token) { + addParseToken(['MMM', 'MMMM'], function(input, array, config, token) { var month = config._locale.monthsParse(input, token, config._strict); // if we didn't find a month name, mark the date as invalid. if (month != null) { @@ -651,26 +821,74 @@ var MONTHS_IN_FORMAT = /D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/; var defaultLocaleMonths = 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'); - function localeMonths (m, format) { - return isArray(this._months) ? this._months[m.month()] : - this._months[MONTHS_IN_FORMAT.test(format) ? 'format' : 'standalone'][m.month()]; + + function localeMonths(m, format) { + return isArray(this._months) ? this._months[m.month()] : this._months[MONTHS_IN_FORMAT.test(format) ? 'format' : 'standalone'][m.month()]; + } + + var defaultLocaleMonthsShort = 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'); + + function localeMonthsShort(m, format) { + return isArray(this._monthsShort) ? this._monthsShort[m.month()] : this._monthsShort[MONTHS_IN_FORMAT.test(format) ? 'format' : 'standalone'][m.month()]; } - var defaultLocaleMonthsShort = 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sept_Oct_Nov_Dec'.split('_'); - function localeMonthsShort (m, format) { - return isArray(this._monthsShort) ? this._monthsShort[m.month()] : - this._monthsShort[MONTHS_IN_FORMAT.test(format) ? 'format' : 'standalone'][m.month()]; + function units_month__handleStrictParse(monthName, format, strict) { + var i, ii, mom, llc = monthName.toLocaleLowerCase(); + if (!this._monthsParse) { + // this is not used + this._monthsParse = []; + this._longMonthsParse = []; + this._shortMonthsParse = []; + for (i = 0; i < 12; ++i) { + mom = create_utc__createUTC([2000, i]); + this._shortMonthsParse[i] = this.monthsShort(mom, '').toLocaleLowerCase(); + this._longMonthsParse[i] = this.months(mom, '').toLocaleLowerCase(); + } + } + + if (strict) { + if (format === 'MMM') { + ii = indexOf.call(this._shortMonthsParse, llc); + return ii !== -1 ? ii : null; + } else { + ii = indexOf.call(this._longMonthsParse, llc); + return ii !== -1 ? ii : null; + } + } else { + if (format === 'MMM') { + ii = indexOf.call(this._shortMonthsParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._longMonthsParse, llc); + return ii !== -1 ? ii : null; + } else { + ii = indexOf.call(this._longMonthsParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._shortMonthsParse, llc); + return ii !== -1 ? ii : null; + } + } } - function localeMonthsParse (monthName, format, strict) { + function localeMonthsParse(monthName, format, strict) { var i, mom, regex; + if (this._monthsParseExact) { + return units_month__handleStrictParse.call(this, monthName, format, strict); + } + if (!this._monthsParse) { this._monthsParse = []; this._longMonthsParse = []; this._shortMonthsParse = []; } + // TODO: add sorting + // Sorting makes sure if one month (or abbr) is a prefix of another + // see sorting in computeMonthsParse for (i = 0; i < 12; i++) { // make the regex if we don't have it already mom = create_utc__createUTC([2000, i]); @@ -695,7 +913,7 @@ // MOMENTS - function setMonth (mom, value) { + function setMonth(mom, value) { var dayOfMonth; if (!mom.isValid()) { @@ -703,12 +921,15 @@ return mom; } - // TODO: Move this out of here! if (typeof value === 'string') { - value = mom.localeData().monthsParse(value); - // TODO: Another silent failure? - if (typeof value !== 'number') { - return mom; + if (/^\d+$/.test(value)) { + value = toInt(value); + } else { + value = mom.localeData().monthsParse(value); + // TODO: Another silent failure? + if (typeof value !== 'number') { + return mom; + } } } @@ -717,7 +938,7 @@ return mom; } - function getSetMonth (value) { + function getSetMonth(value) { if (value != null) { setMonth(this, value); utils_hooks__hooks.updateOffset(this, true); @@ -727,23 +948,84 @@ } } - function getDaysInMonth () { + function getDaysInMonth() { return daysInMonth(this.year(), this.month()); } - function checkOverflow (m) { + var defaultMonthsShortRegex = matchWord; + + function monthsShortRegex(isStrict) { + if (this._monthsParseExact) { + if (!hasOwnProp(this, '_monthsRegex')) { + computeMonthsParse.call(this); + } + if (isStrict) { + return this._monthsShortStrictRegex; + } else { + return this._monthsShortRegex; + } + } else { + return this._monthsShortStrictRegex && isStrict ? this._monthsShortStrictRegex : this._monthsShortRegex; + } + } + + var defaultMonthsRegex = matchWord; + + function monthsRegex(isStrict) { + if (this._monthsParseExact) { + if (!hasOwnProp(this, '_monthsRegex')) { + computeMonthsParse.call(this); + } + if (isStrict) { + return this._monthsStrictRegex; + } else { + return this._monthsRegex; + } + } else { + return this._monthsStrictRegex && isStrict ? this._monthsStrictRegex : this._monthsRegex; + } + } + + function computeMonthsParse() { + function cmpLenRev(a, b) { + return b.length - a.length; + } + + var shortPieces = [], + longPieces = [], + mixedPieces = [], + i, mom; + for (i = 0; i < 12; i++) { + // make the regex if we don't have it already + mom = create_utc__createUTC([2000, i]); + shortPieces.push(this.monthsShort(mom, '')); + longPieces.push(this.months(mom, '')); + mixedPieces.push(this.months(mom, '')); + mixedPieces.push(this.monthsShort(mom, '')); + } + // Sorting makes sure if one month (or abbr) is a prefix of another it + // will match the longer piece. + shortPieces.sort(cmpLenRev); + longPieces.sort(cmpLenRev); + mixedPieces.sort(cmpLenRev); + for (i = 0; i < 12; i++) { + shortPieces[i] = regexEscape(shortPieces[i]); + longPieces[i] = regexEscape(longPieces[i]); + mixedPieces[i] = regexEscape(mixedPieces[i]); + } + + this._monthsRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i'); + this._monthsShortRegex = this._monthsRegex; + this._monthsStrictRegex = new RegExp('^(' + longPieces.join('|') + ')', 'i'); + this._monthsShortStrictRegex = new RegExp('^(' + shortPieces.join('|') + ')', 'i'); + } + + function checkOverflow(m) { var overflow; var a = m._a; if (a && getParsingFlags(m).overflow === -2) { - overflow = - a[MONTH] < 0 || a[MONTH] > 11 ? MONTH : - a[DATE] < 1 || a[DATE] > daysInMonth(a[YEAR], a[MONTH]) ? DATE : - a[HOUR] < 0 || a[HOUR] > 24 || (a[HOUR] === 24 && (a[MINUTE] !== 0 || a[SECOND] !== 0 || a[MILLISECOND] !== 0)) ? HOUR : - a[MINUTE] < 0 || a[MINUTE] > 59 ? MINUTE : - a[SECOND] < 0 || a[SECOND] > 59 ? SECOND : - a[MILLISECOND] < 0 || a[MILLISECOND] > 999 ? MILLISECOND : - -1; + overflow = a[MONTH] < 0 || a[MONTH] > 11 ? MONTH : a[DATE] < 1 || a[DATE] > daysInMonth(a[YEAR], a[MONTH]) ? DATE : a[HOUR] < 0 || a[HOUR] > 24 || (a[HOUR] === 24 && (a[MINUTE] !== 0 || a[SECOND] !== 0 || a[MILLISECOND] !== 0)) ? HOUR : a[MINUTE] < 0 || a[MINUTE] > 59 ? MINUTE : a[SECOND] < 0 || a[SECOND] > 59 ? SECOND : a[MILLISECOND] < 0 || a[MILLISECOND] > 999 ? MILLISECOND : -1; if (getParsingFlags(m)._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { overflow = DATE; @@ -761,35 +1043,6 @@ return m; } - function warn(msg) { - if (utils_hooks__hooks.suppressDeprecationWarnings === false && !isUndefined(console) && console.warn) { - console.warn('Deprecation warning: ' + msg); - } - } - - function deprecate(msg, fn) { - var firstTime = true; - - return extend(function () { - if (firstTime) { - warn(msg + '\nArguments: ' + Array.prototype.slice.call(arguments).join(', ') + '\n' + (new Error()).stack); - firstTime = false; - } - return fn.apply(this, arguments); - }, fn); - } - - var deprecations = {}; - - function deprecateSimple(name, msg) { - if (!deprecations[name]) { - warn(msg); - deprecations[name] = true; - } - } - - utils_hooks__hooks.suppressDeprecationWarnings = false; - // iso 8601 regex // 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 or +00) var extendedIsoRegex = /^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?/; @@ -830,7 +1083,7 @@ // date from iso format function configFromISO(config) { var i, l, - string = config._i, + string = config._i, match = extendedIsoRegex.exec(string) || basicIsoRegex.exec(string), allowTime, dateFormat, timeFormat, tzFormat; @@ -896,17 +1149,13 @@ } } - utils_hooks__hooks.createFromInputFallback = deprecate( - 'moment construction falls back to js Date. This is ' + - 'discouraged and will be removed in upcoming major ' + - 'release. Please refer to ' + - 'https://github.com/moment/moment/issues/1407 for more info.', - function (config) { - config._d = new Date(config._i + (config._useUTC ? ' UTC' : '')); - } - ); + utils_hooks__hooks.createFromInputFallback = deprecate('moment construction falls back to js Date. This is ' + 'discouraged and will be removed in upcoming major ' + 'release. Please refer to ' + 'https://github.com/moment/moment/issues/1407 for more info.', + + function(config) { + config._d = new Date(config._i + (config._useUTC ? ' UTC' : '')); + }); - function createDate (y, m, d, h, M, s, ms) { + function createDate(y, m, d, h, M, s, ms) { //can't just apply() to create a date: //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply var date = new Date(y, m, d, h, M, s, ms); @@ -918,7 +1167,7 @@ return date; } - function createUTCDate (y) { + function createUTCDate(y) { var date = new Date(Date.UTC.apply(null, arguments)); //the Date.UTC function remaps years 0-99 to 1900-1999 @@ -930,12 +1179,17 @@ // FORMATTING - addFormatToken(0, ['YY', 2], 0, function () { + addFormatToken('Y', 0, 0, function() { + var y = this.year(); + return y <= 9999 ? '' + y : '+' + y; + }); + + addFormatToken(0, ['YY', 2], 0, function() { return this.year() % 100; }); - addFormatToken(0, ['YYYY', 4], 0, 'year'); - addFormatToken(0, ['YYYYY', 5], 0, 'year'); + addFormatToken(0, ['YYYY', 4], 0, 'year'); + addFormatToken(0, ['YYYYY', 5], 0, 'year'); addFormatToken(0, ['YYYYYY', 6, true], 0, 'year'); // ALIASES @@ -944,19 +1198,22 @@ // PARSING - addRegexToken('Y', matchSigned); - addRegexToken('YY', match1to2, match2); - addRegexToken('YYYY', match1to4, match4); - addRegexToken('YYYYY', match1to6, match6); + addRegexToken('Y', matchSigned); + addRegexToken('YY', match1to2, match2); + addRegexToken('YYYY', match1to4, match4); + addRegexToken('YYYYY', match1to6, match6); addRegexToken('YYYYYY', match1to6, match6); addParseToken(['YYYYY', 'YYYYYY'], YEAR); - addParseToken('YYYY', function (input, array) { + addParseToken('YYYY', function(input, array) { array[YEAR] = input.length === 2 ? utils_hooks__hooks.parseTwoDigitYear(input) : toInt(input); }); - addParseToken('YY', function (input, array) { + addParseToken('YY', function(input, array) { array[YEAR] = utils_hooks__hooks.parseTwoDigitYear(input); }); + addParseToken('Y', function(input, array) { + array[YEAR] = parseInt(input, 10); + }); // HELPERS @@ -970,22 +1227,22 @@ // HOOKS - utils_hooks__hooks.parseTwoDigitYear = function (input) { + utils_hooks__hooks.parseTwoDigitYear = function(input) { return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); }; // MOMENTS - var getSetYear = makeGetSet('FullYear', false); + var getSetYear = makeGetSet('FullYear', true); - function getIsLeapYear () { + function getIsLeapYear() { return isLeapYear(this.year()); } // start-of-first-week - start-of-year function firstWeekOffset(year, dow, doy) { var // first-week day -- which january is always in the first week (4 for iso, 1 for other) - fwd = 7 + dow - doy, + fwd = 7 + dow - doy, // first-week day local weekday -- which local weekday is fwd fwdlw = (7 + createUTCDate(year, 0, fwd).getUTCDay() - dow) % 7; @@ -1068,8 +1325,9 @@ // the array should mirror the parameters below // note: all values past the year are optional and will default to the lowest possible value. // [year, month, day , hour, minute, second, millisecond] - function configFromArray (config) { - var i, date, input = [], currentDate, yearToUse; + function configFromArray(config) { + var i, date, input = [], + currentDate, yearToUse; if (config._d) { return; @@ -1110,10 +1368,7 @@ } // Check for 24:00:00.000 - if (config._a[HOUR] === 24 && - config._a[MINUTE] === 0 && - config._a[SECOND] === 0 && - config._a[MILLISECOND] === 0) { + if (config._a[HOUR] === 24 && config._a[MINUTE] === 0 && config._a[SECOND] === 0 && config._a[MILLISECOND] === 0) { config._nextDay = true; config._a[HOUR] = 0; } @@ -1184,7 +1439,7 @@ } // constant that refers to the ISO standard - utils_hooks__hooks.ISO_8601 = function () {}; + utils_hooks__hooks.ISO_8601 = function() {}; // date from string and format string function configFromStringAndFormat(config) { @@ -1208,6 +1463,8 @@ for (i = 0; i < tokens.length; i++) { token = tokens[i]; parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0]; + // console.log('token', token, 'parsedInput', parsedInput, + // 'regex', getParseRegexForToken(token, config)); if (parsedInput) { skipped = string.substr(0, string.indexOf(parsedInput)); if (skipped.length > 0) { @@ -1220,13 +1477,11 @@ if (formatTokenFunctions[token]) { if (parsedInput) { getParsingFlags(config).empty = false; - } - else { + } else { getParsingFlags(config).unusedTokens.push(token); } addTimeToArrayFromToken(token, parsedInput, config); - } - else if (config._strict && !parsedInput) { + } else if (config._strict && !parsedInput) { getParsingFlags(config).unusedTokens.push(token); } } @@ -1238,11 +1493,12 @@ } // clear _12h flag if hour is <= 12 - if (getParsingFlags(config).bigHour === true && - config._a[HOUR] <= 12 && - config._a[HOUR] > 0) { + if (getParsingFlags(config).bigHour === true && config._a[HOUR] <= 12 && config._a[HOUR] > 0) { getParsingFlags(config).bigHour = undefined; } + + getParsingFlags(config).parsedDateParts = config._a.slice(0); + getParsingFlags(config).meridiem = config._meridiem; // handle meridiem config._a[HOUR] = meridiemFixWrap(config._locale, config._a[HOUR], config._meridiem); @@ -1251,7 +1507,7 @@ } - function meridiemFixWrap (locale, hour, meridiem) { + function meridiemFixWrap(locale, hour, meridiem) { var isPm; if (meridiem == null) { @@ -1279,11 +1535,11 @@ // date from string and array of format strings function configFromStringAndArray(config) { var tempConfig, - bestMoment, + bestMoment, - scoreToBeat, - i, - currentScore; + scoreToBeat, + i, + currentScore; if (config._f.length === 0) { getParsingFlags(config).invalidFormat = true; @@ -1327,14 +1583,14 @@ } var i = normalizeObjectUnits(config._i); - config._a = map([i.year, i.month, i.day || i.date, i.hour, i.minute, i.second, i.millisecond], function (obj) { + config._a = map([i.year, i.month, i.day || i.date, i.hour, i.minute, i.second, i.millisecond], function(obj) { return obj && parseInt(obj, 10); }); configFromArray(config); } - function createFromConfig (config) { + function createFromConfig(config) { var res = new Moment(checkOverflow(prepareConfig(config))); if (res._nextDay) { // Adding is smart enough around DST @@ -1345,14 +1601,16 @@ return res; } - function prepareConfig (config) { + function prepareConfig(config) { var input = config._i, format = config._f; config._locale = config._locale || locale_locales__getLocale(config._l); if (input === null || (format === undefined && input === '')) { - return valid__createInvalid({nullInput: true}); + return valid__createInvalid({ + nullInput: true + }); } if (typeof input === 'string') { @@ -1383,11 +1641,11 @@ if (input === undefined) { config._d = new Date(utils_hooks__hooks.now()); } else if (isDate(input)) { - config._d = new Date(+input); + config._d = new Date(input.valueOf()); } else if (typeof input === 'string') { configFromString(config); } else if (isArray(input)) { - config._a = map(input.slice(0), function (obj) { + config._a = map(input.slice(0), function(obj) { return parseInt(obj, 10); }); configFromArray(config); @@ -1401,7 +1659,7 @@ } } - function createLocalOrUTC (input, format, locale, strict, isUTC) { + function createLocalOrUTC(input, format, locale, strict, isUTC) { var c = {}; if (typeof(locale) === 'boolean') { @@ -1420,33 +1678,31 @@ return createFromConfig(c); } - function local__createLocal (input, format, locale, strict) { + function local__createLocal(input, format, locale, strict) { return createLocalOrUTC(input, format, locale, strict, false); } - var prototypeMin = deprecate( - 'moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548', - function () { - var other = local__createLocal.apply(null, arguments); - if (this.isValid() && other.isValid()) { - return other < this ? this : other; - } else { - return valid__createInvalid(); - } - } - ); - - var prototypeMax = deprecate( - 'moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548', - function () { - var other = local__createLocal.apply(null, arguments); - if (this.isValid() && other.isValid()) { - return other > this ? this : other; - } else { - return valid__createInvalid(); - } + var prototypeMin = deprecate('moment().min is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548', + + function() { + var other = local__createLocal.apply(null, arguments); + if (this.isValid() && other.isValid()) { + return other < this ? this : other; + } else { + return valid__createInvalid(); + } + }); + + var prototypeMax = deprecate('moment().max is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548', + + function() { + var other = local__createLocal.apply(null, arguments); + if (this.isValid() && other.isValid()) { + return other > this ? this : other; + } else { + return valid__createInvalid(); } - ); + }); // Pick a moment m from moments so that m[fn](other) is true for all // other. This relies on the function fn to be transitive. @@ -1471,23 +1727,23 @@ } // TODO: Use [].sort instead? - function min () { + function min() { var args = [].slice.call(arguments, 0); return pickBy('isBefore', args); } - function max () { + function max() { var args = [].slice.call(arguments, 0); return pickBy('isAfter', args); } - var now = Date.now || function () { - return +(new Date()); + var now = function() { + return Date.now ? Date.now() : +(new Date()); }; - function Duration (duration) { + function Duration(duration) { var normalizedInput = normalizeObjectUnits(duration), years = normalizedInput.year || 0, quarters = normalizedInput.quarter || 0, @@ -1500,20 +1756,16 @@ milliseconds = normalizedInput.millisecond || 0; // representation for dateAddRemove - this._milliseconds = +milliseconds + - seconds * 1e3 + // 1000 - minutes * 6e4 + // 1000 * 60 - hours * 36e5; // 1000 * 60 * 60 + this._milliseconds = +milliseconds + seconds * 1e3 + // 1000 + minutes * 6e4 + // 1000 * 60 + hours * 1000 * 60 * 60; //using 1000 * 60 * 60 instead of 36e5 to avoid floating point rounding errors https://github.com/moment/moment/issues/2978 // Because of dateAddRemove treats 24 hours as different from a // day when working around DST, we need to store them separately - this._days = +days + - weeks * 7; + this._days = +days + weeks * 7; // It is impossible translate months into days without knowing // which months you are are talking about, so we have to store // it separately. - this._months = +months + - quarters * 3 + - years * 12; + this._months = +months + quarters * 3 + years * 12; this._data = {}; @@ -1522,21 +1774,21 @@ this._bubble(); } - function isDuration (obj) { + function isDuration(obj) { return obj instanceof Duration; } // FORMATTING - function offset (token, separator) { - addFormatToken(token, 0, 0, function () { + function offset(token, separator) { + addFormatToken(token, 0, 0, function() { var offset = this.utcOffset(); var sign = '+'; if (offset < 0) { offset = -offset; sign = '-'; } - return sign + zeroFill(~~(offset / 60), 2) + separator + zeroFill(~~(offset) % 60, 2); + return sign + zeroFill(~~ (offset / 60), 2) + separator + zeroFill(~~ (offset) % 60, 2); }); } @@ -1545,9 +1797,9 @@ // PARSING - addRegexToken('Z', matchShortOffset); + addRegexToken('Z', matchShortOffset); addRegexToken('ZZ', matchShortOffset); - addParseToken(['Z', 'ZZ'], function (input, array, config) { + addParseToken(['Z', 'ZZ'], function(input, array, config) { config._useUTC = true; config._tzm = offsetFromString(matchShortOffset, input); }); @@ -1561,8 +1813,8 @@ function offsetFromString(matcher, string) { var matches = ((string || '').match(matcher) || []); - var chunk = matches[matches.length - 1] || []; - var parts = (chunk + '').match(chunkOffset) || ['-', 0, 0]; + var chunk = matches[matches.length - 1] || []; + var parts = (chunk + '').match(chunkOffset) || ['-', 0, 0]; var minutes = +(parts[1] * 60) + toInt(parts[2]); return parts[0] === '+' ? minutes : -minutes; @@ -1573,9 +1825,9 @@ var res, diff; if (model._isUTC) { res = model.clone(); - diff = (isMoment(input) || isDate(input) ? +input : +local__createLocal(input)) - (+res); + diff = (isMoment(input) || isDate(input) ? input.valueOf() : local__createLocal(input).valueOf()) - res.valueOf(); // Use low-level api, because this fn is low-level api. - res._d.setTime(+res._d + diff); + res._d.setTime(res._d.valueOf() + diff); utils_hooks__hooks.updateOffset(res, false); return res; } else { @@ -1583,7 +1835,7 @@ } } - function getDateOffset (m) { + function getDateOffset(m) { // On Firefox.24 Date#getTimezoneOffset returns a floating point. // https://github.com/moment/moment/pull/1871 return -Math.round(m._d.getTimezoneOffset() / 15) * 15; @@ -1593,7 +1845,7 @@ // This function will be called whenever a moment is mutated. // It is intended to keep the offset in sync with the timezone. - utils_hooks__hooks.updateOffset = function () {}; + utils_hooks__hooks.updateOffset = function() {}; // MOMENTS @@ -1607,7 +1859,7 @@ // a second time. In case it wants us to change the offset again // _changeInProgress == true case, then we have to adjust, because // there is no such time in the given timezone. - function getSetOffset (input, keepLocalTime) { + function getSetOffset(input, keepLocalTime) { var offset = this._offset || 0, localAdjust; if (!this.isValid()) { @@ -1642,7 +1894,7 @@ } } - function getSetZone (input, keepLocalTime) { + function getSetZone(input, keepLocalTime) { if (input != null) { if (typeof input !== 'string') { input = -input; @@ -1656,11 +1908,11 @@ } } - function setOffsetToUTC (keepLocalTime) { + function setOffsetToUTC(keepLocalTime) { return this.utcOffset(0, keepLocalTime); } - function setOffsetToLocal (keepLocalTime) { + function setOffsetToLocal(keepLocalTime) { if (this._isUTC) { this.utcOffset(0, keepLocalTime); this._isUTC = false; @@ -1672,7 +1924,7 @@ return this; } - function setOffsetToParsedOffset () { + function setOffsetToParsedOffset() { if (this._tzm) { this.utcOffset(this._tzm); } else if (typeof this._i === 'string') { @@ -1681,7 +1933,7 @@ return this; } - function hasAlignedHourOffset (input) { + function hasAlignedHourOffset(input) { if (!this.isValid()) { return false; } @@ -1690,14 +1942,12 @@ return (this.utcOffset() - input) % 60 === 0; } - function isDaylightSavingTime () { + function isDaylightSavingTime() { return ( - this.utcOffset() > this.clone().month(0).utcOffset() || - this.utcOffset() > this.clone().month(5).utcOffset() - ); + this.utcOffset() > this.clone().month(0).utcOffset() || this.utcOffset() > this.clone().month(5).utcOffset()); } - function isDaylightSavingTimeShifted () { + function isDaylightSavingTimeShifted() { if (!isUndefined(this._isDSTShifted)) { return this._isDSTShifted; } @@ -1709,8 +1959,7 @@ if (c._a) { var other = c._isUTC ? create_utc__createUTC(c._a) : local__createLocal(c._a); - this._isDSTShifted = this.isValid() && - compareArrays(c._a, other.toArray()) > 0; + this._isDSTShifted = this.isValid() && compareArrays(c._a, other.toArray()) > 0; } else { this._isDSTShifted = false; } @@ -1718,26 +1967,27 @@ return this._isDSTShifted; } - function isLocal () { + function isLocal() { return this.isValid() ? !this._isUTC : false; } - function isUtcOffset () { + function isUtcOffset() { return this.isValid() ? this._isUTC : false; } - function isUtc () { + function isUtc() { return this.isValid() ? this._isUTC && this._offset === 0 : false; } // ASP.NET json date format regex - var aspNetRegex = /(\-)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/; + var aspNetRegex = /^(\-)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?\d*)?$/; // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere - var isoRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/; + // and further modified to allow for strings containing both week and day + var isoRegex = /^(-)?P(?:(-?[0-9,.]*)Y)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)W)?(?:(-?[0-9,.]*)D)?(?:T(?:(-?[0-9,.]*)H)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)S)?)?$/; - function create__createDuration (input, key) { + function create__createDuration(input, key) { var duration = input, // matching against regexp is expensive, do it on demand match = null, @@ -1747,9 +1997,9 @@ if (isDuration(input)) { duration = { - ms : input._milliseconds, - d : input._days, - M : input._months + ms: input._milliseconds, + d: input._days, + M: input._months }; } else if (typeof input === 'number') { duration = {}; @@ -1758,28 +2008,28 @@ } else { duration.milliseconds = input; } - } else if (!!(match = aspNetRegex.exec(input))) { + } else if ( !! (match = aspNetRegex.exec(input))) { sign = (match[1] === '-') ? -1 : 1; duration = { - y : 0, - d : toInt(match[DATE]) * sign, - h : toInt(match[HOUR]) * sign, - m : toInt(match[MINUTE]) * sign, - s : toInt(match[SECOND]) * sign, - ms : toInt(match[MILLISECOND]) * sign + y: 0, + d: toInt(match[DATE]) * sign, + h: toInt(match[HOUR]) * sign, + m: toInt(match[MINUTE]) * sign, + s: toInt(match[SECOND]) * sign, + ms: toInt(match[MILLISECOND]) * sign }; - } else if (!!(match = isoRegex.exec(input))) { + } else if ( !! (match = isoRegex.exec(input))) { sign = (match[1] === '-') ? -1 : 1; duration = { - y : parseIso(match[2], sign), - M : parseIso(match[3], sign), - d : parseIso(match[4], sign), - h : parseIso(match[5], sign), - m : parseIso(match[6], sign), - s : parseIso(match[7], sign), - w : parseIso(match[8], sign) + y: parseIso(match[2], sign), + M: parseIso(match[3], sign), + w: parseIso(match[4], sign), + d: parseIso(match[5], sign), + h: parseIso(match[6], sign), + m: parseIso(match[7], sign), + s: parseIso(match[8], sign) }; - } else if (duration == null) {// checks for null or undefined + } else if (duration == null) { // checks for null or undefined duration = {}; } else if (typeof duration === 'object' && ('from' in duration || 'to' in duration)) { diffRes = momentsDifference(local__createLocal(duration.from), local__createLocal(duration.to)); @@ -1800,7 +2050,7 @@ create__createDuration.fn = Duration.prototype; - function parseIso (inp, sign) { + function parseIso(inp, sign) { // We'd normally use ~~inp for this, but unfortunately it also // converts floats to ints. // inp may be undefined, so careful calling replace on it. @@ -1810,10 +2060,12 @@ } function positiveMomentsDifference(base, other) { - var res = {milliseconds: 0, months: 0}; + var res = { + milliseconds: 0, + months: 0 + }; - res.months = other.month() - base.month() + - (other.year() - base.year()) * 12; + res.months = other.month() - base.month() + (other.year() - base.year()) * 12; if (base.clone().add(res.months, 'M').isAfter(other)) { --res.months; } @@ -1826,7 +2078,10 @@ function momentsDifference(base, other) { var res; if (!(base.isValid() && other.isValid())) { - return {milliseconds: 0, months: 0}; + return { + milliseconds: 0, + months: 0 + }; } other = cloneWithOffset(other, base); @@ -1841,14 +2096,24 @@ return res; } + function absRound(number) { + if (number < 0) { + return Math.round(-1 * number) * -1; + } else { + return Math.round(number); + } + } + // TODO: remove 'name' arg after deprecation is removed function createAdder(direction, name) { - return function (val, period) { + return function(val, period) { var dur, tmp; //invert the arguments, but complain about it if (period !== null && !isNaN(+period)) { - deprecateSimple(name, 'moment().' + name + '(period, number) is deprecated. Please use moment().' + name + '(number, period).'); - tmp = val; val = period; period = tmp; + deprecateSimple(name, 'moment().' + name + '(period, number) is deprecated. Please use moment().' + name + '(number, period).'); + tmp = val; + val = period; + period = tmp; } val = typeof val === 'string' ? +val : val; @@ -1858,10 +2123,10 @@ }; } - function add_subtract__addSubtract (mom, duration, isAdding, updateOffset) { + function add_subtract__addSubtract(mom, duration, isAdding, updateOffset) { var milliseconds = duration._milliseconds, - days = duration._days, - months = duration._months; + days = absRound(duration._days), + months = absRound(duration._months); if (!mom.isValid()) { // No op @@ -1871,7 +2136,7 @@ updateOffset = updateOffset == null ? true : updateOffset; if (milliseconds) { - mom._d.setTime(+mom._d + milliseconds * isAdding); + mom._d.setTime(mom._d.valueOf() + milliseconds * isAdding); } if (days) { get_set__set(mom, 'Date', get_set__get(mom, 'Date') + days * isAdding); @@ -1884,62 +2149,58 @@ } } - var add_subtract__add = createAdder(1, 'add'); + var add_subtract__add = createAdder(1, 'add'); var add_subtract__subtract = createAdder(-1, 'subtract'); - function moment_calendar__calendar (time, formats) { + function moment_calendar__calendar(time, formats) { // We want to compare the start of today, vs this. // Getting start-of-today depends on whether we're local/utc/offset or not. var now = time || local__createLocal(), sod = cloneWithOffset(now, this).startOf('day'), diff = this.diff(sod, 'days', true), - format = diff < -6 ? 'sameElse' : - diff < -1 ? 'lastWeek' : - diff < 0 ? 'lastDay' : - diff < 1 ? 'sameDay' : - diff < 2 ? 'nextDay' : - diff < 7 ? 'nextWeek' : 'sameElse'; + format = diff < -6 ? 'sameElse' : diff < -1 ? 'lastWeek' : diff < 0 ? 'lastDay' : diff < 1 ? 'sameDay' : diff < 2 ? 'nextDay' : diff < 7 ? 'nextWeek' : 'sameElse'; var output = formats && (isFunction(formats[format]) ? formats[format]() : formats[format]); return this.format(output || this.localeData().calendar(format, this, local__createLocal(now))); } - function clone () { + function clone() { return new Moment(this); } - function isAfter (input, units) { + function isAfter(input, units) { var localInput = isMoment(input) ? input : local__createLocal(input); if (!(this.isValid() && localInput.isValid())) { return false; } units = normalizeUnits(!isUndefined(units) ? units : 'millisecond'); if (units === 'millisecond') { - return +this > +localInput; + return this.valueOf() > localInput.valueOf(); } else { - return +localInput < +this.clone().startOf(units); + return localInput.valueOf() < this.clone().startOf(units).valueOf(); } } - function isBefore (input, units) { + function isBefore(input, units) { var localInput = isMoment(input) ? input : local__createLocal(input); if (!(this.isValid() && localInput.isValid())) { return false; } units = normalizeUnits(!isUndefined(units) ? units : 'millisecond'); if (units === 'millisecond') { - return +this < +localInput; + return this.valueOf() < localInput.valueOf(); } else { - return +this.clone().endOf(units) < +localInput; + return this.clone().endOf(units).valueOf() < localInput.valueOf(); } } - function isBetween (from, to, units) { - return this.isAfter(from, units) && this.isBefore(to, units); + function isBetween(from, to, units, inclusivity) { + inclusivity = inclusivity || '()'; + return (inclusivity[0] === '(' ? this.isAfter(from, units) : !this.isBefore(from, units)) && (inclusivity[1] === ')' ? this.isBefore(to, units) : !this.isAfter(to, units)); } - function isSame (input, units) { + function isSame(input, units) { var localInput = isMoment(input) ? input : local__createLocal(input), inputMs; if (!(this.isValid() && localInput.isValid())) { @@ -1947,25 +2208,25 @@ } units = normalizeUnits(units || 'millisecond'); if (units === 'millisecond') { - return +this === +localInput; + return this.valueOf() === localInput.valueOf(); } else { - inputMs = +localInput; - return +(this.clone().startOf(units)) <= inputMs && inputMs <= +(this.clone().endOf(units)); + inputMs = localInput.valueOf(); + return this.clone().startOf(units).valueOf() <= inputMs && inputMs <= this.clone().endOf(units).valueOf(); } } - function isSameOrAfter (input, units) { - return this.isSame(input, units) || this.isAfter(input,units); + function isSameOrAfter(input, units) { + return this.isSame(input, units) || this.isAfter(input, units); } - function isSameOrBefore (input, units) { - return this.isSame(input, units) || this.isBefore(input,units); + function isSameOrBefore(input, units) { + return this.isSame(input, units) || this.isBefore(input, units); } - function diff (input, units, asFloat) { + function diff(input, units, asFloat) { var that, - zoneDelta, - delta, output; + zoneDelta, + delta, output; if (!this.isValid()) { return NaN; @@ -1991,16 +2252,16 @@ } else { delta = this - that; output = units === 'second' ? delta / 1e3 : // 1000 - units === 'minute' ? delta / 6e4 : // 1000 * 60 - units === 'hour' ? delta / 36e5 : // 1000 * 60 * 60 - units === 'day' ? (delta - zoneDelta) / 864e5 : // 1000 * 60 * 60 * 24, negate dst - units === 'week' ? (delta - zoneDelta) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst - delta; + units === 'minute' ? delta / 6e4 : // 1000 * 60 + units === 'hour' ? delta / 36e5 : // 1000 * 60 * 60 + units === 'day' ? (delta - zoneDelta) / 864e5 : // 1000 * 60 * 60 * 24, negate dst + units === 'week' ? (delta - zoneDelta) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst + delta; } return asFloat ? output : absFloor(output); } - function monthDiff (a, b) { + function monthDiff(a, b) { // difference in months var wholeMonthDiff = ((b.year() - a.year()) * 12) + (b.month() - a.month()), // b is in (anchor - 1 month, anchor + 1 month) @@ -2017,16 +2278,18 @@ adjust = (b - anchor) / (anchor2 - anchor); } - return -(wholeMonthDiff + adjust); + //check for negative zero, return zero if negative zero + return -(wholeMonthDiff + adjust) || 0; } utils_hooks__hooks.defaultFormat = 'YYYY-MM-DDTHH:mm:ssZ'; + utils_hooks__hooks.defaultFormatUtc = 'YYYY-MM-DDTHH:mm:ss[Z]'; - function toString () { + function toString() { return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); } - function moment_format__toISOString () { + function moment_format__toISOString() { var m = this.clone().utc(); if (0 < m.year() && m.year() <= 9999) { if (isFunction(Date.prototype.toISOString)) { @@ -2040,43 +2303,48 @@ } } - function format (inputString) { - var output = formatMoment(this, inputString || utils_hooks__hooks.defaultFormat); + function format(inputString) { + if (!inputString) { + inputString = this.isUtc() ? utils_hooks__hooks.defaultFormatUtc : utils_hooks__hooks.defaultFormat; + } + var output = formatMoment(this, inputString); return this.localeData().postformat(output); } - function from (time, withoutSuffix) { - if (this.isValid() && - ((isMoment(time) && time.isValid()) || - local__createLocal(time).isValid())) { - return create__createDuration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix); + function from(time, withoutSuffix) { + if (this.isValid() && ((isMoment(time) && time.isValid()) || local__createLocal(time).isValid())) { + return create__createDuration({ + to: this, + from: time + }).locale(this.locale()).humanize(!withoutSuffix); } else { return this.localeData().invalidDate(); } } - function fromNow (withoutSuffix) { + function fromNow(withoutSuffix) { return this.from(local__createLocal(), withoutSuffix); } - function to (time, withoutSuffix) { - if (this.isValid() && - ((isMoment(time) && time.isValid()) || - local__createLocal(time).isValid())) { - return create__createDuration({from: this, to: time}).locale(this.locale()).humanize(!withoutSuffix); + function to(time, withoutSuffix) { + if (this.isValid() && ((isMoment(time) && time.isValid()) || local__createLocal(time).isValid())) { + return create__createDuration({ + from: this, + to: time + }).locale(this.locale()).humanize(!withoutSuffix); } else { return this.localeData().invalidDate(); } } - function toNow (withoutSuffix) { + function toNow(withoutSuffix) { return this.to(local__createLocal(), withoutSuffix); } // If passed a locale key, it will set the locale for this // instance. Otherwise, it will return the locale configuration // variables for this instance. - function locale (key) { + function locale(key) { var newLocaleData; if (key === undefined) { @@ -2090,46 +2358,46 @@ } } - var lang = deprecate( - 'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.', - function (key) { - if (key === undefined) { - return this.localeData(); - } else { - return this.locale(key); - } + var lang = deprecate('moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.', + + function(key) { + if (key === undefined) { + return this.localeData(); + } else { + return this.locale(key); } - ); + }); - function localeData () { + function localeData() { return this._locale; } - function startOf (units) { + function startOf(units) { units = normalizeUnits(units); // the following switch intentionally omits break keywords // to utilize falling through the cases. switch (units) { - case 'year': - this.month(0); - /* falls through */ - case 'quarter': - case 'month': - this.date(1); - /* falls through */ - case 'week': - case 'isoWeek': - case 'day': - this.hours(0); - /* falls through */ - case 'hour': - this.minutes(0); - /* falls through */ - case 'minute': - this.seconds(0); - /* falls through */ - case 'second': - this.milliseconds(0); + case 'year': + this.month(0); + /* falls through */ + case 'quarter': + case 'month': + this.date(1); + /* falls through */ + case 'week': + case 'isoWeek': + case 'day': + case 'date': + this.hours(0); + /* falls through */ + case 'hour': + this.minutes(0); + /* falls through */ + case 'minute': + this.seconds(0); + /* falls through */ + case 'second': + this.milliseconds(0); } // weeks are a special case @@ -2148,32 +2416,38 @@ return this; } - function endOf (units) { + function endOf(units) { units = normalizeUnits(units); if (units === undefined || units === 'millisecond') { return this; } + + // 'date' is an alias for 'day', so it should be considered as such. + if (units === 'date') { + units = 'day'; + } + return this.startOf(units).add(1, (units === 'isoWeek' ? 'week' : units)).subtract(1, 'ms'); } - function to_type__valueOf () { - return +this._d - ((this._offset || 0) * 60000); + function to_type__valueOf() { + return this._d.valueOf() - ((this._offset || 0) * 60000); } - function unix () { - return Math.floor(+this / 1000); + function unix() { + return Math.floor(this.valueOf() / 1000); } - function toDate () { - return this._offset ? new Date(+this) : this._d; + function toDate() { + return this._offset ? new Date(this.valueOf()) : this._d; } - function toArray () { + function toArray() { var m = this; return [m.year(), m.month(), m.date(), m.hour(), m.minute(), m.second(), m.millisecond()]; } - function toObject () { + function toObject() { var m = this; return { years: m.year(), @@ -2186,20 +2460,20 @@ }; } - function toJSON () { - // JSON.stringify(new Date(NaN)) === 'null' - return this.isValid() ? this.toISOString() : 'null'; + function toJSON() { + // new Date(NaN).toJSON() === null + return this.isValid() ? this.toISOString() : null; } - function moment_valid__isValid () { + function moment_valid__isValid() { return valid__isValid(this); } - function parsingFlags () { + function parsingFlags() { return extend({}, getParsingFlags(this)); } - function invalidAt () { + function invalidAt() { return getParsingFlags(this).overflow; } @@ -2215,21 +2489,21 @@ // FORMATTING - addFormatToken(0, ['gg', 2], 0, function () { + addFormatToken(0, ['gg', 2], 0, function() { return this.weekYear() % 100; }); - addFormatToken(0, ['GG', 2], 0, function () { + addFormatToken(0, ['GG', 2], 0, function() { return this.isoWeekYear() % 100; }); - function addWeekYearFormatToken (token, getter) { + function addWeekYearFormatToken(token, getter) { addFormatToken(0, [token, token.length], 0, getter); } - addWeekYearFormatToken('gggg', 'weekYear'); - addWeekYearFormatToken('ggggg', 'weekYear'); - addWeekYearFormatToken('GGGG', 'isoWeekYear'); + addWeekYearFormatToken('gggg', 'weekYear'); + addWeekYearFormatToken('ggggg', 'weekYear'); + addWeekYearFormatToken('GGGG', 'isoWeekYear'); addWeekYearFormatToken('GGGGG', 'isoWeekYear'); // ALIASES @@ -2239,44 +2513,44 @@ // PARSING - addRegexToken('G', matchSigned); - addRegexToken('g', matchSigned); - addRegexToken('GG', match1to2, match2); - addRegexToken('gg', match1to2, match2); - addRegexToken('GGGG', match1to4, match4); - addRegexToken('gggg', match1to4, match4); - addRegexToken('GGGGG', match1to6, match6); - addRegexToken('ggggg', match1to6, match6); + addRegexToken('G', matchSigned); + addRegexToken('g', matchSigned); + addRegexToken('GG', match1to2, match2); + addRegexToken('gg', match1to2, match2); + addRegexToken('GGGG', match1to4, match4); + addRegexToken('gggg', match1to4, match4); + addRegexToken('GGGGG', match1to6, match6); + addRegexToken('ggggg', match1to6, match6); - addWeekParseToken(['gggg', 'ggggg', 'GGGG', 'GGGGG'], function (input, week, config, token) { + addWeekParseToken(['gggg', 'ggggg', 'GGGG', 'GGGGG'], function(input, week, config, token) { week[token.substr(0, 2)] = toInt(input); }); - addWeekParseToken(['gg', 'GG'], function (input, week, config, token) { + addWeekParseToken(['gg', 'GG'], function(input, week, config, token) { week[token] = utils_hooks__hooks.parseTwoDigitYear(input); }); // MOMENTS - function getSetWeekYear (input) { + function getSetWeekYear(input) { return getSetWeekYearHelper.call(this, - input, - this.week(), - this.weekday(), - this.localeData()._week.dow, - this.localeData()._week.doy); + input, + this.week(), + this.weekday(), + this.localeData()._week.dow, + this.localeData()._week.doy); } - function getSetISOWeekYear (input) { + function getSetISOWeekYear(input) { return getSetWeekYearHelper.call(this, - input, this.isoWeek(), this.isoWeekday(), 1, 4); + input, this.isoWeek(), this.isoWeekday(), 1, 4); } - function getISOWeeksInYear () { + function getISOWeeksInYear() { return weeksInYear(this.year(), 1, 4); } - function getWeeksInYear () { + function getWeeksInYear() { var weekInfo = this.localeData()._week; return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); } @@ -2298,7 +2572,6 @@ var dayOfYearData = dayOfYearFromWeeks(weekYear, week, weekday, dow, doy), date = createUTCDate(dayOfYearData.year, 0, dayOfYearData.dayOfYear); - // console.log("got", weekYear, week, weekday, "set", date.toISOString()); this.year(date.getUTCFullYear()); this.month(date.getUTCMonth()); this.date(date.getUTCDate()); @@ -2316,13 +2589,13 @@ // PARSING addRegexToken('Q', match1); - addParseToken('Q', function (input, array) { + addParseToken('Q', function(input, array) { array[MONTH] = (toInt(input) - 1) * 3; }); // MOMENTS - function getSetQuarter (input) { + function getSetQuarter(input) { return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3); } @@ -2338,12 +2611,12 @@ // PARSING - addRegexToken('w', match1to2); + addRegexToken('w', match1to2); addRegexToken('ww', match1to2, match2); - addRegexToken('W', match1to2); + addRegexToken('W', match1to2); addRegexToken('WW', match1to2, match2); - addWeekParseToken(['w', 'ww', 'W', 'WW'], function (input, week, config, token) { + addWeekParseToken(['w', 'ww', 'W', 'WW'], function(input, week, config, token) { week[token.substr(0, 1)] = toInt(input); }); @@ -2351,31 +2624,31 @@ // LOCALES - function localeWeek (mom) { + function localeWeek(mom) { return weekOfYear(mom, this._week.dow, this._week.doy).week; } var defaultLocaleWeek = { - dow : 0, // Sunday is the first day of the week. - doy : 6 // The week that contains Jan 1st is the first week of the year. + dow: 0, // Sunday is the first day of the week. + doy: 6 // The week that contains Jan 1st is the first week of the year. }; - function localeFirstDayOfWeek () { + function localeFirstDayOfWeek() { return this._week.dow; } - function localeFirstDayOfYear () { + function localeFirstDayOfYear() { return this._week.doy; } // MOMENTS - function getSetWeek (input) { + function getSetWeek(input) { var week = this.localeData().week(this); return input == null ? week : this.add((input - week) * 7, 'd'); } - function getSetISOWeek (input) { + function getSetISOWeek(input) { var week = weekOfYear(this, 1, 4).week; return input == null ? week : this.add((input - week) * 7, 'd'); } @@ -2390,14 +2663,14 @@ // PARSING - addRegexToken('D', match1to2); + addRegexToken('D', match1to2); addRegexToken('DD', match1to2, match2); - addRegexToken('Do', function (isStrict, locale) { + addRegexToken('Do', function(isStrict, locale) { return isStrict ? locale._ordinalParse : locale._ordinalParseLenient; }); addParseToken(['D', 'DD'], DATE); - addParseToken('Do', function (input, array) { + addParseToken('Do', function(input, array) { array[DATE] = toInt(input.match(match1to2)[0], 10); }); @@ -2409,15 +2682,15 @@ addFormatToken('d', 0, 'do', 'day'); - addFormatToken('dd', 0, 0, function (format) { + addFormatToken('dd', 0, 0, function(format) { return this.localeData().weekdaysMin(this, format); }); - addFormatToken('ddd', 0, 0, function (format) { + addFormatToken('ddd', 0, 0, function(format) { return this.localeData().weekdaysShort(this, format); }); - addFormatToken('dddd', 0, 0, function (format) { + addFormatToken('dddd', 0, 0, function(format) { return this.localeData().weekdays(this, format); }); @@ -2432,14 +2705,20 @@ // PARSING - addRegexToken('d', match1to2); - addRegexToken('e', match1to2); - addRegexToken('E', match1to2); - addRegexToken('dd', matchWord); - addRegexToken('ddd', matchWord); - addRegexToken('dddd', matchWord); + addRegexToken('d', match1to2); + addRegexToken('e', match1to2); + addRegexToken('E', match1to2); + addRegexToken('dd', function(isStrict, locale) { + return locale.weekdaysMinRegex(isStrict); + }); + addRegexToken('ddd', function(isStrict, locale) { + return locale.weekdaysShortRegex(isStrict); + }); + addRegexToken('dddd', function(isStrict, locale) { + return locale.weekdaysRegex(isStrict); + }); - addWeekParseToken(['dd', 'ddd', 'dddd'], function (input, week, config, token) { + addWeekParseToken(['dd', 'ddd', 'dddd'], function(input, week, config, token) { var weekday = config._locale.weekdaysParse(input, token, config._strict); // if we didn't get a weekday name, mark the date as invalid if (weekday != null) { @@ -2449,7 +2728,7 @@ } }); - addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) { + addWeekParseToken(['d', 'e', 'E'], function(input, week, config, token) { week[token] = toInt(input); }); @@ -2475,24 +2754,94 @@ // LOCALES var defaultLocaleWeekdays = 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'); - function localeWeekdays (m, format) { - return isArray(this._weekdays) ? this._weekdays[m.day()] : - this._weekdays[this._weekdays.isFormat.test(format) ? 'format' : 'standalone'][m.day()]; + + function localeWeekdays(m, format) { + return isArray(this._weekdays) ? this._weekdays[m.day()] : this._weekdays[this._weekdays.isFormat.test(format) ? 'format' : 'standalone'][m.day()]; } var defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'); - function localeWeekdaysShort (m) { + + function localeWeekdaysShort(m) { return this._weekdaysShort[m.day()]; } var defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'); - function localeWeekdaysMin (m) { + + function localeWeekdaysMin(m) { return this._weekdaysMin[m.day()]; } - function localeWeekdaysParse (weekdayName, format, strict) { + function day_of_week__handleStrictParse(weekdayName, format, strict) { + var i, ii, mom, llc = weekdayName.toLocaleLowerCase(); + if (!this._weekdaysParse) { + this._weekdaysParse = []; + this._shortWeekdaysParse = []; + this._minWeekdaysParse = []; + + for (i = 0; i < 7; ++i) { + mom = create_utc__createUTC([2000, 1]).day(i); + this._minWeekdaysParse[i] = this.weekdaysMin(mom, '').toLocaleLowerCase(); + this._shortWeekdaysParse[i] = this.weekdaysShort(mom, '').toLocaleLowerCase(); + this._weekdaysParse[i] = this.weekdays(mom, '').toLocaleLowerCase(); + } + } + + if (strict) { + if (format === 'dddd') { + ii = indexOf.call(this._weekdaysParse, llc); + return ii !== -1 ? ii : null; + } else if (format === 'ddd') { + ii = indexOf.call(this._shortWeekdaysParse, llc); + return ii !== -1 ? ii : null; + } else { + ii = indexOf.call(this._minWeekdaysParse, llc); + return ii !== -1 ? ii : null; + } + } else { + if (format === 'dddd') { + ii = indexOf.call(this._weekdaysParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._shortWeekdaysParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._minWeekdaysParse, llc); + return ii !== -1 ? ii : null; + } else if (format === 'ddd') { + ii = indexOf.call(this._shortWeekdaysParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._weekdaysParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._minWeekdaysParse, llc); + return ii !== -1 ? ii : null; + } else { + ii = indexOf.call(this._minWeekdaysParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._weekdaysParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._shortWeekdaysParse, llc); + return ii !== -1 ? ii : null; + } + } + } + + function localeWeekdaysParse(weekdayName, format, strict) { var i, mom, regex; + if (this._weekdaysParseExact) { + return day_of_week__handleStrictParse.call(this, weekdayName, format, strict); + } + if (!this._weekdaysParse) { this._weekdaysParse = []; this._minWeekdaysParse = []; @@ -2503,7 +2852,7 @@ for (i = 0; i < 7; i++) { // make the regex if we don't have it already - mom = local__createLocal([2000, 1]).day(i); + mom = create_utc__createUTC([2000, 1]).day(i); if (strict && !this._fullWeekdaysParse[i]) { this._fullWeekdaysParse[i] = new RegExp('^' + this.weekdays(mom, '').replace('.', '\.?') + '$', 'i'); this._shortWeekdaysParse[i] = new RegExp('^' + this.weekdaysShort(mom, '').replace('.', '\.?') + '$', 'i'); @@ -2528,7 +2877,7 @@ // MOMENTS - function getSetDayOfWeek (input) { + function getSetDayOfWeek(input) { if (!this.isValid()) { return input != null ? this : NaN; } @@ -2541,7 +2890,7 @@ } } - function getSetLocaleDayOfWeek (input) { + function getSetLocaleDayOfWeek(input) { if (!this.isValid()) { return input != null ? this : NaN; } @@ -2549,7 +2898,7 @@ return input == null ? weekday : this.add(input - weekday, 'd'); } - function getSetISODayOfWeek (input) { + function getSetISODayOfWeek(input) { if (!this.isValid()) { return input != null ? this : NaN; } @@ -2559,6 +2908,102 @@ return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7); } + var defaultWeekdaysRegex = matchWord; + + function weekdaysRegex(isStrict) { + if (this._weekdaysParseExact) { + if (!hasOwnProp(this, '_weekdaysRegex')) { + computeWeekdaysParse.call(this); + } + if (isStrict) { + return this._weekdaysStrictRegex; + } else { + return this._weekdaysRegex; + } + } else { + return this._weekdaysStrictRegex && isStrict ? this._weekdaysStrictRegex : this._weekdaysRegex; + } + } + + var defaultWeekdaysShortRegex = matchWord; + + function weekdaysShortRegex(isStrict) { + if (this._weekdaysParseExact) { + if (!hasOwnProp(this, '_weekdaysRegex')) { + computeWeekdaysParse.call(this); + } + if (isStrict) { + return this._weekdaysShortStrictRegex; + } else { + return this._weekdaysShortRegex; + } + } else { + return this._weekdaysShortStrictRegex && isStrict ? this._weekdaysShortStrictRegex : this._weekdaysShortRegex; + } + } + + var defaultWeekdaysMinRegex = matchWord; + + function weekdaysMinRegex(isStrict) { + if (this._weekdaysParseExact) { + if (!hasOwnProp(this, '_weekdaysRegex')) { + computeWeekdaysParse.call(this); + } + if (isStrict) { + return this._weekdaysMinStrictRegex; + } else { + return this._weekdaysMinRegex; + } + } else { + return this._weekdaysMinStrictRegex && isStrict ? this._weekdaysMinStrictRegex : this._weekdaysMinRegex; + } + } + + + function computeWeekdaysParse() { + function cmpLenRev(a, b) { + return b.length - a.length; + } + + var minPieces = [], + shortPieces = [], + longPieces = [], + mixedPieces = [], + i, mom, minp, shortp, longp; + for (i = 0; i < 7; i++) { + // make the regex if we don't have it already + mom = create_utc__createUTC([2000, 1]).day(i); + minp = this.weekdaysMin(mom, ''); + shortp = this.weekdaysShort(mom, ''); + longp = this.weekdays(mom, ''); + minPieces.push(minp); + shortPieces.push(shortp); + longPieces.push(longp); + mixedPieces.push(minp); + mixedPieces.push(shortp); + mixedPieces.push(longp); + } + // Sorting makes sure if one weekday (or abbr) is a prefix of another it + // will match the longer piece. + minPieces.sort(cmpLenRev); + shortPieces.sort(cmpLenRev); + longPieces.sort(cmpLenRev); + mixedPieces.sort(cmpLenRev); + for (i = 0; i < 7; i++) { + shortPieces[i] = regexEscape(shortPieces[i]); + longPieces[i] = regexEscape(longPieces[i]); + mixedPieces[i] = regexEscape(mixedPieces[i]); + } + + this._weekdaysRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i'); + this._weekdaysShortRegex = this._weekdaysRegex; + this._weekdaysMinRegex = this._weekdaysRegex; + + this._weekdaysStrictRegex = new RegExp('^(' + longPieces.join('|') + ')', 'i'); + this._weekdaysShortStrictRegex = new RegExp('^(' + shortPieces.join('|') + ')', 'i'); + this._weekdaysMinStrictRegex = new RegExp('^(' + minPieces.join('|') + ')', 'i'); + } + // FORMATTING addFormatToken('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear'); @@ -2569,9 +3014,9 @@ // PARSING - addRegexToken('DDD', match1to3); + addRegexToken('DDD', match1to3); addRegexToken('DDDD', match3); - addParseToken(['DDD', 'DDDD'], function (input, array, config) { + addParseToken(['DDD', 'DDDD'], function(input, array, config) { config._dayOfYear = toInt(input); }); @@ -2579,7 +3024,7 @@ // MOMENTS - function getSetDayOfYear (input) { + function getSetDayOfYear(input) { var dayOfYear = Math.round((this.clone().startOf('day') - this.clone().startOf('year')) / 864e5) + 1; return input == null ? dayOfYear : this.add((input - dayOfYear), 'd'); } @@ -2590,29 +3035,32 @@ return this.hours() % 12 || 12; } + function kFormat() { + return this.hours() || 24; + } + addFormatToken('H', ['HH', 2], 0, 'hour'); addFormatToken('h', ['hh', 2], 0, hFormat); + addFormatToken('k', ['kk', 2], 0, kFormat); - addFormatToken('hmm', 0, 0, function () { + addFormatToken('hmm', 0, 0, function() { return '' + hFormat.apply(this) + zeroFill(this.minutes(), 2); }); - addFormatToken('hmmss', 0, 0, function () { - return '' + hFormat.apply(this) + zeroFill(this.minutes(), 2) + - zeroFill(this.seconds(), 2); + addFormatToken('hmmss', 0, 0, function() { + return '' + hFormat.apply(this) + zeroFill(this.minutes(), 2) + zeroFill(this.seconds(), 2); }); - addFormatToken('Hmm', 0, 0, function () { + addFormatToken('Hmm', 0, 0, function() { return '' + this.hours() + zeroFill(this.minutes(), 2); }); - addFormatToken('Hmmss', 0, 0, function () { - return '' + this.hours() + zeroFill(this.minutes(), 2) + - zeroFill(this.seconds(), 2); + addFormatToken('Hmmss', 0, 0, function() { + return '' + this.hours() + zeroFill(this.minutes(), 2) + zeroFill(this.seconds(), 2); }); - function meridiem (token, lowercase) { - addFormatToken(token, 0, 0, function () { + function meridiem(token, lowercase) { + addFormatToken(token, 0, 0, function() { return this.localeData().meridiem(this.hours(), this.minutes(), lowercase); }); } @@ -2626,14 +3074,14 @@ // PARSING - function matchMeridiem (isStrict, locale) { + function matchMeridiem(isStrict, locale) { return locale._meridiemParse; } - addRegexToken('a', matchMeridiem); - addRegexToken('A', matchMeridiem); - addRegexToken('H', match1to2); - addRegexToken('h', match1to2); + addRegexToken('a', matchMeridiem); + addRegexToken('A', matchMeridiem); + addRegexToken('H', match1to2); + addRegexToken('h', match1to2); addRegexToken('HH', match1to2, match2); addRegexToken('hh', match1to2, match2); @@ -2643,21 +3091,21 @@ addRegexToken('Hmmss', match5to6); addParseToken(['H', 'HH'], HOUR); - addParseToken(['a', 'A'], function (input, array, config) { + addParseToken(['a', 'A'], function(input, array, config) { config._isPm = config._locale.isPM(input); config._meridiem = input; }); - addParseToken(['h', 'hh'], function (input, array, config) { + addParseToken(['h', 'hh'], function(input, array, config) { array[HOUR] = toInt(input); getParsingFlags(config).bigHour = true; }); - addParseToken('hmm', function (input, array, config) { + addParseToken('hmm', function(input, array, config) { var pos = input.length - 2; array[HOUR] = toInt(input.substr(0, pos)); array[MINUTE] = toInt(input.substr(pos)); getParsingFlags(config).bigHour = true; }); - addParseToken('hmmss', function (input, array, config) { + addParseToken('hmmss', function(input, array, config) { var pos1 = input.length - 4; var pos2 = input.length - 2; array[HOUR] = toInt(input.substr(0, pos1)); @@ -2665,12 +3113,12 @@ array[SECOND] = toInt(input.substr(pos2)); getParsingFlags(config).bigHour = true; }); - addParseToken('Hmm', function (input, array, config) { + addParseToken('Hmm', function(input, array, config) { var pos = input.length - 2; array[HOUR] = toInt(input.substr(0, pos)); array[MINUTE] = toInt(input.substr(pos)); }); - addParseToken('Hmmss', function (input, array, config) { + addParseToken('Hmmss', function(input, array, config) { var pos1 = input.length - 4; var pos2 = input.length - 2; array[HOUR] = toInt(input.substr(0, pos1)); @@ -2680,14 +3128,15 @@ // LOCALES - function localeIsPM (input) { + function localeIsPM(input) { // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays // Using charAt should be more compatible. return ((input + '').toLowerCase().charAt(0) === 'p'); } var defaultLocaleMeridiemParse = /[ap]\.?m?\.?/i; - function localeMeridiem (hours, minutes, isLower) { + + function localeMeridiem(hours, minutes, isLower) { if (hours > 11) { return isLower ? 'pm' : 'PM'; } else { @@ -2714,7 +3163,7 @@ // PARSING - addRegexToken('m', match1to2); + addRegexToken('m', match1to2); addRegexToken('mm', match1to2, match2); addParseToken(['m', 'mm'], MINUTE); @@ -2732,7 +3181,7 @@ // PARSING - addRegexToken('s', match1to2); + addRegexToken('s', match1to2); addRegexToken('ss', match1to2, match2); addParseToken(['s', 'ss'], SECOND); @@ -2742,31 +3191,31 @@ // FORMATTING - addFormatToken('S', 0, 0, function () { - return ~~(this.millisecond() / 100); + addFormatToken('S', 0, 0, function() { + return~~ (this.millisecond() / 100); }); - addFormatToken(0, ['SS', 2], 0, function () { - return ~~(this.millisecond() / 10); + addFormatToken(0, ['SS', 2], 0, function() { + return~~ (this.millisecond() / 10); }); addFormatToken(0, ['SSS', 3], 0, 'millisecond'); - addFormatToken(0, ['SSSS', 4], 0, function () { + addFormatToken(0, ['SSSS', 4], 0, function() { return this.millisecond() * 10; }); - addFormatToken(0, ['SSSSS', 5], 0, function () { + addFormatToken(0, ['SSSSS', 5], 0, function() { return this.millisecond() * 100; }); - addFormatToken(0, ['SSSSSS', 6], 0, function () { + addFormatToken(0, ['SSSSSS', 6], 0, function() { return this.millisecond() * 1000; }); - addFormatToken(0, ['SSSSSSS', 7], 0, function () { + addFormatToken(0, ['SSSSSSS', 7], 0, function() { return this.millisecond() * 10000; }); - addFormatToken(0, ['SSSSSSSS', 8], 0, function () { + addFormatToken(0, ['SSSSSSSS', 8], 0, function() { return this.millisecond() * 100000; }); - addFormatToken(0, ['SSSSSSSSS', 9], 0, function () { + addFormatToken(0, ['SSSSSSSSS', 9], 0, function() { return this.millisecond() * 1000000; }); @@ -2777,9 +3226,9 @@ // PARSING - addRegexToken('S', match1to3, match1); - addRegexToken('SS', match1to3, match2); - addRegexToken('SSS', match1to3, match3); + addRegexToken('S', match1to3, match1); + addRegexToken('SS', match1to3, match2); + addRegexToken('SSS', match1to3, match3); var token; for (token = 'SSSS'; token.length <= 9; token += 'S') { @@ -2799,86 +3248,86 @@ // FORMATTING - addFormatToken('z', 0, 0, 'zoneAbbr'); + addFormatToken('z', 0, 0, 'zoneAbbr'); addFormatToken('zz', 0, 0, 'zoneName'); // MOMENTS - function getZoneAbbr () { + function getZoneAbbr() { return this._isUTC ? 'UTC' : ''; } - function getZoneName () { + function getZoneName() { return this._isUTC ? 'Coordinated Universal Time' : ''; } var momentPrototype__proto = Moment.prototype; - momentPrototype__proto.add = add_subtract__add; - momentPrototype__proto.calendar = moment_calendar__calendar; - momentPrototype__proto.clone = clone; - momentPrototype__proto.diff = diff; - momentPrototype__proto.endOf = endOf; - momentPrototype__proto.format = format; - momentPrototype__proto.from = from; - momentPrototype__proto.fromNow = fromNow; - momentPrototype__proto.to = to; - momentPrototype__proto.toNow = toNow; - momentPrototype__proto.get = getSet; - momentPrototype__proto.invalidAt = invalidAt; - momentPrototype__proto.isAfter = isAfter; - momentPrototype__proto.isBefore = isBefore; - momentPrototype__proto.isBetween = isBetween; - momentPrototype__proto.isSame = isSame; - momentPrototype__proto.isSameOrAfter = isSameOrAfter; - momentPrototype__proto.isSameOrBefore = isSameOrBefore; - momentPrototype__proto.isValid = moment_valid__isValid; - momentPrototype__proto.lang = lang; - momentPrototype__proto.locale = locale; - momentPrototype__proto.localeData = localeData; - momentPrototype__proto.max = prototypeMax; - momentPrototype__proto.min = prototypeMin; - momentPrototype__proto.parsingFlags = parsingFlags; - momentPrototype__proto.set = getSet; - momentPrototype__proto.startOf = startOf; - momentPrototype__proto.subtract = add_subtract__subtract; - momentPrototype__proto.toArray = toArray; - momentPrototype__proto.toObject = toObject; - momentPrototype__proto.toDate = toDate; - momentPrototype__proto.toISOString = moment_format__toISOString; - momentPrototype__proto.toJSON = toJSON; - momentPrototype__proto.toString = toString; - momentPrototype__proto.unix = unix; - momentPrototype__proto.valueOf = to_type__valueOf; - momentPrototype__proto.creationData = creationData; + momentPrototype__proto.add = add_subtract__add; + momentPrototype__proto.calendar = moment_calendar__calendar; + momentPrototype__proto.clone = clone; + momentPrototype__proto.diff = diff; + momentPrototype__proto.endOf = endOf; + momentPrototype__proto.format = format; + momentPrototype__proto.from = from; + momentPrototype__proto.fromNow = fromNow; + momentPrototype__proto.to = to; + momentPrototype__proto.toNow = toNow; + momentPrototype__proto.get = getSet; + momentPrototype__proto.invalidAt = invalidAt; + momentPrototype__proto.isAfter = isAfter; + momentPrototype__proto.isBefore = isBefore; + momentPrototype__proto.isBetween = isBetween; + momentPrototype__proto.isSame = isSame; + momentPrototype__proto.isSameOrAfter = isSameOrAfter; + momentPrototype__proto.isSameOrBefore = isSameOrBefore; + momentPrototype__proto.isValid = moment_valid__isValid; + momentPrototype__proto.lang = lang; + momentPrototype__proto.locale = locale; + momentPrototype__proto.localeData = localeData; + momentPrototype__proto.max = prototypeMax; + momentPrototype__proto.min = prototypeMin; + momentPrototype__proto.parsingFlags = parsingFlags; + momentPrototype__proto.set = getSet; + momentPrototype__proto.startOf = startOf; + momentPrototype__proto.subtract = add_subtract__subtract; + momentPrototype__proto.toArray = toArray; + momentPrototype__proto.toObject = toObject; + momentPrototype__proto.toDate = toDate; + momentPrototype__proto.toISOString = moment_format__toISOString; + momentPrototype__proto.toJSON = toJSON; + momentPrototype__proto.toString = toString; + momentPrototype__proto.unix = unix; + momentPrototype__proto.valueOf = to_type__valueOf; + momentPrototype__proto.creationData = creationData; // Year - momentPrototype__proto.year = getSetYear; + momentPrototype__proto.year = getSetYear; momentPrototype__proto.isLeapYear = getIsLeapYear; // Week Year - momentPrototype__proto.weekYear = getSetWeekYear; + momentPrototype__proto.weekYear = getSetWeekYear; momentPrototype__proto.isoWeekYear = getSetISOWeekYear; // Quarter momentPrototype__proto.quarter = momentPrototype__proto.quarters = getSetQuarter; // Month - momentPrototype__proto.month = getSetMonth; + momentPrototype__proto.month = getSetMonth; momentPrototype__proto.daysInMonth = getDaysInMonth; // Week - momentPrototype__proto.week = momentPrototype__proto.weeks = getSetWeek; - momentPrototype__proto.isoWeek = momentPrototype__proto.isoWeeks = getSetISOWeek; - momentPrototype__proto.weeksInYear = getWeeksInYear; + momentPrototype__proto.week = momentPrototype__proto.weeks = getSetWeek; + momentPrototype__proto.isoWeek = momentPrototype__proto.isoWeeks = getSetISOWeek; + momentPrototype__proto.weeksInYear = getWeeksInYear; momentPrototype__proto.isoWeeksInYear = getISOWeeksInYear; // Day - momentPrototype__proto.date = getSetDayOfMonth; - momentPrototype__proto.day = momentPrototype__proto.days = getSetDayOfWeek; - momentPrototype__proto.weekday = getSetLocaleDayOfWeek; + momentPrototype__proto.date = getSetDayOfMonth; + momentPrototype__proto.day = momentPrototype__proto.days = getSetDayOfWeek; + momentPrototype__proto.weekday = getSetLocaleDayOfWeek; momentPrototype__proto.isoWeekday = getSetISODayOfWeek; - momentPrototype__proto.dayOfYear = getSetDayOfYear; + momentPrototype__proto.dayOfYear = getSetDayOfYear; // Hour momentPrototype__proto.hour = momentPrototype__proto.hours = getSetHour; @@ -2893,62 +3342,62 @@ momentPrototype__proto.millisecond = momentPrototype__proto.milliseconds = getSetMillisecond; // Offset - momentPrototype__proto.utcOffset = getSetOffset; - momentPrototype__proto.utc = setOffsetToUTC; - momentPrototype__proto.local = setOffsetToLocal; - momentPrototype__proto.parseZone = setOffsetToParsedOffset; + momentPrototype__proto.utcOffset = getSetOffset; + momentPrototype__proto.utc = setOffsetToUTC; + momentPrototype__proto.local = setOffsetToLocal; + momentPrototype__proto.parseZone = setOffsetToParsedOffset; momentPrototype__proto.hasAlignedHourOffset = hasAlignedHourOffset; - momentPrototype__proto.isDST = isDaylightSavingTime; - momentPrototype__proto.isDSTShifted = isDaylightSavingTimeShifted; - momentPrototype__proto.isLocal = isLocal; - momentPrototype__proto.isUtcOffset = isUtcOffset; - momentPrototype__proto.isUtc = isUtc; - momentPrototype__proto.isUTC = isUtc; + momentPrototype__proto.isDST = isDaylightSavingTime; + momentPrototype__proto.isDSTShifted = isDaylightSavingTimeShifted; + momentPrototype__proto.isLocal = isLocal; + momentPrototype__proto.isUtcOffset = isUtcOffset; + momentPrototype__proto.isUtc = isUtc; + momentPrototype__proto.isUTC = isUtc; // Timezone momentPrototype__proto.zoneAbbr = getZoneAbbr; momentPrototype__proto.zoneName = getZoneName; // Deprecations - momentPrototype__proto.dates = deprecate('dates accessor is deprecated. Use date instead.', getSetDayOfMonth); + momentPrototype__proto.dates = deprecate('dates accessor is deprecated. Use date instead.', getSetDayOfMonth); momentPrototype__proto.months = deprecate('months accessor is deprecated. Use month instead', getSetMonth); - momentPrototype__proto.years = deprecate('years accessor is deprecated. Use year instead', getSetYear); - momentPrototype__proto.zone = deprecate('moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779', getSetZone); + momentPrototype__proto.years = deprecate('years accessor is deprecated. Use year instead', getSetYear); + momentPrototype__proto.zone = deprecate('moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779', getSetZone); var momentPrototype = momentPrototype__proto; - function moment__createUnix (input) { + function moment__createUnix(input) { return local__createLocal(input * 1000); } - function moment__createInZone () { + function moment__createInZone() { return local__createLocal.apply(null, arguments).parseZone(); } var defaultCalendar = { - sameDay : '[Today at] LT', - nextDay : '[Tomorrow at] LT', - nextWeek : 'dddd [at] LT', - lastDay : '[Yesterday at] LT', - lastWeek : '[Last] dddd [at] LT', - sameElse : 'L' + sameDay: '[Today at] LT', + nextDay: '[Tomorrow at] LT', + nextWeek: 'dddd [at] LT', + lastDay: '[Yesterday at] LT', + lastWeek: '[Last] dddd [at] LT', + sameElse: 'L' }; - function locale_calendar__calendar (key, mom, now) { + function locale_calendar__calendar(key, mom, now) { var output = this._calendar[key]; return isFunction(output) ? output.call(mom, now) : output; } var defaultLongDateFormat = { - LTS : 'h:mm:ss A', - LT : 'h:mm A', - L : 'MM/DD/YYYY', - LL : 'MMMM D, YYYY', - LLL : 'MMMM D, YYYY h:mm A', - LLLL : 'dddd, MMMM D, YYYY h:mm A' + LTS: 'h:mm:ss A', + LT: 'h:mm A', + L: 'MM/DD/YYYY', + LL: 'MMMM D, YYYY', + LLL: 'MMMM D, YYYY h:mm A', + LLLL: 'dddd, MMMM D, YYYY h:mm A' }; - function longDateFormat (key) { + function longDateFormat(key) { var format = this._longDateFormat[key], formatUpper = this._longDateFormat[key.toUpperCase()]; @@ -2956,7 +3405,7 @@ return format; } - this._longDateFormat[key] = formatUpper.replace(/MMMM|MM|DD|dddd/g, function (val) { + this._longDateFormat[key] = formatUpper.replace(/MMMM|MM|DD|dddd/g, function(val) { return val.slice(1); }); @@ -2965,88 +3414,75 @@ var defaultInvalidDate = 'Invalid date'; - function invalidDate () { + function invalidDate() { return this._invalidDate; } var defaultOrdinal = '%d'; var defaultOrdinalParse = /\d{1,2}/; - function ordinal (number) { + function ordinal(number) { return this._ordinal.replace('%d', number); } - function preParsePostFormat (string) { + function preParsePostFormat(string) { return string; } var defaultRelativeTime = { - future : 'in %s', - past : '%s ago', - s : 'a few seconds', - m : 'a minute', - mm : '%d minutes', - h : 'an hour', - hh : '%d hours', - d : 'a day', - dd : '%d days', - M : 'a month', - MM : '%d months', - y : 'a year', - yy : '%d years' + future: 'in %s', + past: '%s ago', + s: 'a few seconds', + m: 'a minute', + mm: '%d minutes', + h: 'an hour', + hh: '%d hours', + d: 'a day', + dd: '%d days', + M: 'a month', + MM: '%d months', + y: 'a year', + yy: '%d years' }; - function relative__relativeTime (number, withoutSuffix, string, isFuture) { + function relative__relativeTime(number, withoutSuffix, string, isFuture) { var output = this._relativeTime[string]; - return (isFunction(output)) ? - output(number, withoutSuffix, string, isFuture) : - output.replace(/%d/i, number); + return (isFunction(output)) ? output(number, withoutSuffix, string, isFuture) : output.replace(/%d/i, number); } - function pastFuture (diff, output) { + function pastFuture(diff, output) { var format = this._relativeTime[diff > 0 ? 'future' : 'past']; return isFunction(format) ? format(output) : format.replace(/%s/i, output); } - function locale_set__set (config) { - var prop, i; - for (i in config) { - prop = config[i]; - if (isFunction(prop)) { - this[i] = prop; - } else { - this['_' + i] = prop; - } - } - // Lenient ordinal parsing accepts just a number in addition to - // number + (possibly) stuff coming from _ordinalParseLenient. - this._ordinalParseLenient = new RegExp(this._ordinalParse.source + '|' + (/\d{1,2}/).source); - } - var prototype__proto = Locale.prototype; - prototype__proto._calendar = defaultCalendar; - prototype__proto.calendar = locale_calendar__calendar; + prototype__proto._calendar = defaultCalendar; + prototype__proto.calendar = locale_calendar__calendar; prototype__proto._longDateFormat = defaultLongDateFormat; - prototype__proto.longDateFormat = longDateFormat; - prototype__proto._invalidDate = defaultInvalidDate; - prototype__proto.invalidDate = invalidDate; - prototype__proto._ordinal = defaultOrdinal; - prototype__proto.ordinal = ordinal; - prototype__proto._ordinalParse = defaultOrdinalParse; - prototype__proto.preparse = preParsePostFormat; - prototype__proto.postformat = preParsePostFormat; - prototype__proto._relativeTime = defaultRelativeTime; - prototype__proto.relativeTime = relative__relativeTime; - prototype__proto.pastFuture = pastFuture; - prototype__proto.set = locale_set__set; + prototype__proto.longDateFormat = longDateFormat; + prototype__proto._invalidDate = defaultInvalidDate; + prototype__proto.invalidDate = invalidDate; + prototype__proto._ordinal = defaultOrdinal; + prototype__proto.ordinal = ordinal; + prototype__proto._ordinalParse = defaultOrdinalParse; + prototype__proto.preparse = preParsePostFormat; + prototype__proto.postformat = preParsePostFormat; + prototype__proto._relativeTime = defaultRelativeTime; + prototype__proto.relativeTime = relative__relativeTime; + prototype__proto.pastFuture = pastFuture; + prototype__proto.set = locale_set__set; // Month - prototype__proto.months = localeMonths; - prototype__proto._months = defaultLocaleMonths; - prototype__proto.monthsShort = localeMonthsShort; + prototype__proto.months = localeMonths; + prototype__proto._months = defaultLocaleMonths; + prototype__proto.monthsShort = localeMonthsShort; prototype__proto._monthsShort = defaultLocaleMonthsShort; - prototype__proto.monthsParse = localeMonthsParse; + prototype__proto.monthsParse = localeMonthsParse; + prototype__proto._monthsRegex = defaultMonthsRegex; + prototype__proto.monthsRegex = monthsRegex; + prototype__proto._monthsShortRegex = defaultMonthsShortRegex; + prototype__proto.monthsShortRegex = monthsShortRegex; // Week prototype__proto.week = localeWeek; @@ -3055,26 +3491,33 @@ prototype__proto.firstDayOfWeek = localeFirstDayOfWeek; // Day of Week - prototype__proto.weekdays = localeWeekdays; - prototype__proto._weekdays = defaultLocaleWeekdays; - prototype__proto.weekdaysMin = localeWeekdaysMin; - prototype__proto._weekdaysMin = defaultLocaleWeekdaysMin; - prototype__proto.weekdaysShort = localeWeekdaysShort; + prototype__proto.weekdays = localeWeekdays; + prototype__proto._weekdays = defaultLocaleWeekdays; + prototype__proto.weekdaysMin = localeWeekdaysMin; + prototype__proto._weekdaysMin = defaultLocaleWeekdaysMin; + prototype__proto.weekdaysShort = localeWeekdaysShort; prototype__proto._weekdaysShort = defaultLocaleWeekdaysShort; - prototype__proto.weekdaysParse = localeWeekdaysParse; + prototype__proto.weekdaysParse = localeWeekdaysParse; + + prototype__proto._weekdaysRegex = defaultWeekdaysRegex; + prototype__proto.weekdaysRegex = weekdaysRegex; + prototype__proto._weekdaysShortRegex = defaultWeekdaysShortRegex; + prototype__proto.weekdaysShortRegex = weekdaysShortRegex; + prototype__proto._weekdaysMinRegex = defaultWeekdaysMinRegex; + prototype__proto.weekdaysMinRegex = weekdaysMinRegex; // Hours prototype__proto.isPM = localeIsPM; prototype__proto._meridiemParse = defaultLocaleMeridiemParse; prototype__proto.meridiem = localeMeridiem; - function lists__get (format, index, field, setter) { + function lists__get(format, index, field, setter) { var locale = locale_locales__getLocale(); var utc = create_utc__createUTC().set(setter, index); return locale[field](utc, format); } - function list (format, index, field, count, setter) { + function listMonthsImpl(format, index, field) { if (typeof format === 'number') { index = format; format = undefined; @@ -3083,48 +3526,86 @@ format = format || ''; if (index != null) { - return lists__get(format, index, field, setter); + return lists__get(format, index, field, 'month'); } var i; var out = []; - for (i = 0; i < count; i++) { - out[i] = lists__get(format, i, field, setter); + for (i = 0; i < 12; i++) { + out[i] = lists__get(format, i, field, 'month'); } return out; } - function lists__listMonths (format, index) { - return list(format, index, 'months', 12, 'month'); + // () + // (5) + // (fmt, 5) + // (fmt) + // (true) + // (true, 5) + // (true, fmt, 5) + // (true, fmt) + function listWeekdaysImpl(localeSorted, format, index, field) { + if (typeof localeSorted === 'boolean') { + if (typeof format === 'number') { + index = format; + format = undefined; + } + + format = format || ''; + } else { + format = localeSorted; + index = format; + localeSorted = false; + + if (typeof format === 'number') { + index = format; + format = undefined; + } + + format = format || ''; + } + + var locale = locale_locales__getLocale(), + shift = localeSorted ? locale._week.dow : 0; + + if (index != null) { + return lists__get(format, (index + shift) % 7, field, 'day'); + } + + var i; + var out = []; + for (i = 0; i < 7; i++) { + out[i] = lists__get(format, (i + shift) % 7, field, 'day'); + } + return out; + } + + function lists__listMonths(format, index) { + return listMonthsImpl(format, index, 'months'); } - function lists__listMonthsShort (format, index) { - return list(format, index, 'monthsShort', 12, 'month'); + function lists__listMonthsShort(format, index) { + return listMonthsImpl(format, index, 'monthsShort'); } - function lists__listWeekdays (format, index) { - return list(format, index, 'weekdays', 7, 'day'); + function lists__listWeekdays(localeSorted, format, index) { + return listWeekdaysImpl(localeSorted, format, index, 'weekdays'); } - function lists__listWeekdaysShort (format, index) { - return list(format, index, 'weekdaysShort', 7, 'day'); + function lists__listWeekdaysShort(localeSorted, format, index) { + return listWeekdaysImpl(localeSorted, format, index, 'weekdaysShort'); } - function lists__listWeekdaysMin (format, index) { - return list(format, index, 'weekdaysMin', 7, 'day'); + function lists__listWeekdaysMin(localeSorted, format, index) { + return listWeekdaysImpl(localeSorted, format, index, 'weekdaysMin'); } locale_locales__getSetGlobalLocale('en', { - monthsParse : [/^jan/i, /^feb/i, /^mar/i, /^apr/i, /^may/i, /^jun/i, /^jul/i, /^aug/i, /^sep/i, /^oct/i, /^nov/i, /^dec/i], - longMonthsParse : [/^january$/i, /^february$/i, /^march$/i, /^april$/i, /^may$/i, /^june$/i, /^july$/i, /^august$/i, /^september$/i, /^october$/i, /^november$/i, /^december$/i], - shortMonthsParse : [/^jan$/i, /^feb$/i, /^mar$/i, /^apr$/i, /^may$/i, /^jun$/i, /^jul$/i, /^aug/i, /^sept?$/i, /^oct$/i, /^nov$/i, /^dec$/i], ordinalParse: /\d{1,2}(th|st|nd|rd)/, - ordinal : function (number) { + ordinal: function(number) { var b = number % 10, - output = (toInt(number % 100 / 10) === 1) ? 'th' : - (b === 1) ? 'st' : - (b === 2) ? 'nd' : - (b === 3) ? 'rd' : 'th'; + output = (toInt(number % 100 / 10) === 1) ? 'th' : (b === 1) ? 'st' : (b === 2) ? 'nd' : (b === 3) ? 'rd' : 'th'; return number + output; } }); @@ -3135,44 +3616,44 @@ var mathAbs = Math.abs; - function duration_abs__abs () { - var data = this._data; + function duration_abs__abs() { + var data = this._data; this._milliseconds = mathAbs(this._milliseconds); - this._days = mathAbs(this._days); - this._months = mathAbs(this._months); + this._days = mathAbs(this._days); + this._months = mathAbs(this._months); - data.milliseconds = mathAbs(data.milliseconds); - data.seconds = mathAbs(data.seconds); - data.minutes = mathAbs(data.minutes); - data.hours = mathAbs(data.hours); - data.months = mathAbs(data.months); - data.years = mathAbs(data.years); + data.milliseconds = mathAbs(data.milliseconds); + data.seconds = mathAbs(data.seconds); + data.minutes = mathAbs(data.minutes); + data.hours = mathAbs(data.hours); + data.months = mathAbs(data.months); + data.years = mathAbs(data.years); return this; } - function duration_add_subtract__addSubtract (duration, input, value, direction) { + function duration_add_subtract__addSubtract(duration, input, value, direction) { var other = create__createDuration(input, value); duration._milliseconds += direction * other._milliseconds; - duration._days += direction * other._days; - duration._months += direction * other._months; + duration._days += direction * other._days; + duration._months += direction * other._months; return duration._bubble(); } // supports only 2.0-style add(1, 's') or add(duration) - function duration_add_subtract__add (input, value) { + function duration_add_subtract__add(input, value) { return duration_add_subtract__addSubtract(this, input, value, 1); } // supports only 2.0-style subtract(1, 's') or subtract(duration) - function duration_add_subtract__subtract (input, value) { - return duration_add_subtract__addSubtract(this, input, value, -1); + function duration_add_subtract__subtract(input, value) { + return duration_add_subtract__addSubtract(this, input, value, - 1); } - function absCeil (number) { + function absCeil(number) { if (number < 0) { return Math.floor(number); } else { @@ -3180,17 +3661,16 @@ } } - function bubble () { + function bubble() { var milliseconds = this._milliseconds; - var days = this._days; - var months = this._months; - var data = this._data; + var days = this._days; + var months = this._months; + var data = this._data; var seconds, minutes, hours, years, monthsFromDays; // if we have a mix of positive and negative values, bubble down first // check: https://github.com/moment/moment/issues/2166 - if (!((milliseconds >= 0 && days >= 0 && months >= 0) || - (milliseconds <= 0 && days <= 0 && months <= 0))) { + if (!((milliseconds >= 0 && days >= 0 && months >= 0) || (milliseconds <= 0 && days <= 0 && months <= 0))) { milliseconds += absCeil(monthsToDays(months) + days) * 864e5; days = 0; months = 0; @@ -3200,14 +3680,14 @@ // examples of what that means. data.milliseconds = milliseconds % 1000; - seconds = absFloor(milliseconds / 1000); - data.seconds = seconds % 60; + seconds = absFloor(milliseconds / 1000); + data.seconds = seconds % 60; - minutes = absFloor(seconds / 60); - data.minutes = minutes % 60; + minutes = absFloor(seconds / 60); + data.minutes = minutes % 60; - hours = absFloor(minutes / 60); - data.hours = hours % 24; + hours = absFloor(minutes / 60); + data.hours = hours % 24; days += absFloor(hours / 24); @@ -3220,25 +3700,25 @@ years = absFloor(months / 12); months %= 12; - data.days = days; + data.days = days; data.months = months; - data.years = years; + data.years = years; return this; } - function daysToMonths (days) { + function daysToMonths(days) { // 400 years have 146097 days (taking into account leap year rules) // 400 years have 12 months === 4800 return days * 4800 / 146097; } - function monthsToDays (months) { + function monthsToDays(months) { // the reverse of daysToMonths return months * 146097 / 4800; } - function as (units) { + function as(units) { var days; var months; var milliseconds = this._milliseconds; @@ -3246,106 +3726,100 @@ units = normalizeUnits(units); if (units === 'month' || units === 'year') { - days = this._days + milliseconds / 864e5; + days = this._days + milliseconds / 864e5; months = this._months + daysToMonths(days); return units === 'month' ? months : months / 12; } else { // handle milliseconds separately because of floating point math errors (issue #1867) days = this._days + Math.round(monthsToDays(this._months)); switch (units) { - case 'week' : return days / 7 + milliseconds / 6048e5; - case 'day' : return days + milliseconds / 864e5; - case 'hour' : return days * 24 + milliseconds / 36e5; - case 'minute' : return days * 1440 + milliseconds / 6e4; - case 'second' : return days * 86400 + milliseconds / 1000; - // Math.floor prevents floating point math errors here - case 'millisecond': return Math.floor(days * 864e5) + milliseconds; - default: throw new Error('Unknown unit ' + units); + case 'week': + return days / 7 + milliseconds / 6048e5; + case 'day': + return days + milliseconds / 864e5; + case 'hour': + return days * 24 + milliseconds / 36e5; + case 'minute': + return days * 1440 + milliseconds / 6e4; + case 'second': + return days * 86400 + milliseconds / 1000; + // Math.floor prevents floating point math errors here + case 'millisecond': + return Math.floor(days * 864e5) + milliseconds; + default: + throw new Error('Unknown unit ' + units); } } } // TODO: Use this.as('ms')? - function duration_as__valueOf () { + function duration_as__valueOf() { return ( - this._milliseconds + - this._days * 864e5 + - (this._months % 12) * 2592e6 + - toInt(this._months / 12) * 31536e6 - ); + this._milliseconds + this._days * 864e5 + (this._months % 12) * 2592e6 + toInt(this._months / 12) * 31536e6); } - function makeAs (alias) { - return function () { + function makeAs(alias) { + return function() { return this.as(alias); }; } var asMilliseconds = makeAs('ms'); - var asSeconds = makeAs('s'); - var asMinutes = makeAs('m'); - var asHours = makeAs('h'); - var asDays = makeAs('d'); - var asWeeks = makeAs('w'); - var asMonths = makeAs('M'); - var asYears = makeAs('y'); - - function duration_get__get (units) { + var asSeconds = makeAs('s'); + var asMinutes = makeAs('m'); + var asHours = makeAs('h'); + var asDays = makeAs('d'); + var asWeeks = makeAs('w'); + var asMonths = makeAs('M'); + var asYears = makeAs('y'); + + function duration_get__get(units) { units = normalizeUnits(units); return this[units + 's'](); } function makeGetter(name) { - return function () { + return function() { return this._data[name]; }; } var milliseconds = makeGetter('milliseconds'); - var seconds = makeGetter('seconds'); - var minutes = makeGetter('minutes'); - var hours = makeGetter('hours'); - var days = makeGetter('days'); - var months = makeGetter('months'); - var years = makeGetter('years'); - - function weeks () { + var seconds = makeGetter('seconds'); + var minutes = makeGetter('minutes'); + var hours = makeGetter('hours'); + var days = makeGetter('days'); + var months = makeGetter('months'); + var years = makeGetter('years'); + + function weeks() { return absFloor(this.days() / 7); } var round = Math.round; var thresholds = { - s: 45, // seconds to minute - m: 45, // minutes to hour - h: 22, // hours to day - d: 26, // days to month - M: 11 // months to year + s: 45, // seconds to minute + m: 45, // minutes to hour + h: 22, // hours to day + d: 26, // days to month + M: 11 // months to year }; // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) { - return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture); + return locale.relativeTime(number || 1, !! withoutSuffix, string, isFuture); } - function duration_humanize__relativeTime (posNegDuration, withoutSuffix, locale) { + function duration_humanize__relativeTime(posNegDuration, withoutSuffix, locale) { var duration = create__createDuration(posNegDuration).abs(); - var seconds = round(duration.as('s')); - var minutes = round(duration.as('m')); - var hours = round(duration.as('h')); - var days = round(duration.as('d')); - var months = round(duration.as('M')); - var years = round(duration.as('y')); - - var a = seconds < thresholds.s && ['s', seconds] || - minutes <= 1 && ['m'] || - minutes < thresholds.m && ['mm', minutes] || - hours <= 1 && ['h'] || - hours < thresholds.h && ['hh', hours] || - days <= 1 && ['d'] || - days < thresholds.d && ['dd', days] || - months <= 1 && ['M'] || - months < thresholds.M && ['MM', months] || - years <= 1 && ['y'] || ['yy', years]; + var seconds = round(duration.as('s')); + var minutes = round(duration.as('m')); + var hours = round(duration.as('h')); + var days = round(duration.as('d')); + var months = round(duration.as('M')); + var years = round(duration.as('y')); + + var a = seconds < thresholds.s && ['s', seconds] || minutes <= 1 && ['m'] || minutes < thresholds.m && ['mm', minutes] || hours <= 1 && ['h'] || hours < thresholds.h && ['hh', hours] || days <= 1 && ['d'] || days < thresholds.d && ['dd', days] || months <= 1 && ['M'] || months < thresholds.M && ['MM', months] || years <= 1 && ['y'] || ['yy', years]; a[2] = withoutSuffix; a[3] = +posNegDuration > 0; @@ -3354,7 +3828,7 @@ } // This function allows you to set a threshold for relative time strings - function duration_humanize__getSetRelativeTimeThreshold (threshold, limit) { + function duration_humanize__getSetRelativeTimeThreshold(threshold, limit) { if (thresholds[threshold] === undefined) { return false; } @@ -3365,7 +3839,7 @@ return true; } - function humanize (withSuffix) { + function humanize(withSuffix) { var locale = this.localeData(); var output = duration_humanize__relativeTime(this, !withSuffix, locale); @@ -3387,18 +3861,18 @@ // (think of clock changes) // and also not between days and months (28-31 days per month) var seconds = iso_string__abs(this._milliseconds) / 1000; - var days = iso_string__abs(this._days); - var months = iso_string__abs(this._months); + var days = iso_string__abs(this._days); + var months = iso_string__abs(this._months); var minutes, hours, years; // 3600 seconds -> 60 minutes -> 1 hour - minutes = absFloor(seconds / 60); - hours = absFloor(minutes / 60); + minutes = absFloor(seconds / 60); + hours = absFloor(minutes / 60); seconds %= 60; minutes %= 60; // 12 months -> 1 year - years = absFloor(months / 12); + years = absFloor(months / 12); months %= 12; @@ -3417,48 +3891,40 @@ return 'P0D'; } - return (total < 0 ? '-' : '') + - 'P' + - (Y ? Y + 'Y' : '') + - (M ? M + 'M' : '') + - (D ? D + 'D' : '') + - ((h || m || s) ? 'T' : '') + - (h ? h + 'H' : '') + - (m ? m + 'M' : '') + - (s ? s + 'S' : ''); + return (total < 0 ? '-' : '') + 'P' + (Y ? Y + 'Y' : '') + (M ? M + 'M' : '') + (D ? D + 'D' : '') + ((h || m || s) ? 'T' : '') + (h ? h + 'H' : '') + (m ? m + 'M' : '') + (s ? s + 'S' : ''); } var duration_prototype__proto = Duration.prototype; - duration_prototype__proto.abs = duration_abs__abs; - duration_prototype__proto.add = duration_add_subtract__add; - duration_prototype__proto.subtract = duration_add_subtract__subtract; - duration_prototype__proto.as = as; + duration_prototype__proto.abs = duration_abs__abs; + duration_prototype__proto.add = duration_add_subtract__add; + duration_prototype__proto.subtract = duration_add_subtract__subtract; + duration_prototype__proto.as = as; duration_prototype__proto.asMilliseconds = asMilliseconds; - duration_prototype__proto.asSeconds = asSeconds; - duration_prototype__proto.asMinutes = asMinutes; - duration_prototype__proto.asHours = asHours; - duration_prototype__proto.asDays = asDays; - duration_prototype__proto.asWeeks = asWeeks; - duration_prototype__proto.asMonths = asMonths; - duration_prototype__proto.asYears = asYears; - duration_prototype__proto.valueOf = duration_as__valueOf; - duration_prototype__proto._bubble = bubble; - duration_prototype__proto.get = duration_get__get; - duration_prototype__proto.milliseconds = milliseconds; - duration_prototype__proto.seconds = seconds; - duration_prototype__proto.minutes = minutes; - duration_prototype__proto.hours = hours; - duration_prototype__proto.days = days; - duration_prototype__proto.weeks = weeks; - duration_prototype__proto.months = months; - duration_prototype__proto.years = years; - duration_prototype__proto.humanize = humanize; - duration_prototype__proto.toISOString = iso_string__toISOString; - duration_prototype__proto.toString = iso_string__toISOString; - duration_prototype__proto.toJSON = iso_string__toISOString; - duration_prototype__proto.locale = locale; - duration_prototype__proto.localeData = localeData; + duration_prototype__proto.asSeconds = asSeconds; + duration_prototype__proto.asMinutes = asMinutes; + duration_prototype__proto.asHours = asHours; + duration_prototype__proto.asDays = asDays; + duration_prototype__proto.asWeeks = asWeeks; + duration_prototype__proto.asMonths = asMonths; + duration_prototype__proto.asYears = asYears; + duration_prototype__proto.valueOf = duration_as__valueOf; + duration_prototype__proto._bubble = bubble; + duration_prototype__proto.get = duration_get__get; + duration_prototype__proto.milliseconds = milliseconds; + duration_prototype__proto.seconds = seconds; + duration_prototype__proto.minutes = minutes; + duration_prototype__proto.hours = hours; + duration_prototype__proto.days = days; + duration_prototype__proto.weeks = weeks; + duration_prototype__proto.months = months; + duration_prototype__proto.years = years; + duration_prototype__proto.humanize = humanize; + duration_prototype__proto.toISOString = iso_string__toISOString; + duration_prototype__proto.toString = iso_string__toISOString; + duration_prototype__proto.toJSON = iso_string__toISOString; + duration_prototype__proto.locale = locale; + duration_prototype__proto.localeData = localeData; // Deprecations duration_prototype__proto.toIsoString = deprecate('toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', iso_string__toISOString); @@ -3475,46 +3941,48 @@ addRegexToken('x', matchSigned); addRegexToken('X', matchTimestamp); - addParseToken('X', function (input, array, config) { + addParseToken('X', function(input, array, config) { config._d = new Date(parseFloat(input, 10) * 1000); }); - addParseToken('x', function (input, array, config) { + addParseToken('x', function(input, array, config) { config._d = new Date(toInt(input)); }); // Side effect imports - utils_hooks__hooks.version = '2.11.0'; + utils_hooks__hooks.version = '2.13.0'; setHookCallback(local__createLocal); - utils_hooks__hooks.fn = momentPrototype; - utils_hooks__hooks.min = min; - utils_hooks__hooks.max = max; - utils_hooks__hooks.now = now; - utils_hooks__hooks.utc = create_utc__createUTC; - utils_hooks__hooks.unix = moment__createUnix; - utils_hooks__hooks.months = lists__listMonths; - utils_hooks__hooks.isDate = isDate; - utils_hooks__hooks.locale = locale_locales__getSetGlobalLocale; - utils_hooks__hooks.invalid = valid__createInvalid; - utils_hooks__hooks.duration = create__createDuration; - utils_hooks__hooks.isMoment = isMoment; - utils_hooks__hooks.weekdays = lists__listWeekdays; - utils_hooks__hooks.parseZone = moment__createInZone; - utils_hooks__hooks.localeData = locale_locales__getLocale; - utils_hooks__hooks.isDuration = isDuration; - utils_hooks__hooks.monthsShort = lists__listMonthsShort; - utils_hooks__hooks.weekdaysMin = lists__listWeekdaysMin; - utils_hooks__hooks.defineLocale = defineLocale; - utils_hooks__hooks.weekdaysShort = lists__listWeekdaysShort; - utils_hooks__hooks.normalizeUnits = normalizeUnits; + utils_hooks__hooks.fn = momentPrototype; + utils_hooks__hooks.min = min; + utils_hooks__hooks.max = max; + utils_hooks__hooks.now = now; + utils_hooks__hooks.utc = create_utc__createUTC; + utils_hooks__hooks.unix = moment__createUnix; + utils_hooks__hooks.months = lists__listMonths; + utils_hooks__hooks.isDate = isDate; + utils_hooks__hooks.locale = locale_locales__getSetGlobalLocale; + utils_hooks__hooks.invalid = valid__createInvalid; + utils_hooks__hooks.duration = create__createDuration; + utils_hooks__hooks.isMoment = isMoment; + utils_hooks__hooks.weekdays = lists__listWeekdays; + utils_hooks__hooks.parseZone = moment__createInZone; + utils_hooks__hooks.localeData = locale_locales__getLocale; + utils_hooks__hooks.isDuration = isDuration; + utils_hooks__hooks.monthsShort = lists__listMonthsShort; + utils_hooks__hooks.weekdaysMin = lists__listWeekdaysMin; + utils_hooks__hooks.defineLocale = defineLocale; + utils_hooks__hooks.updateLocale = updateLocale; + utils_hooks__hooks.locales = locale_locales__listLocales; + utils_hooks__hooks.weekdaysShort = lists__listWeekdaysShort; + utils_hooks__hooks.normalizeUnits = normalizeUnits; utils_hooks__hooks.relativeTimeThreshold = duration_humanize__getSetRelativeTimeThreshold; - utils_hooks__hooks.prototype = momentPrototype; + utils_hooks__hooks.prototype = momentPrototype; var _moment = utils_hooks__hooks; return _moment; -})); +})); diff --git a/test/Widgets/Calendar.mpk b/test/Widgets/Calendar.mpk index 14b153a..afcf2b5 100644 Binary files a/test/Widgets/Calendar.mpk and b/test/Widgets/Calendar.mpk differ diff --git a/test/[Test] Calendar Widget.mpr b/test/[Test] Calendar Widget.mpr index f02e1c2..cb1933d 100644 Binary files a/test/[Test] Calendar Widget.mpr and b/test/[Test] Calendar Widget.mpr differ