diff options
| author | Romeo Fragomeli <rfr@odoo.com> | 2019-12-12 11:55:40 +0000 |
|---|---|---|
| committer | Pierre Paridans <app@odoo.com> | 2020-03-31 15:06:11 +0000 |
| commit | ebce7719b624136da2b26a73acf8930538c7a38c (patch) | |
| tree | c89870d188705a37dee60e69aed2dd6e9eee524b | |
| parent | d2851b203924dcb0e56e534751a552e99757971b (diff) | |
[REF] calendar,web: update to FullCalendar v4
This commit updates the FullCalendar library from v3 to v4.
The purpose of this update for us is to add a proper support of the
drag-and-drop of events on mobile.
But also to fix scrolling issues present mainly on smaller screens. In
those configurations multi/inner-scrolls were present resulting into
either the inability to properly scroll the desired content or even
blocking alltogether the scroll.
As a nice-to-have feature, the version 4 removes its dependency on the
jQuery library, allowing a faster loading of a bigger set of events.
Last but not least, this version also reduces its dependency on the
Moment.js library (but still keeps a binding to it for compatibility
purpose).
This commit focus only on the update of the library itself and the
adaptation of the related code to keep it working. Further
enhancements/fixes will be done later on.
To do so it modifies the following points:
* Update the library in itself (js, css...).
* Convert all FullCalendar's method calls to the new syntax (e.g.
`$calendar.fullCalendar('destroy')` becomes `calendar.destroy()`).
* Add a utility method to convert v4 events into the old v3 event
structure to ease the transition from one version to the other.
* Wrap all event's custom fields into a new event's
`extendedProps` object (as its the new way of storing them instead of
directly as a prop. of the event).
* Adapt `slotLabelFormat` and `weekNumberCalculation`.
* Use the view's new names:
** `agendaDay` -> `timeGridDay`
** `agendaWeek` -> `timeGridWeek`
** `month` -> `dayGridMonth`
* Use the option's new names:
** `viewRender` -> `datesRender`
** `eventMouseover` -> `eventMouseEnter`
** `eventMouseout` -> `eventMouseLeave`
** `isRTL` -> `dir`
** `selectHelper` -> `selectMirror`
** `weekNumberTitle` -> `weekLabel`
Ref:
https://fullcalendar.io/docs/upgrading-from-v3
Task ID: 2092550
97 files changed, 20298 insertions, 15236 deletions
diff --git a/addons/calendar/static/src/js/calendar_view.js b/addons/calendar/static/src/js/calendar_view.js index 1717deb7899..6c6f8ea0d54 100644 --- a/addons/calendar/static/src/js/calendar_view.js +++ b/addons/calendar/static/src/js/calendar_view.js @@ -25,7 +25,7 @@ var AttendeeCalendarPopover = CalendarPopover.extend({ _.each(this.fields.attendee_status.selection, function (selection) { self.statusInfo[selection[0]] = {text: selection[1], color: self.statusColors[selection[0]]}; }); - this.selectedStatusInfo = this.statusInfo[this.event.record.attendee_status]; + this.selectedStatusInfo = this.statusInfo[this.event.extendedProps.record.attendee_status]; } }, @@ -37,7 +37,7 @@ var AttendeeCalendarPopover = CalendarPopover.extend({ * @return {boolean} */ isCurrentPartnerAttendee() { - return this.event.record.partner_ids.includes(session.partner_id); + return this.event.extendedProps.record.partner_ids.includes(session.partner_id); }, /** * @override @@ -70,7 +70,7 @@ var AttendeeCalendarPopover = CalendarPopover.extend({ * @return {boolean} */ _isEventPrivate() { - return this.event.record.privacy === 'private'; + return this.event.extendedProps.record.privacy === 'private'; }, //-------------------------------------------------------------------------- @@ -90,7 +90,7 @@ var AttendeeCalendarPopover = CalendarPopover.extend({ method: 'change_attendee_status', args: [this.event.id, selectedStatus], }).then(function () { - self.event.record.attendee_status = selectedStatus; // FIXEME: Maybe we have to reload view + self.event.extendedProps.record.attendee_status = selectedStatus; // FIXEME: Maybe we have to reload view self.$('.o-calendar-attendee-status-text').text(self.statusInfo[selectedStatus].text); self.$('.o-calendar-attendee-status-icon').removeClass(_.values(self.statusColors).join(' ')).addClass(self.statusInfo[selectedStatus].color); }); diff --git a/addons/web/static/lib/fullcalendar/LICENSE.txt b/addons/web/static/lib/fullcalendar/LICENSE.txt index eafaf97ec40..2149cfbeff4 100644 --- a/addons/web/static/lib/fullcalendar/LICENSE.txt +++ b/addons/web/static/lib/fullcalendar/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2015 Adam Shaw +Copyright (c) 2019 Adam Shaw Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/addons/web/static/lib/fullcalendar/bootstrap/main.css b/addons/web/static/lib/fullcalendar/bootstrap/main.css new file mode 100644 index 00000000000..22b33f56ba0 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/bootstrap/main.css @@ -0,0 +1,36 @@ +.fc.fc-bootstrap a { + text-decoration: none; +} + +.fc.fc-bootstrap a[data-goto]:hover { + text-decoration: underline; +} + +.fc-bootstrap hr.fc-divider { + border-color: inherit; +} + +.fc-bootstrap .fc-today.alert { + border-radius: 0; +} + +.fc-bootstrap a.fc-event:not([href]):not([tabindex]) { + color: #fff; +} + +.fc-bootstrap .fc-popover.card { + position: absolute; +} + +/* Popover +--------------------------------------------------------------------------------------------------*/ +.fc-bootstrap .fc-popover .card-body { + padding: 0; +} + +/* TimeGrid Slats (lines that run horizontally) +--------------------------------------------------------------------------------------------------*/ +.fc-bootstrap .fc-time-grid .fc-slats table { + /* some themes have background color. see through to slats */ + background: none; +}
\ No newline at end of file diff --git a/addons/web/static/lib/fullcalendar/bootstrap/main.js b/addons/web/static/lib/fullcalendar/bootstrap/main.js new file mode 100644 index 00000000000..5f966890cf7 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/bootstrap/main.js @@ -0,0 +1,91 @@ +/*! +FullCalendar Bootstrap Plugin v4.4.0 +Docs & License: https://fullcalendar.io/ +(c) 2019 Adam Shaw +*/ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@fullcalendar/core')) : + typeof define === 'function' && define.amd ? define(['exports', '@fullcalendar/core'], factory) : + (global = global || self, factory(global.FullCalendarBootstrap = {}, global.FullCalendar)); +}(this, function (exports, core) { 'use strict'; + + /*! *****************************************************************************
+ Copyright (c) Microsoft Corporation. All rights reserved.
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+ this file except in compliance with the License. You may obtain a copy of the
+ License at http://www.apache.org/licenses/LICENSE-2.0
+
+ THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
+ WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+ MERCHANTABLITY OR NON-INFRINGEMENT.
+
+ See the Apache Version 2.0 License for specific language governing permissions
+ and limitations under the License.
+ ***************************************************************************** */
+ /* global Reflect, Promise */
+
+ var extendStatics = function(d, b) {
+ extendStatics = Object.setPrototypeOf ||
+ ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+ function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+ return extendStatics(d, b);
+ };
+
+ function __extends(d, b) {
+ extendStatics(d, b);
+ function __() { this.constructor = d; }
+ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+ } + + var BootstrapTheme = /** @class */ (function (_super) { + __extends(BootstrapTheme, _super); + function BootstrapTheme() { + return _super !== null && _super.apply(this, arguments) || this; + } + return BootstrapTheme; + }(core.Theme)); + BootstrapTheme.prototype.classes = { + widget: 'fc-bootstrap', + tableGrid: 'table-bordered', + tableList: 'table', + tableListHeading: 'table-active', + buttonGroup: 'btn-group', + button: 'btn btn-primary', + buttonActive: 'active', + today: 'alert alert-info', + popover: 'card card-primary', + popoverHeader: 'card-header', + popoverContent: 'card-body', + // day grid + // for left/right border color when border is inset from edges (all-day in timeGrid view) + // avoid `table` class b/c don't want margins/padding/structure. only border color. + headerRow: 'table-bordered', + dayRow: 'table-bordered', + // list view + listView: 'card card-primary' + }; + BootstrapTheme.prototype.baseIconClass = 'fa'; + BootstrapTheme.prototype.iconClasses = { + close: 'fa-times', + prev: 'fa-chevron-left', + next: 'fa-chevron-right', + prevYear: 'fa-angle-double-left', + nextYear: 'fa-angle-double-right' + }; + BootstrapTheme.prototype.iconOverrideOption = 'bootstrapFontAwesome'; + BootstrapTheme.prototype.iconOverrideCustomButtonOption = 'bootstrapFontAwesome'; + BootstrapTheme.prototype.iconOverridePrefix = 'fa-'; + var main = core.createPlugin({ + themeClasses: { + bootstrap: BootstrapTheme + } + }); + + exports.BootstrapTheme = BootstrapTheme; + exports.default = main; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales-all.js b/addons/web/static/lib/fullcalendar/core/locales-all.js new file mode 100644 index 00000000000..e6c77df2c09 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales-all.js @@ -0,0 +1,1405 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, global.FullCalendarLocalesAll = factory()); +}(this, function () { 'use strict'; + + var _m0 = { + code: "af", + week: { + dow: 1, + doy: 4 // Die week wat die 4de Januarie bevat is die eerste week van die jaar. + }, + buttonText: { + prev: "Vorige", + next: "Volgende", + today: "Vandag", + year: "Jaar", + month: "Maand", + week: "Week", + day: "Dag", + list: "Agenda" + }, + allDayHtml: "Heeldag", + eventLimitText: "Addisionele", + noEventsMessage: "Daar is geen gebeurtenisse nie" + }; + + var _m1 = { + code: "ar-dz", + week: { + dow: 0, + doy: 4 // The week that contains Jan 1st is the first week of the year. + }, + dir: 'rtl', + buttonText: { + prev: "السابق", + next: "التالي", + today: "اليوم", + month: "شهر", + week: "أسبوع", + day: "يوم", + list: "أجندة" + }, + weekLabel: "أسبوع", + allDayText: "اليوم كله", + eventLimitText: "أخرى", + noEventsMessage: "أي أحداث لعرض" + }; + + var _m2 = { + code: "ar-kw", + week: { + dow: 0, + doy: 12 // The week that contains Jan 1st is the first week of the year. + }, + dir: 'rtl', + buttonText: { + prev: "السابق", + next: "التالي", + today: "اليوم", + month: "شهر", + week: "أسبوع", + day: "يوم", + list: "أجندة" + }, + weekLabel: "أسبوع", + allDayText: "اليوم كله", + eventLimitText: "أخرى", + noEventsMessage: "أي أحداث لعرض" + }; + + var _m3 = { + code: "ar-ly", + week: { + dow: 6, + doy: 12 // The week that contains Jan 1st is the first week of the year. + }, + dir: 'rtl', + buttonText: { + prev: "السابق", + next: "التالي", + today: "اليوم", + month: "شهر", + week: "أسبوع", + day: "يوم", + list: "أجندة" + }, + weekLabel: "أسبوع", + allDayText: "اليوم كله", + eventLimitText: "أخرى", + noEventsMessage: "أي أحداث لعرض" + }; + + var _m4 = { + code: "ar-ma", + week: { + dow: 6, + doy: 12 // The week that contains Jan 1st is the first week of the year. + }, + dir: 'rtl', + buttonText: { + prev: "السابق", + next: "التالي", + today: "اليوم", + month: "شهر", + week: "أسبوع", + day: "يوم", + list: "أجندة" + }, + weekLabel: "أسبوع", + allDayText: "اليوم كله", + eventLimitText: "أخرى", + noEventsMessage: "أي أحداث لعرض" + }; + + var _m5 = { + code: "ar-sa", + week: { + dow: 0, + doy: 6 // The week that contains Jan 1st is the first week of the year. + }, + dir: 'rtl', + buttonText: { + prev: "السابق", + next: "التالي", + today: "اليوم", + month: "شهر", + week: "أسبوع", + day: "يوم", + list: "أجندة" + }, + weekLabel: "أسبوع", + allDayText: "اليوم كله", + eventLimitText: "أخرى", + noEventsMessage: "أي أحداث لعرض" + }; + + var _m6 = { + code: "ar-tn", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + dir: 'rtl', + buttonText: { + prev: "السابق", + next: "التالي", + today: "اليوم", + month: "شهر", + week: "أسبوع", + day: "يوم", + list: "أجندة" + }, + weekLabel: "أسبوع", + allDayText: "اليوم كله", + eventLimitText: "أخرى", + noEventsMessage: "أي أحداث لعرض" + }; + + var _m7 = { + code: "ar", + week: { + dow: 6, + doy: 12 // The week that contains Jan 1st is the first week of the year. + }, + dir: 'rtl', + buttonText: { + prev: "السابق", + next: "التالي", + today: "اليوم", + month: "شهر", + week: "أسبوع", + day: "يوم", + list: "أجندة" + }, + weekLabel: "أسبوع", + allDayText: "اليوم كله", + eventLimitText: "أخرى", + noEventsMessage: "أي أحداث لعرض" + }; + + var _m8 = { + code: "az", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Əvvəl", + next: "Sonra", + today: "Bu Gün", + month: "Ay", + week: "Həftə", + day: "Gün", + list: "Gündəm" + }, + weekLabel: "Həftə", + allDayText: "Bütün Gün", + eventLimitText: function (n) { + return "+ daha çox " + n; + }, + noEventsMessage: "Göstərmək üçün hadisə yoxdur" + }; + + var _m9 = { + code: "bg", + week: { + dow: 1, + doy: 7 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "назад", + next: "напред", + today: "днес", + month: "Месец", + week: "Седмица", + day: "Ден", + list: "График" + }, + allDayText: "Цял ден", + eventLimitText: function (n) { + return "+още " + n; + }, + noEventsMessage: "Няма събития за показване" + }; + + var _m10 = { + code: "bs", + week: { + dow: 1, + doy: 7 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "Prošli", + next: "Sljedeći", + today: "Danas", + month: "Mjesec", + week: "Sedmica", + day: "Dan", + list: "Raspored" + }, + weekLabel: "Sed", + allDayText: "Cijeli dan", + eventLimitText: function (n) { + return "+ još " + n; + }, + noEventsMessage: "Nema događaja za prikazivanje" + }; + + var _m11 = { + code: "ca", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Anterior", + next: "Següent", + today: "Avui", + month: "Mes", + week: "Setmana", + day: "Dia", + list: "Agenda" + }, + weekLabel: "Set", + allDayText: "Tot el dia", + eventLimitText: "més", + noEventsMessage: "No hi ha esdeveniments per mostrar" + }; + + var _m12 = { + code: "cs", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Dříve", + next: "Později", + today: "Nyní", + month: "Měsíc", + week: "Týden", + day: "Den", + list: "Agenda" + }, + weekLabel: "Týd", + allDayText: "Celý den", + eventLimitText: function (n) { + return "+další: " + n; + }, + noEventsMessage: "Žádné akce k zobrazení" + }; + + var _m13 = { + code: "da", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Forrige", + next: "Næste", + today: "I dag", + month: "Måned", + week: "Uge", + day: "Dag", + list: "Agenda" + }, + weekLabel: "Uge", + allDayText: "Hele dagen", + eventLimitText: "flere", + noEventsMessage: "Ingen arrangementer at vise" + }; + + var _m14 = { + code: "de", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Zurück", + next: "Vor", + today: "Heute", + year: "Jahr", + month: "Monat", + week: "Woche", + day: "Tag", + list: "Terminübersicht" + }, + weekLabel: "KW", + allDayText: "Ganztägig", + eventLimitText: function (n) { + return "+ weitere " + n; + }, + noEventsMessage: "Keine Ereignisse anzuzeigen" + }; + + var _m15 = { + code: "el", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4st is the first week of the year. + }, + buttonText: { + prev: "Προηγούμενος", + next: "Επόμενος", + today: "Σήμερα", + month: "Μήνας", + week: "Εβδομάδα", + day: "Ημέρα", + list: "Ατζέντα" + }, + weekLabel: "Εβδ", + allDayText: "Ολοήμερο", + eventLimitText: "περισσότερα", + noEventsMessage: "Δεν υπάρχουν γεγονότα προς εμφάνιση" + }; + + var _m16 = { + code: "en-au", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + } + }; + + var _m17 = { + code: "en-gb", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + } + }; + + var _m18 = { + code: "en-nz", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + } + }; + + var _m19 = { + code: "es", + week: { + dow: 0, + doy: 6 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "Ant", + next: "Sig", + today: "Hoy", + month: "Mes", + week: "Semana", + day: "Día", + list: "Agenda" + }, + weekLabel: "Sm", + allDayHtml: "Todo<br/>el día", + eventLimitText: "más", + noEventsMessage: "No hay eventos para mostrar" + }; + + var _m20 = { + code: "es", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Ant", + next: "Sig", + today: "Hoy", + month: "Mes", + week: "Semana", + day: "Día", + list: "Agenda" + }, + weekLabel: "Sm", + allDayHtml: "Todo<br/>el día", + eventLimitText: "más", + noEventsMessage: "No hay eventos para mostrar" + }; + + var _m21 = { + code: "et", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Eelnev", + next: "Järgnev", + today: "Täna", + month: "Kuu", + week: "Nädal", + day: "Päev", + list: "Päevakord" + }, + weekLabel: "näd", + allDayText: "Kogu päev", + eventLimitText: function (n) { + return "+ veel " + n; + }, + noEventsMessage: "Kuvamiseks puuduvad sündmused" + }; + + var _m22 = { + code: "eu", + week: { + dow: 1, + doy: 7 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "Aur", + next: "Hur", + today: "Gaur", + month: "Hilabetea", + week: "Astea", + day: "Eguna", + list: "Agenda" + }, + weekLabel: "As", + allDayHtml: "Egun<br/>osoa", + eventLimitText: "gehiago", + noEventsMessage: "Ez dago ekitaldirik erakusteko" + }; + + var _m23 = { + code: "fa", + week: { + dow: 6, + doy: 12 // The week that contains Jan 1st is the first week of the year. + }, + dir: 'rtl', + buttonText: { + prev: "قبلی", + next: "بعدی", + today: "امروز", + month: "ماه", + week: "هفته", + day: "روز", + list: "برنامه" + }, + weekLabel: "هف", + allDayText: "تمام روز", + eventLimitText: function (n) { + return "بیش از " + n; + }, + noEventsMessage: "هیچ رویدادی به نمایش" + }; + + var _m24 = { + code: "fi", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Edellinen", + next: "Seuraava", + today: "Tänään", + month: "Kuukausi", + week: "Viikko", + day: "Päivä", + list: "Tapahtumat" + }, + weekLabel: "Vk", + allDayText: "Koko päivä", + eventLimitText: "lisää", + noEventsMessage: "Ei näytettäviä tapahtumia" + }; + + var _m25 = { + code: "fr", + buttonText: { + prev: "Précédent", + next: "Suivant", + today: "Aujourd'hui", + year: "Année", + month: "Mois", + week: "Semaine", + day: "Jour", + list: "Mon planning" + }, + weekLabel: "Sem.", + allDayHtml: "Toute la<br/>journée", + eventLimitText: "en plus", + noEventsMessage: "Aucun événement à afficher" + }; + + var _m26 = { + code: "fr-ch", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Précédent", + next: "Suivant", + today: "Courant", + year: "Année", + month: "Mois", + week: "Semaine", + day: "Jour", + list: "Mon planning" + }, + weekLabel: "Sm", + allDayHtml: "Toute la<br/>journée", + eventLimitText: "en plus", + noEventsMessage: "Aucun événement à afficher" + }; + + var _m27 = { + code: "fr", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Précédent", + next: "Suivant", + today: "Aujourd'hui", + year: "Année", + month: "Mois", + week: "Semaine", + day: "Jour", + list: "Planning" + }, + weekLabel: "Sem.", + allDayHtml: "Toute la<br/>journée", + eventLimitText: "en plus", + noEventsMessage: "Aucun événement à afficher" + }; + + var _m28 = { + code: "gl", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Ant", + next: "Seg", + today: "Hoxe", + month: "Mes", + week: "Semana", + day: "Día", + list: "Axenda" + }, + weekLabel: "Sm", + allDayHtml: "Todo<br/>o día", + eventLimitText: "máis", + noEventsMessage: "Non hai eventos para amosar" + }; + + var _m29 = { + code: "he", + dir: 'rtl', + buttonText: { + prev: "הקודם", + next: "הבא", + today: "היום", + month: "חודש", + week: "שבוע", + day: "יום", + list: "סדר יום" + }, + allDayText: "כל היום", + eventLimitText: "אחר", + noEventsMessage: "אין אירועים להצגה", + weekLabel: "שבוע" + }; + + var _m30 = { + code: "hi", + week: { + dow: 0, + doy: 6 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "पिछला", + next: "अगला", + today: "आज", + month: "महीना", + week: "सप्ताह", + day: "दिन", + list: "कार्यसूची" + }, + weekLabel: "हफ्ता", + allDayText: "सभी दिन", + eventLimitText: function (n) { + return "+अधिक " + n; + }, + noEventsMessage: "कोई घटनाओं को प्रदर्शित करने के लिए" + }; + + var _m31 = { + code: "hr", + week: { + dow: 1, + doy: 7 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "Prijašnji", + next: "Sljedeći", + today: "Danas", + month: "Mjesec", + week: "Tjedan", + day: "Dan", + list: "Raspored" + }, + weekLabel: "Tje", + allDayText: "Cijeli dan", + eventLimitText: function (n) { + return "+ još " + n; + }, + noEventsMessage: "Nema događaja za prikaz" + }; + + var _m32 = { + code: "hu", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "vissza", + next: "előre", + today: "ma", + month: "Hónap", + week: "Hét", + day: "Nap", + list: "Napló" + }, + weekLabel: "Hét", + allDayText: "Egész nap", + eventLimitText: "további", + noEventsMessage: "Nincs megjeleníthető esemény" + }; + + var _m33 = { + code: "id", + week: { + dow: 1, + doy: 7 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "mundur", + next: "maju", + today: "hari ini", + month: "Bulan", + week: "Minggu", + day: "Hari", + list: "Agenda" + }, + weekLabel: "Mg", + allDayHtml: "Sehari<br/>penuh", + eventLimitText: "lebih", + noEventsMessage: "Tidak ada acara untuk ditampilkan" + }; + + var _m34 = { + code: "is", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Fyrri", + next: "Næsti", + today: "Í dag", + month: "Mánuður", + week: "Vika", + day: "Dagur", + list: "Dagskrá" + }, + weekLabel: "Vika", + allDayHtml: "Allan<br/>daginn", + eventLimitText: "meira", + noEventsMessage: "Engir viðburðir til að sýna" + }; + + var _m35 = { + code: "it", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Prec", + next: "Succ", + today: "Oggi", + month: "Mese", + week: "Settimana", + day: "Giorno", + list: "Agenda" + }, + weekLabel: "Sm", + allDayHtml: "Tutto il<br/>giorno", + eventLimitText: function (n) { + return "+altri " + n; + }, + noEventsMessage: "Non ci sono eventi da visualizzare" + }; + + var _m36 = { + code: "ja", + buttonText: { + prev: "前", + next: "次", + today: "今日", + month: "月", + week: "週", + day: "日", + list: "予定リスト" + }, + weekLabel: "週", + allDayText: "終日", + eventLimitText: function (n) { + return "他 " + n + " 件"; + }, + noEventsMessage: "表示する予定はありません" + }; + + var _m37 = { + code: "ka", + week: { + dow: 1, + doy: 7 + }, + buttonText: { + prev: "წინა", + next: "შემდეგი", + today: "დღეს", + month: "თვე", + week: "კვირა", + day: "დღე", + list: "დღის წესრიგი" + }, + weekLabel: "კვ", + allDayText: "მთელი დღე", + eventLimitText: function (n) { + return "+ კიდევ " + n; + }, + noEventsMessage: "ღონისძიებები არ არის" + }; + + var _m38 = { + code: "kk", + week: { + dow: 1, + doy: 7 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "Алдыңғы", + next: "Келесі", + today: "Бүгін", + month: "Ай", + week: "Апта", + day: "Күн", + list: "Күн тәртібі" + }, + weekLabel: "Не", + allDayText: "Күні бойы", + eventLimitText: function (n) { + return "+ тағы " + n; + }, + noEventsMessage: "Көрсету үшін оқиғалар жоқ" + }; + + var _m39 = { + code: "ko", + buttonText: { + prev: "이전달", + next: "다음달", + today: "오늘", + month: "월", + week: "주", + day: "일", + list: "일정목록" + }, + weekLabel: "주", + allDayText: "종일", + eventLimitText: "개", + noEventsMessage: "일정이 없습니다" + }; + + var _m40 = { + code: "lb", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Zréck", + next: "Weider", + today: "Haut", + month: "Mount", + week: "Woch", + day: "Dag", + list: "Terminiwwersiicht" + }, + weekLabel: "W", + allDayText: "Ganzen Dag", + eventLimitText: "méi", + noEventsMessage: "Nee Evenementer ze affichéieren" + }; + + var _m41 = { + code: "lt", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Atgal", + next: "Pirmyn", + today: "Šiandien", + month: "Mėnuo", + week: "Savaitė", + day: "Diena", + list: "Darbotvarkė" + }, + weekLabel: "SAV", + allDayText: "Visą dieną", + eventLimitText: "daugiau", + noEventsMessage: "Nėra įvykių rodyti" + }; + + var _m42 = { + code: "lv", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Iepr.", + next: "Nāk.", + today: "Šodien", + month: "Mēnesis", + week: "Nedēļa", + day: "Diena", + list: "Dienas kārtība" + }, + weekLabel: "Ned.", + allDayText: "Visu dienu", + eventLimitText: function (n) { + return "+vēl " + n; + }, + noEventsMessage: "Nav notikumu" + }; + + var _m43 = { + code: "mk", + buttonText: { + prev: "претходно", + next: "следно", + today: "Денес", + month: "Месец", + week: "Недела", + day: "Ден", + list: "График" + }, + weekLabel: "Сед", + allDayText: "Цел ден", + eventLimitText: function (n) { + return "+повеќе " + n; + }, + noEventsMessage: "Нема настани за прикажување" + }; + + var _m44 = { + code: "ms", + week: { + dow: 1, + doy: 7 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "Sebelum", + next: "Selepas", + today: "hari ini", + month: "Bulan", + week: "Minggu", + day: "Hari", + list: "Agenda" + }, + weekLabel: "Mg", + allDayText: "Sepanjang hari", + eventLimitText: function (n) { + return "masih ada " + n + " acara"; + }, + noEventsMessage: "Tiada peristiwa untuk dipaparkan" + }; + + var _m45 = { + code: "nb", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Forrige", + next: "Neste", + today: "I dag", + month: "Måned", + week: "Uke", + day: "Dag", + list: "Agenda" + }, + weekLabel: "Uke", + allDayText: "Hele dagen", + eventLimitText: "til", + noEventsMessage: "Ingen hendelser å vise" + }; + + var _m46 = { + code: "nl", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Voorgaand", + next: "Volgende", + today: "Vandaag", + year: "Jaar", + month: "Maand", + week: "Week", + day: "Dag", + list: "Agenda" + }, + allDayText: "Hele dag", + eventLimitText: "extra", + noEventsMessage: "Geen evenementen om te laten zien" + }; + + var _m47 = { + code: "nn", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Førre", + next: "Neste", + today: "I dag", + month: "Månad", + week: "Veke", + day: "Dag", + list: "Agenda" + }, + weekLabel: "Veke", + allDayText: "Heile dagen", + eventLimitText: "til", + noEventsMessage: "Ingen hendelser å vise" + }; + + var _m48 = { + code: "pl", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Poprzedni", + next: "Następny", + today: "Dziś", + month: "Miesiąc", + week: "Tydzień", + day: "Dzień", + list: "Plan dnia" + }, + weekLabel: "Tydz", + allDayText: "Cały dzień", + eventLimitText: "więcej", + noEventsMessage: "Brak wydarzeń do wyświetlenia" + }; + + var _m49 = { + code: "pt-br", + buttonText: { + prev: "Anterior", + next: "Próximo", + today: "Hoje", + month: "Mês", + week: "Semana", + day: "Dia", + list: "Lista" + }, + weekLabel: "Sm", + allDayText: "dia inteiro", + eventLimitText: function (n) { + return "mais +" + n; + }, + noEventsMessage: "Não há eventos para mostrar" + }; + + var _m50 = { + code: "pt", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Anterior", + next: "Seguinte", + today: "Hoje", + month: "Mês", + week: "Semana", + day: "Dia", + list: "Agenda" + }, + weekLabel: "Sem", + allDayText: "Todo o dia", + eventLimitText: "mais", + noEventsMessage: "Não há eventos para mostrar" + }; + + var _m51 = { + code: "ro", + week: { + dow: 1, + doy: 7 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "precedentă", + next: "următoare", + today: "Azi", + month: "Lună", + week: "Săptămână", + day: "Zi", + list: "Agendă" + }, + weekLabel: "Săpt", + allDayText: "Toată ziua", + eventLimitText: function (n) { + return "+alte " + n; + }, + noEventsMessage: "Nu există evenimente de afișat" + }; + + var _m52 = { + code: "ru", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Пред", + next: "След", + today: "Сегодня", + month: "Месяц", + week: "Неделя", + day: "День", + list: "Повестка дня" + }, + weekLabel: "Нед", + allDayText: "Весь день", + eventLimitText: function (n) { + return "+ ещё " + n; + }, + noEventsMessage: "Нет событий для отображения" + }; + + var _m53 = { + code: "sk", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Predchádzajúci", + next: "Nasledujúci", + today: "Dnes", + month: "Mesiac", + week: "Týždeň", + day: "Deň", + list: "Rozvrh" + }, + weekLabel: "Ty", + allDayText: "Celý deň", + eventLimitText: function (n) { + return "+ďalšie: " + n; + }, + noEventsMessage: "Žiadne akcie na zobrazenie" + }; + + var _m54 = { + code: "sl", + week: { + dow: 1, + doy: 7 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "Prejšnji", + next: "Naslednji", + today: "Trenutni", + month: "Mesec", + week: "Teden", + day: "Dan", + list: "Dnevni red" + }, + weekLabel: "Teden", + allDayText: "Ves dan", + eventLimitText: "več", + noEventsMessage: "Ni dogodkov za prikaz" + }; + + var _m55 = { + code: "sq", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "mbrapa", + next: "Përpara", + today: "sot", + month: "Muaj", + week: "Javë", + day: "Ditë", + list: "Listë" + }, + weekLabel: "Ja", + allDayHtml: "Gjithë<br/>ditën", + eventLimitText: function (n) { + return "+më tepër " + n; + }, + noEventsMessage: "Nuk ka evente për të shfaqur" + }; + + var _m56 = { + code: "sr-cyrl", + week: { + dow: 1, + doy: 7 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "Претходна", + next: "следећи", + today: "Данас", + month: "Месец", + week: "Недеља", + day: "Дан", + list: "Планер" + }, + weekLabel: "Сед", + allDayText: "Цео дан", + eventLimitText: function (n) { + return "+ још " + n; + }, + noEventsMessage: "Нема догађаја за приказ" + }; + + var _m57 = { + code: "sr", + week: { + dow: 1, + doy: 7 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "Prethodna", + next: "Sledeći", + today: "Danas", + month: "Mеsеc", + week: "Nеdеlja", + day: "Dan", + list: "Planеr" + }, + weekLabel: "Sed", + allDayText: "Cеo dan", + eventLimitText: function (n) { + return "+ još " + n; + }, + noEventsMessage: "Nеma događaja za prikaz" + }; + + var _m58 = { + code: "sv", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Förra", + next: "Nästa", + today: "Idag", + month: "Månad", + week: "Vecka", + day: "Dag", + list: "Program" + }, + weekLabel: "v.", + allDayText: "Heldag", + eventLimitText: "till", + noEventsMessage: "Inga händelser att visa" + }; + + var _m59 = { + code: "th", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "ก่อนหน้า", + next: "ถัดไป", + prevYear: 'ปีก่อนหน้า', + nextYear: 'ปีถัดไป', + year: 'ปี', + today: "วันนี้", + month: "เดือน", + week: "สัปดาห์", + day: "วัน", + list: "กำหนดการ" + }, + weekLabel: "สัปดาห์", + allDayText: "ตลอดวัน", + eventLimitText: "เพิ่มเติม", + noEventsMessage: "ไม่มีกิจกรรมที่จะแสดง" + }; + + var _m60 = { + code: "tr", + week: { + dow: 1, + doy: 7 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "geri", + next: "ileri", + today: "bugün", + month: "Ay", + week: "Hafta", + day: "Gün", + list: "Ajanda" + }, + weekLabel: "Hf", + allDayText: "Tüm gün", + eventLimitText: "daha fazla", + noEventsMessage: "Gösterilecek etkinlik yok" + }; + + var _m61 = { + code: "ug", + buttonText: { + month: "ئاي", + week: "ھەپتە", + day: "كۈن", + list: "كۈنتەرتىپ" + }, + allDayText: "پۈتۈن كۈن" + }; + + var _m62 = { + code: "uk", + week: { + dow: 1, + doy: 7 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "Попередній", + next: "далі", + today: "Сьогодні", + month: "Місяць", + week: "Тиждень", + day: "День", + list: "Порядок денний" + }, + weekLabel: "Тиж", + allDayText: "Увесь день", + eventLimitText: function (n) { + return "+ще " + n + "..."; + }, + noEventsMessage: "Немає подій для відображення" + }; + + var _m63 = { + code: "uz", + buttonText: { + month: "Oy", + week: "Xafta", + day: "Kun", + list: "Kun tartibi" + }, + allDayText: "Kun bo'yi", + eventLimitText: function (n) { + return "+ yana " + n; + }, + noEventsMessage: "Ko'rsatish uchun voqealar yo'q" + }; + + var _m64 = { + code: "vi", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Trước", + next: "Tiếp", + today: "Hôm nay", + month: "Tháng", + week: "Tuần", + day: "Ngày", + list: "Lịch biểu" + }, + weekLabel: "Tu", + allDayText: "Cả ngày", + eventLimitText: function (n) { + return "+ thêm " + n; + }, + noEventsMessage: "Không có sự kiện để hiển thị" + }; + + var _m65 = { + code: "zh-cn", + week: { + // GB/T 7408-1994《数据元和交换格式·信息交换·日期和时间表示法》与ISO 8601:1988等效 + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "上月", + next: "下月", + today: "今天", + month: "月", + week: "周", + day: "日", + list: "日程" + }, + weekLabel: "周", + allDayText: "全天", + eventLimitText: function (n) { + return "另外 " + n + " 个"; + }, + noEventsMessage: "没有事件显示" + }; + + var _m66 = { + code: "zh-tw", + buttonText: { + prev: "上月", + next: "下月", + today: "今天", + month: "月", + week: "週", + day: "天", + list: "活動列表" + }, + weekLabel: "周", + allDayText: "整天", + eventLimitText: '顯示更多', + noEventsMessage: "没有任何活動" + }; + + var _rollupPluginMultiEntry_entryPoint = [ + _m0, _m1, _m2, _m3, _m4, _m5, _m6, _m7, _m8, _m9, _m10, _m11, _m12, _m13, _m14, _m15, _m16, _m17, _m18, _m19, _m20, _m21, _m22, _m23, _m24, _m25, _m26, _m27, _m28, _m29, _m30, _m31, _m32, _m33, _m34, _m35, _m36, _m37, _m38, _m39, _m40, _m41, _m42, _m43, _m44, _m45, _m46, _m47, _m48, _m49, _m50, _m51, _m52, _m53, _m54, _m55, _m56, _m57, _m58, _m59, _m60, _m61, _m62, _m63, _m64, _m65, _m66 + ]; + + return _rollupPluginMultiEntry_entryPoint; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/af.js b/addons/web/static/lib/fullcalendar/core/locales/af.js new file mode 100644 index 00000000000..ee9f9f74756 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/af.js @@ -0,0 +1,30 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.af = factory())); +}(this, function () { 'use strict'; + + var af = { + code: "af", + week: { + dow: 1, + doy: 4 // Die week wat die 4de Januarie bevat is die eerste week van die jaar. + }, + buttonText: { + prev: "Vorige", + next: "Volgende", + today: "Vandag", + year: "Jaar", + month: "Maand", + week: "Week", + day: "Dag", + list: "Agenda" + }, + allDayHtml: "Heeldag", + eventLimitText: "Addisionele", + noEventsMessage: "Daar is geen gebeurtenisse nie" + }; + + return af; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/ar-dz.js b/addons/web/static/lib/fullcalendar/core/locales/ar-dz.js new file mode 100644 index 00000000000..201eb171ae6 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/ar-dz.js @@ -0,0 +1,31 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales['ar-dz'] = factory())); +}(this, function () { 'use strict'; + + var arDz = { + code: "ar-dz", + week: { + dow: 0, + doy: 4 // The week that contains Jan 1st is the first week of the year. + }, + dir: 'rtl', + buttonText: { + prev: "السابق", + next: "التالي", + today: "اليوم", + month: "شهر", + week: "أسبوع", + day: "يوم", + list: "أجندة" + }, + weekLabel: "أسبوع", + allDayText: "اليوم كله", + eventLimitText: "أخرى", + noEventsMessage: "أي أحداث لعرض" + }; + + return arDz; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/ar-kw.js b/addons/web/static/lib/fullcalendar/core/locales/ar-kw.js new file mode 100644 index 00000000000..94c69001466 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/ar-kw.js @@ -0,0 +1,31 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales['ar-kw'] = factory())); +}(this, function () { 'use strict'; + + var arKw = { + code: "ar-kw", + week: { + dow: 0, + doy: 12 // The week that contains Jan 1st is the first week of the year. + }, + dir: 'rtl', + buttonText: { + prev: "السابق", + next: "التالي", + today: "اليوم", + month: "شهر", + week: "أسبوع", + day: "يوم", + list: "أجندة" + }, + weekLabel: "أسبوع", + allDayText: "اليوم كله", + eventLimitText: "أخرى", + noEventsMessage: "أي أحداث لعرض" + }; + + return arKw; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/ar-ly.js b/addons/web/static/lib/fullcalendar/core/locales/ar-ly.js new file mode 100644 index 00000000000..e1c8aeb07bc --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/ar-ly.js @@ -0,0 +1,31 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales['ar-ly'] = factory())); +}(this, function () { 'use strict'; + + var arLy = { + code: "ar-ly", + week: { + dow: 6, + doy: 12 // The week that contains Jan 1st is the first week of the year. + }, + dir: 'rtl', + buttonText: { + prev: "السابق", + next: "التالي", + today: "اليوم", + month: "شهر", + week: "أسبوع", + day: "يوم", + list: "أجندة" + }, + weekLabel: "أسبوع", + allDayText: "اليوم كله", + eventLimitText: "أخرى", + noEventsMessage: "أي أحداث لعرض" + }; + + return arLy; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/ar-ma.js b/addons/web/static/lib/fullcalendar/core/locales/ar-ma.js new file mode 100644 index 00000000000..00cc7c67906 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/ar-ma.js @@ -0,0 +1,31 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales['ar-ma'] = factory())); +}(this, function () { 'use strict'; + + var arMa = { + code: "ar-ma", + week: { + dow: 6, + doy: 12 // The week that contains Jan 1st is the first week of the year. + }, + dir: 'rtl', + buttonText: { + prev: "السابق", + next: "التالي", + today: "اليوم", + month: "شهر", + week: "أسبوع", + day: "يوم", + list: "أجندة" + }, + weekLabel: "أسبوع", + allDayText: "اليوم كله", + eventLimitText: "أخرى", + noEventsMessage: "أي أحداث لعرض" + }; + + return arMa; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/ar-sa.js b/addons/web/static/lib/fullcalendar/core/locales/ar-sa.js new file mode 100644 index 00000000000..0361f6d8701 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/ar-sa.js @@ -0,0 +1,31 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales['ar-sa'] = factory())); +}(this, function () { 'use strict'; + + var arSa = { + code: "ar-sa", + week: { + dow: 0, + doy: 6 // The week that contains Jan 1st is the first week of the year. + }, + dir: 'rtl', + buttonText: { + prev: "السابق", + next: "التالي", + today: "اليوم", + month: "شهر", + week: "أسبوع", + day: "يوم", + list: "أجندة" + }, + weekLabel: "أسبوع", + allDayText: "اليوم كله", + eventLimitText: "أخرى", + noEventsMessage: "أي أحداث لعرض" + }; + + return arSa; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/ar-tn.js b/addons/web/static/lib/fullcalendar/core/locales/ar-tn.js new file mode 100644 index 00000000000..57a07f8f514 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/ar-tn.js @@ -0,0 +1,31 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales['ar-tn'] = factory())); +}(this, function () { 'use strict'; + + var arTn = { + code: "ar-tn", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + dir: 'rtl', + buttonText: { + prev: "السابق", + next: "التالي", + today: "اليوم", + month: "شهر", + week: "أسبوع", + day: "يوم", + list: "أجندة" + }, + weekLabel: "أسبوع", + allDayText: "اليوم كله", + eventLimitText: "أخرى", + noEventsMessage: "أي أحداث لعرض" + }; + + return arTn; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/ar.js b/addons/web/static/lib/fullcalendar/core/locales/ar.js new file mode 100644 index 00000000000..f789afd151e --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/ar.js @@ -0,0 +1,31 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.ar = factory())); +}(this, function () { 'use strict'; + + var ar = { + code: "ar", + week: { + dow: 6, + doy: 12 // The week that contains Jan 1st is the first week of the year. + }, + dir: 'rtl', + buttonText: { + prev: "السابق", + next: "التالي", + today: "اليوم", + month: "شهر", + week: "أسبوع", + day: "يوم", + list: "أجندة" + }, + weekLabel: "أسبوع", + allDayText: "اليوم كله", + eventLimitText: "أخرى", + noEventsMessage: "أي أحداث لعرض" + }; + + return ar; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/az.js b/addons/web/static/lib/fullcalendar/core/locales/az.js new file mode 100644 index 00000000000..fbd2dc35cf8 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/az.js @@ -0,0 +1,32 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.az = factory())); +}(this, function () { 'use strict'; + + var az = { + code: "az", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Əvvəl", + next: "Sonra", + today: "Bu Gün", + month: "Ay", + week: "Həftə", + day: "Gün", + list: "Gündəm" + }, + weekLabel: "Həftə", + allDayText: "Bütün Gün", + eventLimitText: function (n) { + return "+ daha çox " + n; + }, + noEventsMessage: "Göstərmək üçün hadisə yoxdur" + }; + + return az; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/bg.js b/addons/web/static/lib/fullcalendar/core/locales/bg.js new file mode 100644 index 00000000000..e7343a6c5ab --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/bg.js @@ -0,0 +1,31 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.bg = factory())); +}(this, function () { 'use strict'; + + var bg = { + code: "bg", + week: { + dow: 1, + doy: 7 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "назад", + next: "напред", + today: "днес", + month: "Месец", + week: "Седмица", + day: "Ден", + list: "График" + }, + allDayText: "Цял ден", + eventLimitText: function (n) { + return "+още " + n; + }, + noEventsMessage: "Няма събития за показване" + }; + + return bg; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/bs.js b/addons/web/static/lib/fullcalendar/core/locales/bs.js new file mode 100644 index 00000000000..d96b8adb3d9 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/bs.js @@ -0,0 +1,32 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.bs = factory())); +}(this, function () { 'use strict'; + + var bs = { + code: "bs", + week: { + dow: 1, + doy: 7 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "Prošli", + next: "Sljedeći", + today: "Danas", + month: "Mjesec", + week: "Sedmica", + day: "Dan", + list: "Raspored" + }, + weekLabel: "Sed", + allDayText: "Cijeli dan", + eventLimitText: function (n) { + return "+ još " + n; + }, + noEventsMessage: "Nema događaja za prikazivanje" + }; + + return bs; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/ca.js b/addons/web/static/lib/fullcalendar/core/locales/ca.js new file mode 100644 index 00000000000..d2d3e2aa770 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/ca.js @@ -0,0 +1,30 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.ca = factory())); +}(this, function () { 'use strict'; + + var ca = { + code: "ca", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Anterior", + next: "Següent", + today: "Avui", + month: "Mes", + week: "Setmana", + day: "Dia", + list: "Agenda" + }, + weekLabel: "Set", + allDayText: "Tot el dia", + eventLimitText: "més", + noEventsMessage: "No hi ha esdeveniments per mostrar" + }; + + return ca; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/cs.js b/addons/web/static/lib/fullcalendar/core/locales/cs.js new file mode 100644 index 00000000000..2624e3607df --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/cs.js @@ -0,0 +1,32 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.cs = factory())); +}(this, function () { 'use strict'; + + var cs = { + code: "cs", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Dříve", + next: "Později", + today: "Nyní", + month: "Měsíc", + week: "Týden", + day: "Den", + list: "Agenda" + }, + weekLabel: "Týd", + allDayText: "Celý den", + eventLimitText: function (n) { + return "+další: " + n; + }, + noEventsMessage: "Žádné akce k zobrazení" + }; + + return cs; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/da.js b/addons/web/static/lib/fullcalendar/core/locales/da.js new file mode 100644 index 00000000000..73d15592990 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/da.js @@ -0,0 +1,30 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.da = factory())); +}(this, function () { 'use strict'; + + var da = { + code: "da", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Forrige", + next: "Næste", + today: "I dag", + month: "Måned", + week: "Uge", + day: "Dag", + list: "Agenda" + }, + weekLabel: "Uge", + allDayText: "Hele dagen", + eventLimitText: "flere", + noEventsMessage: "Ingen arrangementer at vise" + }; + + return da; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/de.js b/addons/web/static/lib/fullcalendar/core/locales/de.js new file mode 100644 index 00000000000..ab5a815a0cc --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/de.js @@ -0,0 +1,33 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.de = factory())); +}(this, function () { 'use strict'; + + var de = { + code: "de", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Zurück", + next: "Vor", + today: "Heute", + year: "Jahr", + month: "Monat", + week: "Woche", + day: "Tag", + list: "Terminübersicht" + }, + weekLabel: "KW", + allDayText: "Ganztägig", + eventLimitText: function (n) { + return "+ weitere " + n; + }, + noEventsMessage: "Keine Ereignisse anzuzeigen" + }; + + return de; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/el.js b/addons/web/static/lib/fullcalendar/core/locales/el.js new file mode 100644 index 00000000000..cdc10a66e4a --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/el.js @@ -0,0 +1,30 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.el = factory())); +}(this, function () { 'use strict'; + + var el = { + code: "el", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4st is the first week of the year. + }, + buttonText: { + prev: "Προηγούμενος", + next: "Επόμενος", + today: "Σήμερα", + month: "Μήνας", + week: "Εβδομάδα", + day: "Ημέρα", + list: "Ατζέντα" + }, + weekLabel: "Εβδ", + allDayText: "Ολοήμερο", + eventLimitText: "περισσότερα", + noEventsMessage: "Δεν υπάρχουν γεγονότα προς εμφάνιση" + }; + + return el; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/en-au.js b/addons/web/static/lib/fullcalendar/core/locales/en-au.js new file mode 100644 index 00000000000..be10bfb66ea --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/en-au.js @@ -0,0 +1,17 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales['en-au'] = factory())); +}(this, function () { 'use strict'; + + var enAu = { + code: "en-au", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + } + }; + + return enAu; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/en-gb.js b/addons/web/static/lib/fullcalendar/core/locales/en-gb.js new file mode 100644 index 00000000000..8a4a84e6b42 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/en-gb.js @@ -0,0 +1,17 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales['en-gb'] = factory())); +}(this, function () { 'use strict'; + + var enGb = { + code: "en-gb", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + } + }; + + return enGb; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/en-nz.js b/addons/web/static/lib/fullcalendar/core/locales/en-nz.js new file mode 100644 index 00000000000..df56c145506 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/en-nz.js @@ -0,0 +1,17 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales['en-nz'] = factory())); +}(this, function () { 'use strict'; + + var enNz = { + code: "en-nz", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + } + }; + + return enNz; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/es-us.js b/addons/web/static/lib/fullcalendar/core/locales/es-us.js new file mode 100644 index 00000000000..1efa89a4d18 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/es-us.js @@ -0,0 +1,30 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales['es-us'] = factory())); +}(this, function () { 'use strict'; + + var esUs = { + code: "es", + week: { + dow: 0, + doy: 6 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "Ant", + next: "Sig", + today: "Hoy", + month: "Mes", + week: "Semana", + day: "Día", + list: "Agenda" + }, + weekLabel: "Sm", + allDayHtml: "Todo<br/>el día", + eventLimitText: "más", + noEventsMessage: "No hay eventos para mostrar" + }; + + return esUs; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/es.js b/addons/web/static/lib/fullcalendar/core/locales/es.js new file mode 100644 index 00000000000..bfd9af4c626 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/es.js @@ -0,0 +1,30 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.es = factory())); +}(this, function () { 'use strict'; + + var es = { + code: "es", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Ant", + next: "Sig", + today: "Hoy", + month: "Mes", + week: "Semana", + day: "Día", + list: "Agenda" + }, + weekLabel: "Sm", + allDayHtml: "Todo<br/>el día", + eventLimitText: "más", + noEventsMessage: "No hay eventos para mostrar" + }; + + return es; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/et.js b/addons/web/static/lib/fullcalendar/core/locales/et.js new file mode 100644 index 00000000000..c44fcaec9e5 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/et.js @@ -0,0 +1,32 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.et = factory())); +}(this, function () { 'use strict'; + + var et = { + code: "et", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Eelnev", + next: "Järgnev", + today: "Täna", + month: "Kuu", + week: "Nädal", + day: "Päev", + list: "Päevakord" + }, + weekLabel: "näd", + allDayText: "Kogu päev", + eventLimitText: function (n) { + return "+ veel " + n; + }, + noEventsMessage: "Kuvamiseks puuduvad sündmused" + }; + + return et; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/eu.js b/addons/web/static/lib/fullcalendar/core/locales/eu.js new file mode 100644 index 00000000000..91903aaaf17 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/eu.js @@ -0,0 +1,30 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.eu = factory())); +}(this, function () { 'use strict'; + + var eu = { + code: "eu", + week: { + dow: 1, + doy: 7 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "Aur", + next: "Hur", + today: "Gaur", + month: "Hilabetea", + week: "Astea", + day: "Eguna", + list: "Agenda" + }, + weekLabel: "As", + allDayHtml: "Egun<br/>osoa", + eventLimitText: "gehiago", + noEventsMessage: "Ez dago ekitaldirik erakusteko" + }; + + return eu; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/fa.js b/addons/web/static/lib/fullcalendar/core/locales/fa.js new file mode 100644 index 00000000000..031fc7b304f --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/fa.js @@ -0,0 +1,33 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.fa = factory())); +}(this, function () { 'use strict'; + + var fa = { + code: "fa", + week: { + dow: 6, + doy: 12 // The week that contains Jan 1st is the first week of the year. + }, + dir: 'rtl', + buttonText: { + prev: "قبلی", + next: "بعدی", + today: "امروز", + month: "ماه", + week: "هفته", + day: "روز", + list: "برنامه" + }, + weekLabel: "هف", + allDayText: "تمام روز", + eventLimitText: function (n) { + return "بیش از " + n; + }, + noEventsMessage: "هیچ رویدادی به نمایش" + }; + + return fa; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/fi.js b/addons/web/static/lib/fullcalendar/core/locales/fi.js new file mode 100644 index 00000000000..3912845cf75 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/fi.js @@ -0,0 +1,30 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.fi = factory())); +}(this, function () { 'use strict'; + + var fi = { + code: "fi", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Edellinen", + next: "Seuraava", + today: "Tänään", + month: "Kuukausi", + week: "Viikko", + day: "Päivä", + list: "Tapahtumat" + }, + weekLabel: "Vk", + allDayText: "Koko päivä", + eventLimitText: "lisää", + noEventsMessage: "Ei näytettäviä tapahtumia" + }; + + return fi; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/fr-ca.js b/addons/web/static/lib/fullcalendar/core/locales/fr-ca.js new file mode 100644 index 00000000000..d554c1408f8 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/fr-ca.js @@ -0,0 +1,27 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales['fr-ca'] = factory())); +}(this, function () { 'use strict'; + + var frCa = { + code: "fr", + buttonText: { + prev: "Précédent", + next: "Suivant", + today: "Aujourd'hui", + year: "Année", + month: "Mois", + week: "Semaine", + day: "Jour", + list: "Mon planning" + }, + weekLabel: "Sem.", + allDayHtml: "Toute la<br/>journée", + eventLimitText: "en plus", + noEventsMessage: "Aucun événement à afficher" + }; + + return frCa; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/fr-ch.js b/addons/web/static/lib/fullcalendar/core/locales/fr-ch.js new file mode 100644 index 00000000000..358b8bf311a --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/fr-ch.js @@ -0,0 +1,31 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales['fr-ch'] = factory())); +}(this, function () { 'use strict'; + + var frCh = { + code: "fr-ch", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Précédent", + next: "Suivant", + today: "Courant", + year: "Année", + month: "Mois", + week: "Semaine", + day: "Jour", + list: "Mon planning" + }, + weekLabel: "Sm", + allDayHtml: "Toute la<br/>journée", + eventLimitText: "en plus", + noEventsMessage: "Aucun événement à afficher" + }; + + return frCh; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/fr.js b/addons/web/static/lib/fullcalendar/core/locales/fr.js new file mode 100644 index 00000000000..4760a71a9be --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/fr.js @@ -0,0 +1,31 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.fr = factory())); +}(this, function () { 'use strict'; + + var fr = { + code: "fr", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Précédent", + next: "Suivant", + today: "Aujourd'hui", + year: "Année", + month: "Mois", + week: "Semaine", + day: "Jour", + list: "Planning" + }, + weekLabel: "Sem.", + allDayHtml: "Toute la<br/>journée", + eventLimitText: "en plus", + noEventsMessage: "Aucun événement à afficher" + }; + + return fr; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/gl.js b/addons/web/static/lib/fullcalendar/core/locales/gl.js new file mode 100644 index 00000000000..721a6a89b25 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/gl.js @@ -0,0 +1,30 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.gl = factory())); +}(this, function () { 'use strict'; + + var gl = { + code: "gl", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Ant", + next: "Seg", + today: "Hoxe", + month: "Mes", + week: "Semana", + day: "Día", + list: "Axenda" + }, + weekLabel: "Sm", + allDayHtml: "Todo<br/>o día", + eventLimitText: "máis", + noEventsMessage: "Non hai eventos para amosar" + }; + + return gl; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/he.js b/addons/web/static/lib/fullcalendar/core/locales/he.js new file mode 100644 index 00000000000..3521d9e335b --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/he.js @@ -0,0 +1,27 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.he = factory())); +}(this, function () { 'use strict'; + + var he = { + code: "he", + dir: 'rtl', + buttonText: { + prev: "הקודם", + next: "הבא", + today: "היום", + month: "חודש", + week: "שבוע", + day: "יום", + list: "סדר יום" + }, + allDayText: "כל היום", + eventLimitText: "אחר", + noEventsMessage: "אין אירועים להצגה", + weekLabel: "שבוע" + }; + + return he; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/hi.js b/addons/web/static/lib/fullcalendar/core/locales/hi.js new file mode 100644 index 00000000000..15348e697f7 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/hi.js @@ -0,0 +1,32 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.hi = factory())); +}(this, function () { 'use strict'; + + var hi = { + code: "hi", + week: { + dow: 0, + doy: 6 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "पिछला", + next: "अगला", + today: "आज", + month: "महीना", + week: "सप्ताह", + day: "दिन", + list: "कार्यसूची" + }, + weekLabel: "हफ्ता", + allDayText: "सभी दिन", + eventLimitText: function (n) { + return "+अधिक " + n; + }, + noEventsMessage: "कोई घटनाओं को प्रदर्शित करने के लिए" + }; + + return hi; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/hr.js b/addons/web/static/lib/fullcalendar/core/locales/hr.js new file mode 100644 index 00000000000..295b4856634 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/hr.js @@ -0,0 +1,32 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.hr = factory())); +}(this, function () { 'use strict'; + + var hr = { + code: "hr", + week: { + dow: 1, + doy: 7 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "Prijašnji", + next: "Sljedeći", + today: "Danas", + month: "Mjesec", + week: "Tjedan", + day: "Dan", + list: "Raspored" + }, + weekLabel: "Tje", + allDayText: "Cijeli dan", + eventLimitText: function (n) { + return "+ još " + n; + }, + noEventsMessage: "Nema događaja za prikaz" + }; + + return hr; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/hu.js b/addons/web/static/lib/fullcalendar/core/locales/hu.js new file mode 100644 index 00000000000..2f0fe8acb6b --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/hu.js @@ -0,0 +1,30 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.hu = factory())); +}(this, function () { 'use strict'; + + var hu = { + code: "hu", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "vissza", + next: "előre", + today: "ma", + month: "Hónap", + week: "Hét", + day: "Nap", + list: "Napló" + }, + weekLabel: "Hét", + allDayText: "Egész nap", + eventLimitText: "további", + noEventsMessage: "Nincs megjeleníthető esemény" + }; + + return hu; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/id.js b/addons/web/static/lib/fullcalendar/core/locales/id.js new file mode 100644 index 00000000000..b742e80dda7 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/id.js @@ -0,0 +1,30 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.id = factory())); +}(this, function () { 'use strict'; + + var id = { + code: "id", + week: { + dow: 1, + doy: 7 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "mundur", + next: "maju", + today: "hari ini", + month: "Bulan", + week: "Minggu", + day: "Hari", + list: "Agenda" + }, + weekLabel: "Mg", + allDayHtml: "Sehari<br/>penuh", + eventLimitText: "lebih", + noEventsMessage: "Tidak ada acara untuk ditampilkan" + }; + + return id; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/is.js b/addons/web/static/lib/fullcalendar/core/locales/is.js new file mode 100644 index 00000000000..dd569bce72f --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/is.js @@ -0,0 +1,30 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.is = factory())); +}(this, function () { 'use strict'; + + var is = { + code: "is", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Fyrri", + next: "Næsti", + today: "Í dag", + month: "Mánuður", + week: "Vika", + day: "Dagur", + list: "Dagskrá" + }, + weekLabel: "Vika", + allDayHtml: "Allan<br/>daginn", + eventLimitText: "meira", + noEventsMessage: "Engir viðburðir til að sýna" + }; + + return is; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/it.js b/addons/web/static/lib/fullcalendar/core/locales/it.js new file mode 100644 index 00000000000..39a2829e522 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/it.js @@ -0,0 +1,32 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.it = factory())); +}(this, function () { 'use strict'; + + var it = { + code: "it", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Prec", + next: "Succ", + today: "Oggi", + month: "Mese", + week: "Settimana", + day: "Giorno", + list: "Agenda" + }, + weekLabel: "Sm", + allDayHtml: "Tutto il<br/>giorno", + eventLimitText: function (n) { + return "+altri " + n; + }, + noEventsMessage: "Non ci sono eventi da visualizzare" + }; + + return it; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/ja.js b/addons/web/static/lib/fullcalendar/core/locales/ja.js new file mode 100644 index 00000000000..eb4245b2a07 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/ja.js @@ -0,0 +1,28 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.ja = factory())); +}(this, function () { 'use strict'; + + var ja = { + code: "ja", + buttonText: { + prev: "前", + next: "次", + today: "今日", + month: "月", + week: "週", + day: "日", + list: "予定リスト" + }, + weekLabel: "週", + allDayText: "終日", + eventLimitText: function (n) { + return "他 " + n + " 件"; + }, + noEventsMessage: "表示する予定はありません" + }; + + return ja; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/ka.js b/addons/web/static/lib/fullcalendar/core/locales/ka.js new file mode 100644 index 00000000000..b971c033f81 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/ka.js @@ -0,0 +1,32 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.ka = factory())); +}(this, function () { 'use strict'; + + var ka = { + code: "ka", + week: { + dow: 1, + doy: 7 + }, + buttonText: { + prev: "წინა", + next: "შემდეგი", + today: "დღეს", + month: "თვე", + week: "კვირა", + day: "დღე", + list: "დღის წესრიგი" + }, + weekLabel: "კვ", + allDayText: "მთელი დღე", + eventLimitText: function (n) { + return "+ კიდევ " + n; + }, + noEventsMessage: "ღონისძიებები არ არის" + }; + + return ka; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/kk.js b/addons/web/static/lib/fullcalendar/core/locales/kk.js new file mode 100644 index 00000000000..5b19b99d510 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/kk.js @@ -0,0 +1,32 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.kk = factory())); +}(this, function () { 'use strict'; + + var kk = { + code: "kk", + week: { + dow: 1, + doy: 7 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "Алдыңғы", + next: "Келесі", + today: "Бүгін", + month: "Ай", + week: "Апта", + day: "Күн", + list: "Күн тәртібі" + }, + weekLabel: "Не", + allDayText: "Күні бойы", + eventLimitText: function (n) { + return "+ тағы " + n; + }, + noEventsMessage: "Көрсету үшін оқиғалар жоқ" + }; + + return kk; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/ko.js b/addons/web/static/lib/fullcalendar/core/locales/ko.js new file mode 100644 index 00000000000..ffe985d6c5d --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/ko.js @@ -0,0 +1,26 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.ko = factory())); +}(this, function () { 'use strict'; + + var ko = { + code: "ko", + buttonText: { + prev: "이전달", + next: "다음달", + today: "오늘", + month: "월", + week: "주", + day: "일", + list: "일정목록" + }, + weekLabel: "주", + allDayText: "종일", + eventLimitText: "개", + noEventsMessage: "일정이 없습니다" + }; + + return ko; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/lb.js b/addons/web/static/lib/fullcalendar/core/locales/lb.js new file mode 100644 index 00000000000..b9b17e3ec1f --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/lb.js @@ -0,0 +1,30 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.lb = factory())); +}(this, function () { 'use strict'; + + var lb = { + code: "lb", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Zréck", + next: "Weider", + today: "Haut", + month: "Mount", + week: "Woch", + day: "Dag", + list: "Terminiwwersiicht" + }, + weekLabel: "W", + allDayText: "Ganzen Dag", + eventLimitText: "méi", + noEventsMessage: "Nee Evenementer ze affichéieren" + }; + + return lb; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/lt.js b/addons/web/static/lib/fullcalendar/core/locales/lt.js new file mode 100644 index 00000000000..ec641b7500e --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/lt.js @@ -0,0 +1,30 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.lt = factory())); +}(this, function () { 'use strict'; + + var lt = { + code: "lt", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Atgal", + next: "Pirmyn", + today: "Šiandien", + month: "Mėnuo", + week: "Savaitė", + day: "Diena", + list: "Darbotvarkė" + }, + weekLabel: "SAV", + allDayText: "Visą dieną", + eventLimitText: "daugiau", + noEventsMessage: "Nėra įvykių rodyti" + }; + + return lt; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/lv.js b/addons/web/static/lib/fullcalendar/core/locales/lv.js new file mode 100644 index 00000000000..5453630df11 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/lv.js @@ -0,0 +1,32 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.lv = factory())); +}(this, function () { 'use strict'; + + var lv = { + code: "lv", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Iepr.", + next: "Nāk.", + today: "Šodien", + month: "Mēnesis", + week: "Nedēļa", + day: "Diena", + list: "Dienas kārtība" + }, + weekLabel: "Ned.", + allDayText: "Visu dienu", + eventLimitText: function (n) { + return "+vēl " + n; + }, + noEventsMessage: "Nav notikumu" + }; + + return lv; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/mk.js b/addons/web/static/lib/fullcalendar/core/locales/mk.js new file mode 100644 index 00000000000..6729fa63d39 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/mk.js @@ -0,0 +1,28 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.mk = factory())); +}(this, function () { 'use strict'; + + var mk = { + code: "mk", + buttonText: { + prev: "претходно", + next: "следно", + today: "Денес", + month: "Месец", + week: "Недела", + day: "Ден", + list: "График" + }, + weekLabel: "Сед", + allDayText: "Цел ден", + eventLimitText: function (n) { + return "+повеќе " + n; + }, + noEventsMessage: "Нема настани за прикажување" + }; + + return mk; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/ms.js b/addons/web/static/lib/fullcalendar/core/locales/ms.js new file mode 100644 index 00000000000..7205ecc7275 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/ms.js @@ -0,0 +1,32 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.ms = factory())); +}(this, function () { 'use strict'; + + var ms = { + code: "ms", + week: { + dow: 1, + doy: 7 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "Sebelum", + next: "Selepas", + today: "hari ini", + month: "Bulan", + week: "Minggu", + day: "Hari", + list: "Agenda" + }, + weekLabel: "Mg", + allDayText: "Sepanjang hari", + eventLimitText: function (n) { + return "masih ada " + n + " acara"; + }, + noEventsMessage: "Tiada peristiwa untuk dipaparkan" + }; + + return ms; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/nb.js b/addons/web/static/lib/fullcalendar/core/locales/nb.js new file mode 100644 index 00000000000..6464461c648 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/nb.js @@ -0,0 +1,30 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.nb = factory())); +}(this, function () { 'use strict'; + + var nb = { + code: "nb", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Forrige", + next: "Neste", + today: "I dag", + month: "Måned", + week: "Uke", + day: "Dag", + list: "Agenda" + }, + weekLabel: "Uke", + allDayText: "Hele dagen", + eventLimitText: "til", + noEventsMessage: "Ingen hendelser å vise" + }; + + return nb; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/nl.js b/addons/web/static/lib/fullcalendar/core/locales/nl.js new file mode 100644 index 00000000000..c91b5e55fdd --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/nl.js @@ -0,0 +1,30 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.nl = factory())); +}(this, function () { 'use strict'; + + var nl = { + code: "nl", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Voorgaand", + next: "Volgende", + today: "Vandaag", + year: "Jaar", + month: "Maand", + week: "Week", + day: "Dag", + list: "Agenda" + }, + allDayText: "Hele dag", + eventLimitText: "extra", + noEventsMessage: "Geen evenementen om te laten zien" + }; + + return nl; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/nn.js b/addons/web/static/lib/fullcalendar/core/locales/nn.js new file mode 100644 index 00000000000..a5cdd16268e --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/nn.js @@ -0,0 +1,30 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.nn = factory())); +}(this, function () { 'use strict'; + + var nn = { + code: "nn", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Førre", + next: "Neste", + today: "I dag", + month: "Månad", + week: "Veke", + day: "Dag", + list: "Agenda" + }, + weekLabel: "Veke", + allDayText: "Heile dagen", + eventLimitText: "til", + noEventsMessage: "Ingen hendelser å vise" + }; + + return nn; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/pl.js b/addons/web/static/lib/fullcalendar/core/locales/pl.js new file mode 100644 index 00000000000..0a22e69c436 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/pl.js @@ -0,0 +1,30 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.pl = factory())); +}(this, function () { 'use strict'; + + var pl = { + code: "pl", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Poprzedni", + next: "Następny", + today: "Dziś", + month: "Miesiąc", + week: "Tydzień", + day: "Dzień", + list: "Plan dnia" + }, + weekLabel: "Tydz", + allDayText: "Cały dzień", + eventLimitText: "więcej", + noEventsMessage: "Brak wydarzeń do wyświetlenia" + }; + + return pl; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/pt-br.js b/addons/web/static/lib/fullcalendar/core/locales/pt-br.js new file mode 100644 index 00000000000..bfa023dd665 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/pt-br.js @@ -0,0 +1,28 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales['pt-br'] = factory())); +}(this, function () { 'use strict'; + + var ptBr = { + code: "pt-br", + buttonText: { + prev: "Anterior", + next: "Próximo", + today: "Hoje", + month: "Mês", + week: "Semana", + day: "Dia", + list: "Lista" + }, + weekLabel: "Sm", + allDayText: "dia inteiro", + eventLimitText: function (n) { + return "mais +" + n; + }, + noEventsMessage: "Não há eventos para mostrar" + }; + + return ptBr; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/pt.js b/addons/web/static/lib/fullcalendar/core/locales/pt.js new file mode 100644 index 00000000000..5c54d8d401e --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/pt.js @@ -0,0 +1,30 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.pt = factory())); +}(this, function () { 'use strict'; + + var pt = { + code: "pt", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Anterior", + next: "Seguinte", + today: "Hoje", + month: "Mês", + week: "Semana", + day: "Dia", + list: "Agenda" + }, + weekLabel: "Sem", + allDayText: "Todo o dia", + eventLimitText: "mais", + noEventsMessage: "Não há eventos para mostrar" + }; + + return pt; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/ro.js b/addons/web/static/lib/fullcalendar/core/locales/ro.js new file mode 100644 index 00000000000..e8992f27628 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/ro.js @@ -0,0 +1,32 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.ro = factory())); +}(this, function () { 'use strict'; + + var ro = { + code: "ro", + week: { + dow: 1, + doy: 7 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "precedentă", + next: "următoare", + today: "Azi", + month: "Lună", + week: "Săptămână", + day: "Zi", + list: "Agendă" + }, + weekLabel: "Săpt", + allDayText: "Toată ziua", + eventLimitText: function (n) { + return "+alte " + n; + }, + noEventsMessage: "Nu există evenimente de afișat" + }; + + return ro; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/ru.js b/addons/web/static/lib/fullcalendar/core/locales/ru.js new file mode 100644 index 00000000000..77e0308e2c4 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/ru.js @@ -0,0 +1,32 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.ru = factory())); +}(this, function () { 'use strict'; + + var ru = { + code: "ru", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Пред", + next: "След", + today: "Сегодня", + month: "Месяц", + week: "Неделя", + day: "День", + list: "Повестка дня" + }, + weekLabel: "Нед", + allDayText: "Весь день", + eventLimitText: function (n) { + return "+ ещё " + n; + }, + noEventsMessage: "Нет событий для отображения" + }; + + return ru; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/sk.js b/addons/web/static/lib/fullcalendar/core/locales/sk.js new file mode 100644 index 00000000000..3513a64ad3b --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/sk.js @@ -0,0 +1,32 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.sk = factory())); +}(this, function () { 'use strict'; + + var sk = { + code: "sk", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Predchádzajúci", + next: "Nasledujúci", + today: "Dnes", + month: "Mesiac", + week: "Týždeň", + day: "Deň", + list: "Rozvrh" + }, + weekLabel: "Ty", + allDayText: "Celý deň", + eventLimitText: function (n) { + return "+ďalšie: " + n; + }, + noEventsMessage: "Žiadne akcie na zobrazenie" + }; + + return sk; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/sl.js b/addons/web/static/lib/fullcalendar/core/locales/sl.js new file mode 100644 index 00000000000..323355359ed --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/sl.js @@ -0,0 +1,30 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.sl = factory())); +}(this, function () { 'use strict'; + + var sl = { + code: "sl", + week: { + dow: 1, + doy: 7 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "Prejšnji", + next: "Naslednji", + today: "Trenutni", + month: "Mesec", + week: "Teden", + day: "Dan", + list: "Dnevni red" + }, + weekLabel: "Teden", + allDayText: "Ves dan", + eventLimitText: "več", + noEventsMessage: "Ni dogodkov za prikaz" + }; + + return sl; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/sq.js b/addons/web/static/lib/fullcalendar/core/locales/sq.js new file mode 100644 index 00000000000..0d43a522094 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/sq.js @@ -0,0 +1,32 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.sq = factory())); +}(this, function () { 'use strict'; + + var sq = { + code: "sq", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "mbrapa", + next: "Përpara", + today: "sot", + month: "Muaj", + week: "Javë", + day: "Ditë", + list: "Listë" + }, + weekLabel: "Ja", + allDayHtml: "Gjithë<br/>ditën", + eventLimitText: function (n) { + return "+më tepër " + n; + }, + noEventsMessage: "Nuk ka evente për të shfaqur" + }; + + return sq; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/sr-cyrl.js b/addons/web/static/lib/fullcalendar/core/locales/sr-cyrl.js new file mode 100644 index 00000000000..ba0d0dfa31e --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/sr-cyrl.js @@ -0,0 +1,32 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales['sr-cyrl'] = factory())); +}(this, function () { 'use strict'; + + var srCyrl = { + code: "sr-cyrl", + week: { + dow: 1, + doy: 7 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "Претходна", + next: "следећи", + today: "Данас", + month: "Месец", + week: "Недеља", + day: "Дан", + list: "Планер" + }, + weekLabel: "Сед", + allDayText: "Цео дан", + eventLimitText: function (n) { + return "+ још " + n; + }, + noEventsMessage: "Нема догађаја за приказ" + }; + + return srCyrl; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/sr.js b/addons/web/static/lib/fullcalendar/core/locales/sr.js new file mode 100644 index 00000000000..23e5c9b23fe --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/sr.js @@ -0,0 +1,32 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.sr = factory())); +}(this, function () { 'use strict'; + + var sr = { + code: "sr", + week: { + dow: 1, + doy: 7 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "Prethodna", + next: "Sledeći", + today: "Danas", + month: "Mеsеc", + week: "Nеdеlja", + day: "Dan", + list: "Planеr" + }, + weekLabel: "Sed", + allDayText: "Cеo dan", + eventLimitText: function (n) { + return "+ još " + n; + }, + noEventsMessage: "Nеma događaja za prikaz" + }; + + return sr; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/sv.js b/addons/web/static/lib/fullcalendar/core/locales/sv.js new file mode 100644 index 00000000000..a887060ba4e --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/sv.js @@ -0,0 +1,30 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.sv = factory())); +}(this, function () { 'use strict'; + + var sv = { + code: "sv", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Förra", + next: "Nästa", + today: "Idag", + month: "Månad", + week: "Vecka", + day: "Dag", + list: "Program" + }, + weekLabel: "v.", + allDayText: "Heldag", + eventLimitText: "till", + noEventsMessage: "Inga händelser att visa" + }; + + return sv; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/th.js b/addons/web/static/lib/fullcalendar/core/locales/th.js new file mode 100644 index 00000000000..faeaee2143c --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/th.js @@ -0,0 +1,33 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.th = factory())); +}(this, function () { 'use strict'; + + var th = { + code: "th", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "ก่อนหน้า", + next: "ถัดไป", + prevYear: 'ปีก่อนหน้า', + nextYear: 'ปีถัดไป', + year: 'ปี', + today: "วันนี้", + month: "เดือน", + week: "สัปดาห์", + day: "วัน", + list: "กำหนดการ" + }, + weekLabel: "สัปดาห์", + allDayText: "ตลอดวัน", + eventLimitText: "เพิ่มเติม", + noEventsMessage: "ไม่มีกิจกรรมที่จะแสดง" + }; + + return th; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/tr.js b/addons/web/static/lib/fullcalendar/core/locales/tr.js new file mode 100644 index 00000000000..48458982fb6 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/tr.js @@ -0,0 +1,30 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.tr = factory())); +}(this, function () { 'use strict'; + + var tr = { + code: "tr", + week: { + dow: 1, + doy: 7 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "geri", + next: "ileri", + today: "bugün", + month: "Ay", + week: "Hafta", + day: "Gün", + list: "Ajanda" + }, + weekLabel: "Hf", + allDayText: "Tüm gün", + eventLimitText: "daha fazla", + noEventsMessage: "Gösterilecek etkinlik yok" + }; + + return tr; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/ug.js b/addons/web/static/lib/fullcalendar/core/locales/ug.js new file mode 100644 index 00000000000..f13a5c28604 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/ug.js @@ -0,0 +1,20 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.ug = factory())); +}(this, function () { 'use strict'; + + var ug = { + code: "ug", + buttonText: { + month: "ئاي", + week: "ھەپتە", + day: "كۈن", + list: "كۈنتەرتىپ" + }, + allDayText: "پۈتۈن كۈن" + }; + + return ug; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/uk.js b/addons/web/static/lib/fullcalendar/core/locales/uk.js new file mode 100644 index 00000000000..de33f250c3e --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/uk.js @@ -0,0 +1,32 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.uk = factory())); +}(this, function () { 'use strict'; + + var uk = { + code: "uk", + week: { + dow: 1, + doy: 7 // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: "Попередній", + next: "далі", + today: "Сьогодні", + month: "Місяць", + week: "Тиждень", + day: "День", + list: "Порядок денний" + }, + weekLabel: "Тиж", + allDayText: "Увесь день", + eventLimitText: function (n) { + return "+ще " + n + "..."; + }, + noEventsMessage: "Немає подій для відображення" + }; + + return uk; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/uz.js b/addons/web/static/lib/fullcalendar/core/locales/uz.js new file mode 100644 index 00000000000..24089756dcf --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/uz.js @@ -0,0 +1,24 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.uz = factory())); +}(this, function () { 'use strict'; + + var uz = { + code: "uz", + buttonText: { + month: "Oy", + week: "Xafta", + day: "Kun", + list: "Kun tartibi" + }, + allDayText: "Kun bo'yi", + eventLimitText: function (n) { + return "+ yana " + n; + }, + noEventsMessage: "Ko'rsatish uchun voqealar yo'q" + }; + + return uz; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/vi.js b/addons/web/static/lib/fullcalendar/core/locales/vi.js new file mode 100644 index 00000000000..167ce11d74b --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/vi.js @@ -0,0 +1,32 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales.vi = factory())); +}(this, function () { 'use strict'; + + var vi = { + code: "vi", + week: { + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "Trước", + next: "Tiếp", + today: "Hôm nay", + month: "Tháng", + week: "Tuần", + day: "Ngày", + list: "Lịch biểu" + }, + weekLabel: "Tu", + allDayText: "Cả ngày", + eventLimitText: function (n) { + return "+ thêm " + n; + }, + noEventsMessage: "Không có sự kiện để hiển thị" + }; + + return vi; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/zh-cn.js b/addons/web/static/lib/fullcalendar/core/locales/zh-cn.js new file mode 100644 index 00000000000..4debbb9e60e --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/zh-cn.js @@ -0,0 +1,33 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales['zh-cn'] = factory())); +}(this, function () { 'use strict'; + + var zhCn = { + code: "zh-cn", + week: { + // GB/T 7408-1994《数据元和交换格式·信息交换·日期和时间表示法》与ISO 8601:1988等效 + dow: 1, + doy: 4 // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: "上月", + next: "下月", + today: "今天", + month: "月", + week: "周", + day: "日", + list: "日程" + }, + weekLabel: "周", + allDayText: "全天", + eventLimitText: function (n) { + return "另外 " + n + " 个"; + }, + noEventsMessage: "没有事件显示" + }; + + return zhCn; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/locales/zh-tw.js b/addons/web/static/lib/fullcalendar/core/locales/zh-tw.js new file mode 100644 index 00000000000..bc14dcd4bca --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/locales/zh-tw.js @@ -0,0 +1,26 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, (global.FullCalendarLocales = global.FullCalendarLocales || {}, global.FullCalendarLocales['zh-tw'] = factory())); +}(this, function () { 'use strict'; + + var zhTw = { + code: "zh-tw", + buttonText: { + prev: "上月", + next: "下月", + today: "今天", + month: "月", + week: "週", + day: "天", + list: "活動列表" + }, + weekLabel: "周", + allDayText: "整天", + eventLimitText: '顯示更多', + noEventsMessage: "没有任何活動" + }; + + return zhTw; + +})); diff --git a/addons/web/static/lib/fullcalendar/core/main.css b/addons/web/static/lib/fullcalendar/core/main.css new file mode 100644 index 00000000000..0bbe9a35b31 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/main.css @@ -0,0 +1,1052 @@ +@charset "UTF-8"; +.fc { + direction: ltr; + text-align: left; +} + +.fc-rtl { + text-align: right; +} + +body .fc { + /* extra precedence to overcome jqui */ + font-size: 1em; +} + +/* Colors +--------------------------------------------------------------------------------------------------*/ +.fc-highlight { + /* when user is selecting cells */ + background: #bce8f1; + opacity: 0.3; +} + +.fc-bgevent { + /* default look for background events */ + background: #8fdf82; + opacity: 0.3; +} + +.fc-nonbusiness { + /* default look for non-business-hours areas */ + /* will inherit .fc-bgevent's styles */ + background: #d7d7d7; +} + +/* Popover +--------------------------------------------------------------------------------------------------*/ +.fc-popover { + position: absolute; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); +} + +.fc-popover .fc-header { + /* TODO: be more consistent with fc-head/fc-body */ + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 2px 4px; +} + +.fc-rtl .fc-popover .fc-header { + flex-direction: row-reverse; +} + +.fc-popover .fc-header .fc-title { + margin: 0 2px; +} + +.fc-popover .fc-header .fc-close { + cursor: pointer; + opacity: 0.65; + font-size: 1.1em; +} + +/* Misc Reusable Components +--------------------------------------------------------------------------------------------------*/ +.fc-divider { + border-style: solid; + border-width: 1px; +} + +hr.fc-divider { + height: 0; + margin: 0; + padding: 0 0 2px; + /* height is unreliable across browsers, so use padding */ + border-width: 1px 0; +} + +.fc-bg, +.fc-bgevent-skeleton, +.fc-highlight-skeleton, +.fc-mirror-skeleton { + /* these element should always cling to top-left/right corners */ + position: absolute; + top: 0; + left: 0; + right: 0; +} + +.fc-bg { + bottom: 0; + /* strech bg to bottom edge */ +} + +.fc-bg table { + height: 100%; + /* strech bg to bottom edge */ +} + +/* Tables +--------------------------------------------------------------------------------------------------*/ +.fc table { + width: 100%; + box-sizing: border-box; + /* fix scrollbar issue in firefox */ + table-layout: fixed; + border-collapse: collapse; + border-spacing: 0; + font-size: 1em; + /* normalize cross-browser */ +} + +.fc th { + text-align: center; +} + +.fc th, +.fc td { + border-style: solid; + border-width: 1px; + padding: 0; + vertical-align: top; +} + +.fc td.fc-today { + border-style: double; + /* overcome neighboring borders */ +} + +/* Internal Nav Links +--------------------------------------------------------------------------------------------------*/ +a[data-goto] { + cursor: pointer; +} + +a[data-goto]:hover { + text-decoration: underline; +} + +/* Fake Table Rows +--------------------------------------------------------------------------------------------------*/ +.fc .fc-row { + /* extra precedence to overcome themes forcing a 1px border */ + /* no visible border by default. but make available if need be (scrollbar width compensation) */ + border-style: solid; + border-width: 0; +} + +.fc-row table { + /* don't put left/right border on anything within a fake row. + the outer tbody will worry about this */ + border-left: 0 hidden transparent; + border-right: 0 hidden transparent; + /* no bottom borders on rows */ + border-bottom: 0 hidden transparent; +} + +.fc-row:first-child table { + border-top: 0 hidden transparent; + /* no top border on first row */ +} + +/* Day Row (used within the header and the DayGrid) +--------------------------------------------------------------------------------------------------*/ +.fc-row { + position: relative; +} + +.fc-row .fc-bg { + z-index: 1; +} + +/* highlighting cells & background event skeleton */ +.fc-row .fc-bgevent-skeleton, +.fc-row .fc-highlight-skeleton { + bottom: 0; + /* stretch skeleton to bottom of row */ +} + +.fc-row .fc-bgevent-skeleton table, +.fc-row .fc-highlight-skeleton table { + height: 100%; + /* stretch skeleton to bottom of row */ +} + +.fc-row .fc-highlight-skeleton td, +.fc-row .fc-bgevent-skeleton td { + border-color: transparent; +} + +.fc-row .fc-bgevent-skeleton { + z-index: 2; +} + +.fc-row .fc-highlight-skeleton { + z-index: 3; +} + +/* +row content (which contains day/week numbers and events) as well as "mirror" (which contains +temporary rendered events). +*/ +.fc-row .fc-content-skeleton { + position: relative; + z-index: 4; + padding-bottom: 2px; + /* matches the space above the events */ +} + +.fc-row .fc-mirror-skeleton { + z-index: 5; +} + +.fc .fc-row .fc-content-skeleton table, +.fc .fc-row .fc-content-skeleton td, +.fc .fc-row .fc-mirror-skeleton td { + /* see-through to the background below */ + /* extra precedence to prevent theme-provided backgrounds */ + background: none; + /* in case <td>s are globally styled */ + border-color: transparent; +} + +.fc-row .fc-content-skeleton td, +.fc-row .fc-mirror-skeleton td { + /* don't put a border between events and/or the day number */ + border-bottom: 0; +} + +.fc-row .fc-content-skeleton tbody td, +.fc-row .fc-mirror-skeleton tbody td { + /* don't put a border between event cells */ + border-top: 0; +} + +/* Scrolling Container +--------------------------------------------------------------------------------------------------*/ +.fc-scroller { + -webkit-overflow-scrolling: touch; +} + +/* TODO: move to timegrid/daygrid */ +.fc-scroller > .fc-day-grid, +.fc-scroller > .fc-time-grid { + position: relative; + /* re-scope all positions */ + width: 100%; + /* hack to force re-sizing this inner element when scrollbars appear/disappear */ +} + +/* Global Event Styles +--------------------------------------------------------------------------------------------------*/ +.fc-event { + position: relative; + /* for resize handle and other inner positioning */ + display: block; + /* make the <a> tag block */ + font-size: 0.85em; + line-height: 1.4; + border-radius: 3px; + border: 1px solid #3788d8; +} + +.fc-event, +.fc-event-dot { + background-color: #3788d8; + /* default BACKGROUND color */ +} + +.fc-event, +.fc-event:hover { + color: #fff; + /* default TEXT color */ + text-decoration: none; + /* if <a> has an href */ +} + +.fc-event[href], +.fc-event.fc-draggable { + cursor: pointer; + /* give events with links and draggable events a hand mouse pointer */ +} + +.fc-not-allowed, +.fc-not-allowed .fc-event { + /* to override an event's custom cursor */ + cursor: not-allowed; +} + +.fc-event .fc-content { + position: relative; + z-index: 2; +} + +/* resizer (cursor AND touch devices) */ +.fc-event .fc-resizer { + position: absolute; + z-index: 4; +} + +/* resizer (touch devices) */ +.fc-event .fc-resizer { + display: none; +} + +.fc-event.fc-allow-mouse-resize .fc-resizer, +.fc-event.fc-selected .fc-resizer { + /* only show when hovering or selected (with touch) */ + display: block; +} + +/* hit area */ +.fc-event.fc-selected .fc-resizer:before { + /* 40x40 touch area */ + content: ""; + position: absolute; + z-index: 9999; + /* user of this util can scope within a lower z-index */ + top: 50%; + left: 50%; + width: 40px; + height: 40px; + margin-left: -20px; + margin-top: -20px; +} + +/* Event Selection (only for touch devices) +--------------------------------------------------------------------------------------------------*/ +.fc-event.fc-selected { + z-index: 9999 !important; + /* overcomes inline z-index */ + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); +} + +.fc-event.fc-selected:after { + content: ""; + position: absolute; + z-index: 1; + /* same z-index as fc-bg, behind text */ + /* overcome the borders */ + top: -1px; + right: -1px; + bottom: -1px; + left: -1px; + /* darkening effect */ + background: #000; + opacity: 0.25; +} + +/* Event Dragging +--------------------------------------------------------------------------------------------------*/ +.fc-event.fc-dragging.fc-selected { + box-shadow: 0 2px 7px rgba(0, 0, 0, 0.3); +} + +.fc-event.fc-dragging:not(.fc-selected) { + opacity: 0.75; +} + +/* Horizontal Events +--------------------------------------------------------------------------------------------------*/ +/* bigger touch area when selected */ +.fc-h-event.fc-selected:before { + content: ""; + position: absolute; + z-index: 3; + /* below resizers */ + top: -10px; + bottom: -10px; + left: 0; + right: 0; +} + +/* events that are continuing to/from another week. kill rounded corners and butt up against edge */ +.fc-ltr .fc-h-event.fc-not-start, +.fc-rtl .fc-h-event.fc-not-end { + margin-left: 0; + border-left-width: 0; + padding-left: 1px; + /* replace the border with padding */ + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.fc-ltr .fc-h-event.fc-not-end, +.fc-rtl .fc-h-event.fc-not-start { + margin-right: 0; + border-right-width: 0; + padding-right: 1px; + /* replace the border with padding */ + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +/* resizer (cursor AND touch devices) */ +/* left resizer */ +.fc-ltr .fc-h-event .fc-start-resizer, +.fc-rtl .fc-h-event .fc-end-resizer { + cursor: w-resize; + left: -1px; + /* overcome border */ +} + +/* right resizer */ +.fc-ltr .fc-h-event .fc-end-resizer, +.fc-rtl .fc-h-event .fc-start-resizer { + cursor: e-resize; + right: -1px; + /* overcome border */ +} + +/* resizer (mouse devices) */ +.fc-h-event.fc-allow-mouse-resize .fc-resizer { + width: 7px; + top: -1px; + /* overcome top border */ + bottom: -1px; + /* overcome bottom border */ +} + +/* resizer (touch devices) */ +.fc-h-event.fc-selected .fc-resizer { + /* 8x8 little dot */ + border-radius: 4px; + border-width: 1px; + width: 6px; + height: 6px; + border-style: solid; + border-color: inherit; + background: #fff; + /* vertically center */ + top: 50%; + margin-top: -4px; +} + +/* left resizer */ +.fc-ltr .fc-h-event.fc-selected .fc-start-resizer, +.fc-rtl .fc-h-event.fc-selected .fc-end-resizer { + margin-left: -4px; + /* centers the 8x8 dot on the left edge */ +} + +/* right resizer */ +.fc-ltr .fc-h-event.fc-selected .fc-end-resizer, +.fc-rtl .fc-h-event.fc-selected .fc-start-resizer { + margin-right: -4px; + /* centers the 8x8 dot on the right edge */ +} + +/* DayGrid events +---------------------------------------------------------------------------------------------------- +We use the full "fc-day-grid-event" class instead of using descendants because the event won't +be a descendant of the grid when it is being dragged. +*/ +.fc-day-grid-event { + margin: 1px 2px 0; + /* spacing between events and edges */ + padding: 0 1px; +} + +tr:first-child > td > .fc-day-grid-event { + margin-top: 2px; + /* a little bit more space before the first event */ +} + +.fc-mirror-skeleton tr:first-child > td > .fc-day-grid-event { + margin-top: 0; + /* except for mirror skeleton */ +} + +.fc-day-grid-event .fc-content { + /* force events to be one-line tall */ + white-space: nowrap; + overflow: hidden; +} + +.fc-day-grid-event .fc-time { + font-weight: bold; +} + +/* resizer (cursor devices) */ +/* left resizer */ +.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer, +.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer { + margin-left: -2px; + /* to the day cell's edge */ +} + +/* right resizer */ +.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer, +.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer { + margin-right: -2px; + /* to the day cell's edge */ +} + +/* Event Limiting +--------------------------------------------------------------------------------------------------*/ +/* "more" link that represents hidden events */ +a.fc-more { + margin: 1px 3px; + font-size: 0.85em; + cursor: pointer; + text-decoration: none; +} + +a.fc-more:hover { + text-decoration: underline; +} + +.fc-limited { + /* rows and cells that are hidden because of a "more" link */ + display: none; +} + +/* popover that appears when "more" link is clicked */ +.fc-day-grid .fc-row { + z-index: 1; + /* make the "more" popover one higher than this */ +} + +.fc-more-popover { + z-index: 2; + width: 220px; +} + +.fc-more-popover .fc-event-container { + padding: 10px; +} + +/* Now Indicator +--------------------------------------------------------------------------------------------------*/ +.fc-now-indicator { + position: absolute; + border: 0 solid red; +} + +/* Utilities +--------------------------------------------------------------------------------------------------*/ +.fc-unselectable { + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-touch-callout: none; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +/* +TODO: more distinction between this file and common.css +*/ +/* Colors +--------------------------------------------------------------------------------------------------*/ +.fc-unthemed th, +.fc-unthemed td, +.fc-unthemed thead, +.fc-unthemed tbody, +.fc-unthemed .fc-divider, +.fc-unthemed .fc-row, +.fc-unthemed .fc-content, +.fc-unthemed .fc-popover, +.fc-unthemed .fc-list-view, +.fc-unthemed .fc-list-heading td { + border-color: #ddd; +} + +.fc-unthemed .fc-popover { + background-color: #fff; +} + +.fc-unthemed .fc-divider, +.fc-unthemed .fc-popover .fc-header, +.fc-unthemed .fc-list-heading td { + background: #eee; +} + +.fc-unthemed td.fc-today { + background: #fcf8e3; +} + +.fc-unthemed .fc-disabled-day { + background: #d7d7d7; + opacity: 0.3; +} + +/* Icons +-------------------------------------------------------------------------------------------------- +from https://feathericons.com/ and built with IcoMoon +*/ +@font-face { + font-family: "fcicons"; + src: url("data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMg8SBfAAAAC8AAAAYGNtYXAXVtKNAAABHAAAAFRnYXNwAAAAEAAAAXAAAAAIZ2x5ZgYydxIAAAF4AAAFNGhlYWQUJ7cIAAAGrAAAADZoaGVhB20DzAAABuQAAAAkaG10eCIABhQAAAcIAAAALGxvY2ED4AU6AAAHNAAAABhtYXhwAA8AjAAAB0wAAAAgbmFtZXsr690AAAdsAAABhnBvc3QAAwAAAAAI9AAAACAAAwPAAZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADpBgPA/8AAQAPAAEAAAAABAAAAAAAAAAAAAAAgAAAAAAADAAAAAwAAABwAAQADAAAAHAADAAEAAAAcAAQAOAAAAAoACAACAAIAAQAg6Qb//f//AAAAAAAg6QD//f//AAH/4xcEAAMAAQAAAAAAAAAAAAAAAQAB//8ADwABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAWIAjQKeAskAEwAAJSc3NjQnJiIHAQYUFwEWMjc2NCcCnuLiDQ0MJAz/AA0NAQAMJAwNDcni4gwjDQwM/wANIwz/AA0NDCMNAAAAAQFiAI0CngLJABMAACUBNjQnASYiBwYUHwEHBhQXFjI3AZ4BAA0N/wAMJAwNDeLiDQ0MJAyNAQAMIw0BAAwMDSMM4uINIwwNDQAAAAIA4gC3Ax4CngATACcAACUnNzY0JyYiDwEGFB8BFjI3NjQnISc3NjQnJiIPAQYUHwEWMjc2NCcB87e3DQ0MIw3VDQ3VDSMMDQ0BK7e3DQ0MJAzVDQ3VDCQMDQ3zuLcMJAwNDdUNIwzWDAwNIwy4twwkDA0N1Q0jDNYMDA0jDAAAAgDiALcDHgKeABMAJwAAJTc2NC8BJiIHBhQfAQcGFBcWMjchNzY0LwEmIgcGFB8BBwYUFxYyNwJJ1Q0N1Q0jDA0Nt7cNDQwjDf7V1Q0N1QwkDA0Nt7cNDQwkDLfWDCMN1Q0NDCQMt7gMIw0MDNYMIw3VDQ0MJAy3uAwjDQwMAAADAFUAAAOrA1UAMwBoAHcAABMiBgcOAQcOAQcOARURFBYXHgEXHgEXHgEzITI2Nz4BNz4BNz4BNRE0JicuAScuAScuASMFITIWFx4BFx4BFx4BFREUBgcOAQcOAQcOASMhIiYnLgEnLgEnLgE1ETQ2Nz4BNz4BNz4BMxMhMjY1NCYjISIGFRQWM9UNGAwLFQkJDgUFBQUFBQ4JCRULDBgNAlYNGAwLFQkJDgUFBQUFBQ4JCRULDBgN/aoCVgQIBAQHAwMFAQIBAQIBBQMDBwQECAT9qgQIBAQHAwMFAQIBAQIBBQMDBwQECASAAVYRGRkR/qoRGRkRA1UFBAUOCQkVDAsZDf2rDRkLDBUJCA4FBQUFBQUOCQgVDAsZDQJVDRkLDBUJCQ4FBAVVAgECBQMCBwQECAX9qwQJAwQHAwMFAQICAgIBBQMDBwQDCQQCVQUIBAQHAgMFAgEC/oAZEhEZGRESGQAAAAADAFUAAAOrA1UAMwBoAIkAABMiBgcOAQcOAQcOARURFBYXHgEXHgEXHgEzITI2Nz4BNz4BNz4BNRE0JicuAScuAScuASMFITIWFx4BFx4BFx4BFREUBgcOAQcOAQcOASMhIiYnLgEnLgEnLgE1ETQ2Nz4BNz4BNz4BMxMzFRQWMzI2PQEzMjY1NCYrATU0JiMiBh0BIyIGFRQWM9UNGAwLFQkJDgUFBQUFBQ4JCRULDBgNAlYNGAwLFQkJDgUFBQUFBQ4JCRULDBgN/aoCVgQIBAQHAwMFAQIBAQIBBQMDBwQECAT9qgQIBAQHAwMFAQIBAQIBBQMDBwQECASAgBkSEhmAERkZEYAZEhIZgBEZGREDVQUEBQ4JCRUMCxkN/asNGQsMFQkIDgUFBQUFBQ4JCBUMCxkNAlUNGQsMFQkJDgUEBVUCAQIFAwIHBAQIBf2rBAkDBAcDAwUBAgICAgEFAwMHBAMJBAJVBQgEBAcCAwUCAQL+gIASGRkSgBkSERmAEhkZEoAZERIZAAABAOIAjQMeAskAIAAAExcHBhQXFjI/ARcWMjc2NC8BNzY0JyYiDwEnJiIHBhQX4uLiDQ0MJAzi4gwkDA0N4uINDQwkDOLiDCQMDQ0CjeLiDSMMDQ3h4Q0NDCMN4uIMIw0MDOLiDAwNIwwAAAABAAAAAQAAa5n0y18PPPUACwQAAAAAANivOVsAAAAA2K85WwAAAAADqwNVAAAACAACAAAAAAAAAAEAAAPA/8AAAAQAAAAAAAOrAAEAAAAAAAAAAAAAAAAAAAALBAAAAAAAAAAAAAAAAgAAAAQAAWIEAAFiBAAA4gQAAOIEAABVBAAAVQQAAOIAAAAAAAoAFAAeAEQAagCqAOoBngJkApoAAQAAAAsAigADAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAA4ArgABAAAAAAABAAcAAAABAAAAAAACAAcAYAABAAAAAAADAAcANgABAAAAAAAEAAcAdQABAAAAAAAFAAsAFQABAAAAAAAGAAcASwABAAAAAAAKABoAigADAAEECQABAA4ABwADAAEECQACAA4AZwADAAEECQADAA4APQADAAEECQAEAA4AfAADAAEECQAFABYAIAADAAEECQAGAA4AUgADAAEECQAKADQApGZjaWNvbnMAZgBjAGkAYwBvAG4Ac1ZlcnNpb24gMS4wAFYAZQByAHMAaQBvAG4AIAAxAC4AMGZjaWNvbnMAZgBjAGkAYwBvAG4Ac2ZjaWNvbnMAZgBjAGkAYwBvAG4Ac1JlZ3VsYXIAUgBlAGcAdQBsAGEAcmZjaWNvbnMAZgBjAGkAYwBvAG4Ac0ZvbnQgZ2VuZXJhdGVkIGJ5IEljb01vb24uAEYAbwBuAHQAIABnAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAEkAYwBvAE0AbwBvAG4ALgAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") format("truetype"); + font-weight: normal; + font-style: normal; +} +.fc-icon { + /* use !important to prevent issues with browser extensions that change fonts */ + font-family: "fcicons" !important; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.fc-icon-chevron-left:before { + content: ""; +} + +.fc-icon-chevron-right:before { + content: ""; +} + +.fc-icon-chevrons-left:before { + content: ""; +} + +.fc-icon-chevrons-right:before { + content: ""; +} + +.fc-icon-minus-square:before { + content: ""; +} + +.fc-icon-plus-square:before { + content: ""; +} + +.fc-icon-x:before { + content: ""; +} + +.fc-icon { + display: inline-block; + width: 1em; + height: 1em; + text-align: center; +} + +/* Buttons +-------------------------------------------------------------------------------------------------- +Lots taken from Flatly (MIT): https://bootswatch.com/4/flatly/bootstrap.css +*/ +/* reset */ +.fc-button { + border-radius: 0; + overflow: visible; + text-transform: none; + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +.fc-button:focus { + outline: 1px dotted; + outline: 5px auto -webkit-focus-ring-color; +} + +.fc-button { + -webkit-appearance: button; +} + +.fc-button:not(:disabled) { + cursor: pointer; +} + +.fc-button::-moz-focus-inner { + padding: 0; + border-style: none; +} + +/* theme */ +.fc-button { + display: inline-block; + font-weight: 400; + color: #212529; + text-align: center; + vertical-align: middle; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-color: transparent; + border: 1px solid transparent; + padding: 0.4em 0.65em; + font-size: 1em; + line-height: 1.5; + border-radius: 0.25em; +} + +.fc-button:hover { + color: #212529; + text-decoration: none; +} + +.fc-button:focus { + outline: 0; + -webkit-box-shadow: 0 0 0 0.2rem rgba(44, 62, 80, 0.25); + box-shadow: 0 0 0 0.2rem rgba(44, 62, 80, 0.25); +} + +.fc-button:disabled { + opacity: 0.65; +} + +/* "primary" coloring */ +.fc-button-primary { + color: #fff; + background-color: #2C3E50; + border-color: #2C3E50; +} + +.fc-button-primary:hover { + color: #fff; + background-color: #1e2b37; + border-color: #1a252f; +} + +.fc-button-primary:focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(76, 91, 106, 0.5); + box-shadow: 0 0 0 0.2rem rgba(76, 91, 106, 0.5); +} + +.fc-button-primary:disabled { + color: #fff; + background-color: #2C3E50; + border-color: #2C3E50; +} + +.fc-button-primary:not(:disabled):active, +.fc-button-primary:not(:disabled).fc-button-active { + color: #fff; + background-color: #1a252f; + border-color: #151e27; +} + +.fc-button-primary:not(:disabled):active:focus, +.fc-button-primary:not(:disabled).fc-button-active:focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(76, 91, 106, 0.5); + box-shadow: 0 0 0 0.2rem rgba(76, 91, 106, 0.5); +} + +/* icons within buttons */ +.fc-button .fc-icon { + vertical-align: middle; + font-size: 1.5em; +} + +/* Buttons Groups +--------------------------------------------------------------------------------------------------*/ +.fc-button-group { + position: relative; + display: -webkit-inline-box; + display: -ms-inline-flexbox; + display: inline-flex; + vertical-align: middle; +} + +.fc-button-group > .fc-button { + position: relative; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; +} + +.fc-button-group > .fc-button:hover { + z-index: 1; +} + +.fc-button-group > .fc-button:focus, +.fc-button-group > .fc-button:active, +.fc-button-group > .fc-button.fc-button-active { + z-index: 1; +} + +.fc-button-group > .fc-button:not(:first-child) { + margin-left: -1px; +} + +.fc-button-group > .fc-button:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.fc-button-group > .fc-button:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +/* Popover +--------------------------------------------------------------------------------------------------*/ +.fc-unthemed .fc-popover { + border-width: 1px; + border-style: solid; +} + +/* List View +--------------------------------------------------------------------------------------------------*/ +.fc-unthemed .fc-list-item:hover td { + background-color: #f5f5f5; +} + +/* Toolbar +--------------------------------------------------------------------------------------------------*/ +.fc-toolbar { + display: flex; + justify-content: space-between; + align-items: center; +} + +.fc-toolbar.fc-header-toolbar { + margin-bottom: 1.5em; +} + +.fc-toolbar.fc-footer-toolbar { + margin-top: 1.5em; +} + +/* inner content */ +.fc-toolbar > * > :not(:first-child) { + margin-left: 0.75em; +} + +.fc-toolbar h2 { + font-size: 1.75em; + margin: 0; +} + +/* View Structure +--------------------------------------------------------------------------------------------------*/ +.fc-view-container { + position: relative; +} + +/* undo twitter bootstrap's box-sizing rules. normalizes positioning techniques */ +/* don't do this for the toolbar because we'll want bootstrap to style those buttons as some pt */ +.fc-view-container *, +.fc-view-container *:before, +.fc-view-container *:after { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +.fc-view, +.fc-view > table { + /* so dragged elements can be above the view's main element */ + position: relative; + z-index: 1; +} + +@media print { + .fc { + max-width: 100% !important; + } + + /* Global Event Restyling + --------------------------------------------------------------------------------------------------*/ + .fc-event { + background: #fff !important; + color: #000 !important; + page-break-inside: avoid; + } + + .fc-event .fc-resizer { + display: none; + } + + /* Table & Day-Row Restyling + --------------------------------------------------------------------------------------------------*/ + .fc th, +.fc td, +.fc hr, +.fc thead, +.fc tbody, +.fc-row { + border-color: #ccc !important; + background: #fff !important; + } + + /* kill the overlaid, absolutely-positioned components */ + /* common... */ + .fc-bg, +.fc-bgevent-skeleton, +.fc-highlight-skeleton, +.fc-mirror-skeleton, +.fc-bgevent-container, +.fc-business-container, +.fc-highlight-container, +.fc-mirror-container { + display: none; + } + + /* don't force a min-height on rows (for DayGrid) */ + .fc tbody .fc-row { + height: auto !important; + /* undo height that JS set in distributeHeight */ + min-height: 0 !important; + /* undo the min-height from each view's specific stylesheet */ + } + + .fc tbody .fc-row .fc-content-skeleton { + position: static; + /* undo .fc-rigid */ + padding-bottom: 0 !important; + /* use a more border-friendly method for this... */ + } + + .fc tbody .fc-row .fc-content-skeleton tbody tr:last-child td { + /* only works in newer browsers */ + padding-bottom: 1em; + /* ...gives space within the skeleton. also ensures min height in a way */ + } + + .fc tbody .fc-row .fc-content-skeleton table { + /* provides a min-height for the row, but only effective for IE, which exaggerates this value, + making it look more like 3em. for other browers, it will already be this tall */ + height: 1em; + } + + /* Undo month-view event limiting. Display all events and hide the "more" links + --------------------------------------------------------------------------------------------------*/ + .fc-more-cell, +.fc-more { + display: none !important; + } + + .fc tr.fc-limited { + display: table-row !important; + } + + .fc td.fc-limited { + display: table-cell !important; + } + + .fc-popover { + display: none; + /* never display the "more.." popover in print mode */ + } + + /* TimeGrid Restyling + --------------------------------------------------------------------------------------------------*/ + /* undo the min-height 100% trick used to fill the container's height */ + .fc-time-grid { + min-height: 0 !important; + } + + /* don't display the side axis at all ("all-day" and time cells) */ + .fc-timeGrid-view .fc-axis { + display: none; + } + + /* don't display the horizontal lines */ + .fc-slats, +.fc-time-grid hr { + /* this hr is used when height is underused and needs to be filled */ + display: none !important; + /* important overrides inline declaration */ + } + + /* let the container that holds the events be naturally positioned and create real height */ + .fc-time-grid .fc-content-skeleton { + position: static; + } + + /* in case there are no events, we still want some height */ + .fc-time-grid .fc-content-skeleton table { + height: 4em; + } + + /* kill the horizontal spacing made by the event container. event margins will be done below */ + .fc-time-grid .fc-event-container { + margin: 0 !important; + } + + /* TimeGrid *Event* Restyling + --------------------------------------------------------------------------------------------------*/ + /* naturally position events, vertically stacking them */ + .fc-time-grid .fc-event { + position: static !important; + margin: 3px 2px !important; + } + + /* for events that continue to a future day, give the bottom border back */ + .fc-time-grid .fc-event.fc-not-end { + border-bottom-width: 1px !important; + } + + /* indicate the event continues via "..." text */ + .fc-time-grid .fc-event.fc-not-end:after { + content: "..."; + } + + /* for events that are continuations from previous days, give the top border back */ + .fc-time-grid .fc-event.fc-not-start { + border-top-width: 1px !important; + } + + /* indicate the event is a continuation via "..." text */ + .fc-time-grid .fc-event.fc-not-start:before { + content: "..."; + } + + /* time */ + /* undo a previous declaration and let the time text span to a second line */ + .fc-time-grid .fc-event .fc-time { + white-space: normal !important; + } + + /* hide the the time that is normally displayed... */ + .fc-time-grid .fc-event .fc-time span { + display: none; + } + + /* ...replace it with a more verbose version (includes AM/PM) stored in an html attribute */ + .fc-time-grid .fc-event .fc-time:after { + content: attr(data-full); + } + + /* Vertical Scroller & Containers + --------------------------------------------------------------------------------------------------*/ + /* kill the scrollbars and allow natural height */ + .fc-scroller, +.fc-day-grid-container, +.fc-time-grid-container { + /* */ + overflow: visible !important; + height: auto !important; + } + + /* kill the horizontal border/padding used to compensate for scrollbars */ + .fc-row { + border: 0 !important; + margin: 0 !important; + } + + /* Button Controls + --------------------------------------------------------------------------------------------------*/ + .fc-button-group, +.fc button { + display: none; + /* don't display any button-related controls */ + } +}
\ No newline at end of file diff --git a/addons/web/static/lib/fullcalendar/core/main.js b/addons/web/static/lib/fullcalendar/core/main.js new file mode 100644 index 00000000000..ed160ddf423 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/core/main.js @@ -0,0 +1,8740 @@ +/*! +FullCalendar Core Package v4.4.0 +Docs & License: https://fullcalendar.io/ +(c) 2019 Adam Shaw +*/ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (global = global || self, factory(global.FullCalendar = {})); +}(this, function (exports) { 'use strict'; + + // Creating + // ---------------------------------------------------------------------------------------------------------------- + var elementPropHash = { + className: true, + colSpan: true, + rowSpan: true + }; + var containerTagHash = { + '<tr': 'tbody', + '<td': 'tr' + }; + function createElement(tagName, attrs, content) { + var el = document.createElement(tagName); + if (attrs) { + for (var attrName in attrs) { + if (attrName === 'style') { + applyStyle(el, attrs[attrName]); + } + else if (elementPropHash[attrName]) { + el[attrName] = attrs[attrName]; + } + else { + el.setAttribute(attrName, attrs[attrName]); + } + } + } + if (typeof content === 'string') { + el.innerHTML = content; // shortcut. no need to process HTML in any way + } + else if (content != null) { + appendToElement(el, content); + } + return el; + } + function htmlToElement(html) { + html = html.trim(); + var container = document.createElement(computeContainerTag(html)); + container.innerHTML = html; + return container.firstChild; + } + function htmlToElements(html) { + return Array.prototype.slice.call(htmlToNodeList(html)); + } + function htmlToNodeList(html) { + html = html.trim(); + var container = document.createElement(computeContainerTag(html)); + container.innerHTML = html; + return container.childNodes; + } + // assumes html already trimmed and tag names are lowercase + function computeContainerTag(html) { + return containerTagHash[html.substr(0, 3) // faster than using regex + ] || 'div'; + } + function appendToElement(el, content) { + var childNodes = normalizeContent(content); + for (var i = 0; i < childNodes.length; i++) { + el.appendChild(childNodes[i]); + } + } + function prependToElement(parent, content) { + var newEls = normalizeContent(content); + var afterEl = parent.firstChild || null; // if no firstChild, will append to end, but that's okay, b/c there were no children + for (var i = 0; i < newEls.length; i++) { + parent.insertBefore(newEls[i], afterEl); + } + } + function insertAfterElement(refEl, content) { + var newEls = normalizeContent(content); + var afterEl = refEl.nextSibling || null; + for (var i = 0; i < newEls.length; i++) { + refEl.parentNode.insertBefore(newEls[i], afterEl); + } + } + function normalizeContent(content) { + var els; + if (typeof content === 'string') { + els = htmlToElements(content); + } + else if (content instanceof Node) { + els = [content]; + } + else { // Node[] or NodeList + els = Array.prototype.slice.call(content); + } + return els; + } + function removeElement(el) { + if (el.parentNode) { + el.parentNode.removeChild(el); + } + } + // Querying + // ---------------------------------------------------------------------------------------------------------------- + // from https://developer.mozilla.org/en-US/docs/Web/API/Element/closest + var matchesMethod = Element.prototype.matches || + Element.prototype.matchesSelector || + Element.prototype.msMatchesSelector; + var closestMethod = Element.prototype.closest || function (selector) { + // polyfill + var el = this; + if (!document.documentElement.contains(el)) { + return null; + } + do { + if (elementMatches(el, selector)) { + return el; + } + el = el.parentElement || el.parentNode; + } while (el !== null && el.nodeType === 1); + return null; + }; + function elementClosest(el, selector) { + return closestMethod.call(el, selector); + } + function elementMatches(el, selector) { + return matchesMethod.call(el, selector); + } + // accepts multiple subject els + // returns a real array. good for methods like forEach + function findElements(container, selector) { + var containers = container instanceof HTMLElement ? [container] : container; + var allMatches = []; + for (var i = 0; i < containers.length; i++) { + var matches = containers[i].querySelectorAll(selector); + for (var j = 0; j < matches.length; j++) { + allMatches.push(matches[j]); + } + } + return allMatches; + } + // accepts multiple subject els + // only queries direct child elements + function findChildren(parent, selector) { + var parents = parent instanceof HTMLElement ? [parent] : parent; + var allMatches = []; + for (var i = 0; i < parents.length; i++) { + var childNodes = parents[i].children; // only ever elements + for (var j = 0; j < childNodes.length; j++) { + var childNode = childNodes[j]; + if (!selector || elementMatches(childNode, selector)) { + allMatches.push(childNode); + } + } + } + return allMatches; + } + // Attributes + // ---------------------------------------------------------------------------------------------------------------- + function forceClassName(el, className, bool) { + if (bool) { + el.classList.add(className); + } + else { + el.classList.remove(className); + } + } + // Style + // ---------------------------------------------------------------------------------------------------------------- + var PIXEL_PROP_RE = /(top|left|right|bottom|width|height)$/i; + function applyStyle(el, props) { + for (var propName in props) { + applyStyleProp(el, propName, props[propName]); + } + } + function applyStyleProp(el, name, val) { + if (val == null) { + el.style[name] = ''; + } + else if (typeof val === 'number' && PIXEL_PROP_RE.test(name)) { + el.style[name] = val + 'px'; + } + else { + el.style[name] = val; + } + } + + function pointInsideRect(point, rect) { + return point.left >= rect.left && + point.left < rect.right && + point.top >= rect.top && + point.top < rect.bottom; + } + // 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; + } + function translateRect(rect, deltaX, deltaY) { + return { + left: rect.left + deltaX, + right: rect.right + deltaX, + top: rect.top + deltaY, + bottom: rect.bottom + deltaY + }; + } + // 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 + }; + } + + // Logic for determining if, when the element is right-to-left, the scrollbar appears on the left side + var isRtlScrollbarOnLeft = null; + function getIsRtlScrollbarOnLeft() { + if (isRtlScrollbarOnLeft === null) { + isRtlScrollbarOnLeft = computeIsRtlScrollbarOnLeft(); + } + return isRtlScrollbarOnLeft; + } + function computeIsRtlScrollbarOnLeft() { + var outerEl = createElement('div', { + style: { + position: 'absolute', + top: -1000, + left: 0, + border: 0, + padding: 0, + overflow: 'scroll', + direction: 'rtl' + } + }, '<div></div>'); + document.body.appendChild(outerEl); + var innerEl = outerEl.firstChild; + var res = innerEl.getBoundingClientRect().left > outerEl.getBoundingClientRect().left; + removeElement(outerEl); + return res; + } + // The scrollbar width computations in computeEdges are sometimes flawed when it comes to + // retina displays, rounding, and IE11. Massage them into a usable value. + function sanitizeScrollbarWidth(width) { + width = Math.max(0, width); // no negatives + width = Math.round(width); + return width; + } + + function computeEdges(el, getPadding) { + if (getPadding === void 0) { getPadding = false; } + var computedStyle = window.getComputedStyle(el); + var borderLeft = parseInt(computedStyle.borderLeftWidth, 10) || 0; + var borderRight = parseInt(computedStyle.borderRightWidth, 10) || 0; + var borderTop = parseInt(computedStyle.borderTopWidth, 10) || 0; + var borderBottom = parseInt(computedStyle.borderBottomWidth, 10) || 0; + // must use offset(Width|Height) because compatible with client(Width|Height) + var scrollbarLeftRight = sanitizeScrollbarWidth(el.offsetWidth - el.clientWidth - borderLeft - borderRight); + var scrollbarBottom = sanitizeScrollbarWidth(el.offsetHeight - el.clientHeight - borderTop - borderBottom); + var res = { + borderLeft: borderLeft, + borderRight: borderRight, + borderTop: borderTop, + borderBottom: borderBottom, + scrollbarBottom: scrollbarBottom, + scrollbarLeft: 0, + scrollbarRight: 0 + }; + if (getIsRtlScrollbarOnLeft() && computedStyle.direction === 'rtl') { // is the scrollbar on the left side? + res.scrollbarLeft = scrollbarLeftRight; + } + else { + res.scrollbarRight = scrollbarLeftRight; + } + if (getPadding) { + res.paddingLeft = parseInt(computedStyle.paddingLeft, 10) || 0; + res.paddingRight = parseInt(computedStyle.paddingRight, 10) || 0; + res.paddingTop = parseInt(computedStyle.paddingTop, 10) || 0; + res.paddingBottom = parseInt(computedStyle.paddingBottom, 10) || 0; + } + return res; + } + function computeInnerRect(el, goWithinPadding) { + if (goWithinPadding === void 0) { goWithinPadding = false; } + var outerRect = computeRect(el); + var edges = computeEdges(el, goWithinPadding); + var res = { + left: outerRect.left + edges.borderLeft + edges.scrollbarLeft, + right: outerRect.right - edges.borderRight - edges.scrollbarRight, + top: outerRect.top + edges.borderTop, + bottom: outerRect.bottom - edges.borderBottom - edges.scrollbarBottom + }; + if (goWithinPadding) { + res.left += edges.paddingLeft; + res.right -= edges.paddingRight; + res.top += edges.paddingTop; + res.bottom -= edges.paddingBottom; + } + return res; + } + function computeRect(el) { + var rect = el.getBoundingClientRect(); + return { + left: rect.left + window.pageXOffset, + top: rect.top + window.pageYOffset, + right: rect.right + window.pageXOffset, + bottom: rect.bottom + window.pageYOffset + }; + } + function computeViewportRect() { + return { + left: window.pageXOffset, + right: window.pageXOffset + document.documentElement.clientWidth, + top: window.pageYOffset, + bottom: window.pageYOffset + document.documentElement.clientHeight + }; + } + function computeHeightAndMargins(el) { + return el.getBoundingClientRect().height + computeVMargins(el); + } + function computeVMargins(el) { + var computed = window.getComputedStyle(el); + return parseInt(computed.marginTop, 10) + + parseInt(computed.marginBottom, 10); + } + // does not return window + function getClippingParents(el) { + var parents = []; + while (el instanceof HTMLElement) { // will stop when gets to document or null + var computedStyle = window.getComputedStyle(el); + if (computedStyle.position === 'fixed') { + break; + } + if ((/(auto|scroll)/).test(computedStyle.overflow + computedStyle.overflowY + computedStyle.overflowX)) { + parents.push(el); + } + el = el.parentNode; + } + return parents; + } + function computeClippingRect(el) { + return getClippingParents(el) + .map(function (el) { + return computeInnerRect(el); + }) + .concat(computeViewportRect()) + .reduce(function (rect0, rect1) { + return intersectRects(rect0, rect1) || rect1; // should always intersect + }); + } + + // Stops a mouse/touch event from doing it's native browser action + function preventDefault(ev) { + ev.preventDefault(); + } + // Event Delegation + // ---------------------------------------------------------------------------------------------------------------- + function listenBySelector(container, eventType, selector, handler) { + function realHandler(ev) { + var matchedChild = elementClosest(ev.target, selector); + if (matchedChild) { + handler.call(matchedChild, ev, matchedChild); + } + } + container.addEventListener(eventType, realHandler); + return function () { + container.removeEventListener(eventType, realHandler); + }; + } + function listenToHoverBySelector(container, selector, onMouseEnter, onMouseLeave) { + var currentMatchedChild; + return listenBySelector(container, 'mouseover', selector, function (ev, matchedChild) { + if (matchedChild !== currentMatchedChild) { + currentMatchedChild = matchedChild; + onMouseEnter(ev, matchedChild); + var realOnMouseLeave_1 = function (ev) { + currentMatchedChild = null; + onMouseLeave(ev, matchedChild); + matchedChild.removeEventListener('mouseleave', realOnMouseLeave_1); + }; + // listen to the next mouseleave, and then unattach + matchedChild.addEventListener('mouseleave', realOnMouseLeave_1); + } + }); + } + // Animation + // ---------------------------------------------------------------------------------------------------------------- + var transitionEventNames = [ + 'webkitTransitionEnd', + 'otransitionend', + 'oTransitionEnd', + 'msTransitionEnd', + 'transitionend' + ]; + // triggered only when the next single subsequent transition finishes + function whenTransitionDone(el, callback) { + var realCallback = function (ev) { + callback(ev); + transitionEventNames.forEach(function (eventName) { + el.removeEventListener(eventName, realCallback); + }); + }; + transitionEventNames.forEach(function (eventName) { + el.addEventListener(eventName, realCallback); // cross-browser way to determine when the transition finishes + }); + } + + var DAY_IDS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; + // Adding + function addWeeks(m, n) { + var a = dateToUtcArray(m); + a[2] += n * 7; + return arrayToUtcDate(a); + } + function addDays(m, n) { + var a = dateToUtcArray(m); + a[2] += n; + return arrayToUtcDate(a); + } + function addMs(m, n) { + var a = dateToUtcArray(m); + a[6] += n; + return arrayToUtcDate(a); + } + // Diffing (all return floats) + function diffWeeks(m0, m1) { + return diffDays(m0, m1) / 7; + } + function diffDays(m0, m1) { + return (m1.valueOf() - m0.valueOf()) / (1000 * 60 * 60 * 24); + } + function diffHours(m0, m1) { + return (m1.valueOf() - m0.valueOf()) / (1000 * 60 * 60); + } + function diffMinutes(m0, m1) { + return (m1.valueOf() - m0.valueOf()) / (1000 * 60); + } + function diffSeconds(m0, m1) { + return (m1.valueOf() - m0.valueOf()) / 1000; + } + function diffDayAndTime(m0, m1) { + var m0day = startOfDay(m0); + var m1day = startOfDay(m1); + return { + years: 0, + months: 0, + days: Math.round(diffDays(m0day, m1day)), + milliseconds: (m1.valueOf() - m1day.valueOf()) - (m0.valueOf() - m0day.valueOf()) + }; + } + // Diffing Whole Units + function diffWholeWeeks(m0, m1) { + var d = diffWholeDays(m0, m1); + if (d !== null && d % 7 === 0) { + return d / 7; + } + return null; + } + function diffWholeDays(m0, m1) { + if (timeAsMs(m0) === timeAsMs(m1)) { + return Math.round(diffDays(m0, m1)); + } + return null; + } + // Start-Of + function startOfDay(m) { + return arrayToUtcDate([ + m.getUTCFullYear(), + m.getUTCMonth(), + m.getUTCDate() + ]); + } + function startOfHour(m) { + return arrayToUtcDate([ + m.getUTCFullYear(), + m.getUTCMonth(), + m.getUTCDate(), + m.getUTCHours() + ]); + } + function startOfMinute(m) { + return arrayToUtcDate([ + m.getUTCFullYear(), + m.getUTCMonth(), + m.getUTCDate(), + m.getUTCHours(), + m.getUTCMinutes() + ]); + } + function startOfSecond(m) { + return arrayToUtcDate([ + m.getUTCFullYear(), + m.getUTCMonth(), + m.getUTCDate(), + m.getUTCHours(), + m.getUTCMinutes(), + m.getUTCSeconds() + ]); + } + // Week Computation + function weekOfYear(marker, dow, doy) { + var y = marker.getUTCFullYear(); + var w = weekOfGivenYear(marker, y, dow, doy); + if (w < 1) { + return weekOfGivenYear(marker, y - 1, dow, doy); + } + var nextW = weekOfGivenYear(marker, y + 1, dow, doy); + if (nextW >= 1) { + return Math.min(w, nextW); + } + return w; + } + function weekOfGivenYear(marker, year, dow, doy) { + var firstWeekStart = arrayToUtcDate([year, 0, 1 + firstWeekOffset(year, dow, doy)]); + var dayStart = startOfDay(marker); + var days = Math.round(diffDays(firstWeekStart, dayStart)); + return Math.floor(days / 7) + 1; // zero-indexed + } + // start-of-first-week - start-of-year + function firstWeekOffset(year, dow, doy) { + // first-week day -- which january is always in the first week (4 for iso, 1 for other) + var fwd = 7 + dow - doy; + // first-week day local weekday -- which local weekday is fwd + var fwdlw = (7 + arrayToUtcDate([year, 0, fwd]).getUTCDay() - dow) % 7; + return -fwdlw + fwd - 1; + } + // Array Conversion + function dateToLocalArray(date) { + return [ + date.getFullYear(), + date.getMonth(), + date.getDate(), + date.getHours(), + date.getMinutes(), + date.getSeconds(), + date.getMilliseconds() + ]; + } + function arrayToLocalDate(a) { + return new Date(a[0], a[1] || 0, a[2] == null ? 1 : a[2], // day of month + a[3] || 0, a[4] || 0, a[5] || 0); + } + function dateToUtcArray(date) { + return [ + date.getUTCFullYear(), + date.getUTCMonth(), + date.getUTCDate(), + date.getUTCHours(), + date.getUTCMinutes(), + date.getUTCSeconds(), + date.getUTCMilliseconds() + ]; + } + function arrayToUtcDate(a) { + // according to web standards (and Safari), a month index is required. + // massage if only given a year. + if (a.length === 1) { + a = a.concat([0]); + } + return new Date(Date.UTC.apply(Date, a)); + } + // Other Utils + function isValidDate(m) { + return !isNaN(m.valueOf()); + } + function timeAsMs(m) { + return m.getUTCHours() * 1000 * 60 * 60 + + m.getUTCMinutes() * 1000 * 60 + + m.getUTCSeconds() * 1000 + + m.getUTCMilliseconds(); + } + + var INTERNAL_UNITS = ['years', 'months', 'days', 'milliseconds']; + var PARSE_RE = /^(-?)(?:(\d+)\.)?(\d+):(\d\d)(?::(\d\d)(?:\.(\d\d\d))?)?/; + // Parsing and Creation + function createDuration(input, unit) { + var _a; + if (typeof input === 'string') { + return parseString(input); + } + else if (typeof input === 'object' && input) { // non-null object + return normalizeObject(input); + } + else if (typeof input === 'number') { + return normalizeObject((_a = {}, _a[unit || 'milliseconds'] = input, _a)); + } + else { + return null; + } + } + function parseString(s) { + var m = PARSE_RE.exec(s); + if (m) { + var sign = m[1] ? -1 : 1; + return { + years: 0, + months: 0, + days: sign * (m[2] ? parseInt(m[2], 10) : 0), + milliseconds: sign * ((m[3] ? parseInt(m[3], 10) : 0) * 60 * 60 * 1000 + // hours + (m[4] ? parseInt(m[4], 10) : 0) * 60 * 1000 + // minutes + (m[5] ? parseInt(m[5], 10) : 0) * 1000 + // seconds + (m[6] ? parseInt(m[6], 10) : 0) // ms + ) + }; + } + return null; + } + function normalizeObject(obj) { + return { + years: obj.years || obj.year || 0, + months: obj.months || obj.month || 0, + days: (obj.days || obj.day || 0) + + getWeeksFromInput(obj) * 7, + milliseconds: (obj.hours || obj.hour || 0) * 60 * 60 * 1000 + // hours + (obj.minutes || obj.minute || 0) * 60 * 1000 + // minutes + (obj.seconds || obj.second || 0) * 1000 + // seconds + (obj.milliseconds || obj.millisecond || obj.ms || 0) // ms + }; + } + function getWeeksFromInput(obj) { + return obj.weeks || obj.week || 0; + } + // Equality + function durationsEqual(d0, d1) { + return d0.years === d1.years && + d0.months === d1.months && + d0.days === d1.days && + d0.milliseconds === d1.milliseconds; + } + function isSingleDay(dur) { + return dur.years === 0 && dur.months === 0 && dur.days === 1 && dur.milliseconds === 0; + } + // Simple Math + function addDurations(d0, d1) { + return { + years: d0.years + d1.years, + months: d0.months + d1.months, + days: d0.days + d1.days, + milliseconds: d0.milliseconds + d1.milliseconds + }; + } + function subtractDurations(d1, d0) { + return { + years: d1.years - d0.years, + months: d1.months - d0.months, + days: d1.days - d0.days, + milliseconds: d1.milliseconds - d0.milliseconds + }; + } + function multiplyDuration(d, n) { + return { + years: d.years * n, + months: d.months * n, + days: d.days * n, + milliseconds: d.milliseconds * n + }; + } + // Conversions + // "Rough" because they are based on average-case Gregorian months/years + function asRoughYears(dur) { + return asRoughDays(dur) / 365; + } + function asRoughMonths(dur) { + return asRoughDays(dur) / 30; + } + function asRoughDays(dur) { + return asRoughMs(dur) / 864e5; + } + function asRoughMinutes(dur) { + return asRoughMs(dur) / (1000 * 60); + } + function asRoughSeconds(dur) { + return asRoughMs(dur) / 1000; + } + function asRoughMs(dur) { + return dur.years * (365 * 864e5) + + dur.months * (30 * 864e5) + + dur.days * 864e5 + + dur.milliseconds; + } + // Advanced Math + function wholeDivideDurations(numerator, denominator) { + var res = null; + for (var i = 0; i < INTERNAL_UNITS.length; i++) { + var unit = INTERNAL_UNITS[i]; + if (denominator[unit]) { + var localRes = numerator[unit] / denominator[unit]; + if (!isInt(localRes) || (res !== null && res !== localRes)) { + return null; + } + res = localRes; + } + else if (numerator[unit]) { + // needs to divide by something but can't! + return null; + } + } + return res; + } + function greatestDurationDenominator(dur, dontReturnWeeks) { + var ms = dur.milliseconds; + if (ms) { + if (ms % 1000 !== 0) { + return { unit: 'millisecond', value: ms }; + } + if (ms % (1000 * 60) !== 0) { + return { unit: 'second', value: ms / 1000 }; + } + if (ms % (1000 * 60 * 60) !== 0) { + return { unit: 'minute', value: ms / (1000 * 60) }; + } + if (ms) { + return { unit: 'hour', value: ms / (1000 * 60 * 60) }; + } + } + if (dur.days) { + if (!dontReturnWeeks && dur.days % 7 === 0) { + return { unit: 'week', value: dur.days / 7 }; + } + return { unit: 'day', value: dur.days }; + } + if (dur.months) { + return { unit: 'month', value: dur.months }; + } + if (dur.years) { + return { unit: 'year', value: dur.years }; + } + return { unit: 'millisecond', value: 0 }; + } + + /* 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(rowEl, scrollbarWidths) { + if (scrollbarWidths.left) { + applyStyle(rowEl, { + borderLeftWidth: 1, + marginLeft: scrollbarWidths.left - 1 + }); + } + if (scrollbarWidths.right) { + applyStyle(rowEl, { + borderRightWidth: 1, + marginRight: scrollbarWidths.right - 1 + }); + } + } + // Undoes compensateScroll and restores all borders/margins + function uncompensateScroll(rowEl) { + applyStyle(rowEl, { + marginLeft: '', + marginRight: '', + borderLeftWidth: '', + borderRightWidth: '' + }); + } + // Make the mouse cursor express that an event is not allowed in the current area + function disableCursor() { + document.body.classList.add('fc-not-allowed'); + } + // Returns the mouse cursor to its original look + function enableCursor() { + document.body.classList.remove('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.forEach(function (el, i) { + var minOffset = i === els.length - 1 ? minOffset2 : minOffset1; + var naturalHeight = el.getBoundingClientRect().height; + var naturalOffset = naturalHeight + computeVMargins(el); + if (naturalOffset < minOffset) { + flexEls.push(el); + flexOffsets.push(naturalOffset); + flexHeights.push(naturalHeight); + } + 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.forEach(function (el, i) { + 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.style.height = newHeight + 'px'; + } + }); + } + // Undoes distrubuteHeight, restoring all els to their natural height + function undistributeHeight(els) { + els.forEach(function (el) { + el.style.height = ''; + }); + } + // Given `els`, a set of <td> cells, find the cell with the largest natural width and set the widths of all the + // cells to be that width. + // PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline + function matchCellWidths(els) { + var maxInnerWidth = 0; + els.forEach(function (el) { + var innerEl = el.firstChild; // hopefully an element + if (innerEl instanceof HTMLElement) { + var innerWidth_1 = innerEl.getBoundingClientRect().width; + if (innerWidth_1 > maxInnerWidth) { + maxInnerWidth = innerWidth_1; + } + } + }); + maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance + els.forEach(function (el) { + el.style.width = maxInnerWidth + 'px'; + }); + return maxInnerWidth; + } + // Given one element that resides inside another, + // Subtracts the height of the inner element from the outer element. + function subtractInnerElHeight(outerEl, innerEl) { + // effin' IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked + var reflowStyleProps = { + position: 'relative', + left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll + }; + applyStyle(outerEl, reflowStyleProps); + applyStyle(innerEl, reflowStyleProps); + var diff = // grab the dimensions + outerEl.getBoundingClientRect().height - + innerEl.getBoundingClientRect().height; + // undo hack + var resetStyleProps = { position: '', left: '' }; + applyStyle(outerEl, resetStyleProps); + applyStyle(innerEl, resetStyleProps); + return diff; + } + /* Selection + ----------------------------------------------------------------------------------------------------------------------*/ + function preventSelection(el) { + el.classList.add('fc-unselectable'); + el.addEventListener('selectstart', preventDefault); + } + function allowSelection(el) { + el.classList.remove('fc-unselectable'); + el.removeEventListener('selectstart', preventDefault); + } + /* Context Menu + ----------------------------------------------------------------------------------------------------------------------*/ + function preventContextMenu(el) { + el.addEventListener('contextmenu', preventDefault); + } + function allowContextMenu(el) { + el.removeEventListener('contextmenu', preventDefault); + } + /* Object Ordering by Field + ----------------------------------------------------------------------------------------------------------------------*/ + function parseFieldSpecs(input) { + var specs = []; + var tokens = []; + var i; + var token; + if (typeof input === 'string') { + tokens = input.split(/\s*,\s*/); + } + else if (typeof input === 'function') { + tokens = [input]; + } + else if (Array.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(obj0, obj1, fieldSpecs) { + var i; + var cmp; + for (i = 0; i < fieldSpecs.length; i++) { + cmp = compareByFieldSpec(obj0, obj1, fieldSpecs[i]); + if (cmp) { + return cmp; + } + } + return 0; + } + function compareByFieldSpec(obj0, obj1, fieldSpec) { + if (fieldSpec.func) { + return fieldSpec.func(obj0, obj1); + } + return flexibleCompare(obj0[fieldSpec.field], obj1[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 (typeof a === 'string' || typeof b === 'string') { + return String(a).localeCompare(String(b)); + } + return a - b; + } + /* String Utilities + ----------------------------------------------------------------------------------------------------------------------*/ + function capitaliseFirstLetter(str) { + return str.charAt(0).toUpperCase() + str.slice(1); + } + function padStart(val, len) { + var s = String(val); + return '000'.substr(0, len - s.length) + s; + } + /* Number Utilities + ----------------------------------------------------------------------------------------------------------------------*/ + function compareNumbers(a, b) { + return a - b; + } + function isInt(n) { + return n % 1 === 0; + } + /* Weird Utilities + ----------------------------------------------------------------------------------------------------------------------*/ + function applyAll(functions, thisObj, args) { + if (typeof functions === 'function') { // supplied a single function + functions = [functions]; + } + if (functions) { + var i = void 0; + var ret = void 0; + for (i = 0; i < functions.length; i++) { + ret = functions[i].apply(thisObj, args) || ret; + } + return ret; + } + } + function firstDefined() { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + for (var i = 0; i < args.length; i++) { + if (args[i] !== undefined) { + return args[i]; + } + } + } + // 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) { + var timeout; + var args; + var context; + var timestamp; + var result; + var later = function () { + var last = new Date().valueOf() - timestamp; + if (last < wait) { + timeout = setTimeout(later, wait - last); + } + else { + timeout = null; + result = func.apply(context, args); + context = args = null; + } + }; + return function () { + context = this; + args = arguments; + timestamp = new Date().valueOf(); + if (!timeout) { + timeout = setTimeout(later, wait); + } + return result; + }; + } + // Number and Boolean are only types that defaults or not computed for + // TODO: write more comments + function refineProps(rawProps, processors, defaults, leftoverProps) { + if (defaults === void 0) { defaults = {}; } + var refined = {}; + for (var key in processors) { + var processor = processors[key]; + if (rawProps[key] !== undefined) { + // found + if (processor === Function) { + refined[key] = typeof rawProps[key] === 'function' ? rawProps[key] : null; + } + else if (processor) { // a refining function? + refined[key] = processor(rawProps[key]); + } + else { + refined[key] = rawProps[key]; + } + } + else if (defaults[key] !== undefined) { + // there's an explicit default + refined[key] = defaults[key]; + } + else { + // must compute a default + if (processor === String) { + refined[key] = ''; // empty string is default for String + } + else if (!processor || processor === Number || processor === Boolean || processor === Function) { + refined[key] = null; // assign null for other non-custom processor funcs + } + else { + refined[key] = processor(null); // run the custom processor func + } + } + } + if (leftoverProps) { + for (var key in rawProps) { + if (processors[key] === undefined) { + leftoverProps[key] = rawProps[key]; + } + } + } + return refined; + } + /* Date stuff that doesn't belong in datelib core + ----------------------------------------------------------------------------------------------------------------------*/ + // given a timed range, computes an all-day range that has the same exact duration, + // but whose start time is aligned with the start of the day. + function computeAlignedDayRange(timedRange) { + var dayCnt = Math.floor(diffDays(timedRange.start, timedRange.end)) || 1; + var start = startOfDay(timedRange.start); + var end = addDays(start, dayCnt); + return { start: start, end: end }; + } + // given a timed range, computes an all-day range based on how for the end date bleeds into the next day + // TODO: give nextDayThreshold a default arg + function computeVisibleDayRange(timedRange, nextDayThreshold) { + if (nextDayThreshold === void 0) { nextDayThreshold = createDuration(0); } + var startDay = null; + var endDay = null; + if (timedRange.end) { + endDay = startOfDay(timedRange.end); + var endTimeMS = timedRange.end.valueOf() - endDay.valueOf(); // # 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 >= asRoughMs(nextDayThreshold)) { + endDay = addDays(endDay, 1); + } + } + if (timedRange.start) { + startDay = startOfDay(timedRange.start); // the beginning of the day the range starts + // If end is within `startDay` but not past nextDayThreshold, assign the default duration of one day. + if (endDay && endDay <= startDay) { + endDay = addDays(startDay, 1); + } + } + return { start: startDay, end: endDay }; + } + // spans from one day into another? + function isMultiDayRange(range) { + var visibleRange = computeVisibleDayRange(range); + return diffDays(visibleRange.start, visibleRange.end) > 1; + } + function diffDates(date0, date1, dateEnv, largeUnit) { + if (largeUnit === 'year') { + return createDuration(dateEnv.diffWholeYears(date0, date1), 'year'); + } + else if (largeUnit === 'month') { + return createDuration(dateEnv.diffWholeMonths(date0, date1), 'month'); + } + else { + return diffDayAndTime(date0, date1); // returns a duration + } + } + + /*! *****************************************************************************
+ Copyright (c) Microsoft Corporation. All rights reserved.
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+ this file except in compliance with the License. You may obtain a copy of the
+ License at http://www.apache.org/licenses/LICENSE-2.0
+
+ THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
+ WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+ MERCHANTABLITY OR NON-INFRINGEMENT.
+
+ See the Apache Version 2.0 License for specific language governing permissions
+ and limitations under the License.
+ ***************************************************************************** */
+ /* global Reflect, Promise */
+
+ var extendStatics = function(d, b) {
+ extendStatics = Object.setPrototypeOf ||
+ ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+ function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+ return extendStatics(d, b);
+ };
+
+ function __extends(d, b) {
+ extendStatics(d, b);
+ function __() { this.constructor = d; }
+ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+ }
+
+ var __assign = function() {
+ __assign = Object.assign || function __assign(t) {
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
+ s = arguments[i];
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
+ }
+ return t;
+ };
+ return __assign.apply(this, arguments);
+ }; + + function parseRecurring(eventInput, allDayDefault, dateEnv, recurringTypes, leftovers) { + for (var i = 0; i < recurringTypes.length; i++) { + var localLeftovers = {}; + var parsed = recurringTypes[i].parse(eventInput, localLeftovers, dateEnv); + if (parsed) { + var allDay = localLeftovers.allDay; + delete localLeftovers.allDay; // remove from leftovers + if (allDay == null) { + allDay = allDayDefault; + if (allDay == null) { + allDay = parsed.allDayGuess; + if (allDay == null) { + allDay = false; + } + } + } + __assign(leftovers, localLeftovers); + return { + allDay: allDay, + duration: parsed.duration, + typeData: parsed.typeData, + typeId: i + }; + } + } + return null; + } + /* + Event MUST have a recurringDef + */ + function expandRecurringRanges(eventDef, duration, framingRange, dateEnv, recurringTypes) { + var typeDef = recurringTypes[eventDef.recurringDef.typeId]; + var markers = typeDef.expand(eventDef.recurringDef.typeData, { + start: dateEnv.subtract(framingRange.start, duration), + end: framingRange.end + }, dateEnv); + // the recurrence plugins don't guarantee that all-day events are start-of-day, so we have to + if (eventDef.allDay) { + markers = markers.map(startOfDay); + } + return markers; + } + + var hasOwnProperty = Object.prototype.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; + var name; + var complexObjs; + var j; + var 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' && val) { // non-null 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; + } + function filterHash(hash, func) { + var filtered = {}; + for (var key in hash) { + if (func(hash[key], key)) { + filtered[key] = hash[key]; + } + } + return filtered; + } + function mapHash(hash, func) { + var newHash = {}; + for (var key in hash) { + newHash[key] = func(hash[key], key); + } + return newHash; + } + function arrayToHash(a) { + var hash = {}; + for (var _i = 0, a_1 = a; _i < a_1.length; _i++) { + var item = a_1[_i]; + hash[item] = true; + } + return hash; + } + function hashValuesToArray(obj) { + var a = []; + for (var key in obj) { + a.push(obj[key]); + } + return a; + } + function isPropsEqual(obj0, obj1) { + for (var key in obj0) { + if (hasOwnProperty.call(obj0, key)) { + if (!(key in obj1)) { + return false; + } + } + } + for (var key in obj1) { + if (hasOwnProperty.call(obj1, key)) { + if (obj0[key] !== obj1[key]) { + return false; + } + } + } + return true; + } + + function parseEvents(rawEvents, sourceId, calendar, allowOpenRange) { + var eventStore = createEmptyEventStore(); + for (var _i = 0, rawEvents_1 = rawEvents; _i < rawEvents_1.length; _i++) { + var rawEvent = rawEvents_1[_i]; + var tuple = parseEvent(rawEvent, sourceId, calendar, allowOpenRange); + if (tuple) { + eventTupleToStore(tuple, eventStore); + } + } + return eventStore; + } + function eventTupleToStore(tuple, eventStore) { + if (eventStore === void 0) { eventStore = createEmptyEventStore(); } + eventStore.defs[tuple.def.defId] = tuple.def; + if (tuple.instance) { + eventStore.instances[tuple.instance.instanceId] = tuple.instance; + } + return eventStore; + } + function expandRecurring(eventStore, framingRange, calendar) { + var dateEnv = calendar.dateEnv; + var defs = eventStore.defs, instances = eventStore.instances; + // remove existing recurring instances + instances = filterHash(instances, function (instance) { + return !defs[instance.defId].recurringDef; + }); + for (var defId in defs) { + var def = defs[defId]; + if (def.recurringDef) { + var duration = def.recurringDef.duration; + if (!duration) { + duration = def.allDay ? + calendar.defaultAllDayEventDuration : + calendar.defaultTimedEventDuration; + } + var starts = expandRecurringRanges(def, duration, framingRange, calendar.dateEnv, calendar.pluginSystem.hooks.recurringTypes); + for (var _i = 0, starts_1 = starts; _i < starts_1.length; _i++) { + var start = starts_1[_i]; + var instance = createEventInstance(defId, { + start: start, + end: dateEnv.add(start, duration) + }); + instances[instance.instanceId] = instance; + } + } + } + return { defs: defs, instances: instances }; + } + // retrieves events that have the same groupId as the instance specified by `instanceId` + // or they are the same as the instance. + // why might instanceId not be in the store? an event from another calendar? + function getRelevantEvents(eventStore, instanceId) { + var instance = eventStore.instances[instanceId]; + if (instance) { + var def_1 = eventStore.defs[instance.defId]; + // get events/instances with same group + var newStore = filterEventStoreDefs(eventStore, function (lookDef) { + return isEventDefsGrouped(def_1, lookDef); + }); + // add the original + // TODO: wish we could use eventTupleToStore or something like it + newStore.defs[def_1.defId] = def_1; + newStore.instances[instance.instanceId] = instance; + return newStore; + } + return createEmptyEventStore(); + } + function isEventDefsGrouped(def0, def1) { + return Boolean(def0.groupId && def0.groupId === def1.groupId); + } + function transformRawEvents(rawEvents, eventSource, calendar) { + var calEachTransform = calendar.opt('eventDataTransform'); + var sourceEachTransform = eventSource ? eventSource.eventDataTransform : null; + if (sourceEachTransform) { + rawEvents = transformEachRawEvent(rawEvents, sourceEachTransform); + } + if (calEachTransform) { + rawEvents = transformEachRawEvent(rawEvents, calEachTransform); + } + return rawEvents; + } + function transformEachRawEvent(rawEvents, func) { + var refinedEvents; + if (!func) { + refinedEvents = rawEvents; + } + else { + refinedEvents = []; + for (var _i = 0, rawEvents_2 = rawEvents; _i < rawEvents_2.length; _i++) { + var rawEvent = rawEvents_2[_i]; + var refinedEvent = func(rawEvent); + if (refinedEvent) { + refinedEvents.push(refinedEvent); + } + else if (refinedEvent == null) { + refinedEvents.push(rawEvent); + } // if a different falsy value, do nothing + } + } + return refinedEvents; + } + function createEmptyEventStore() { + return { defs: {}, instances: {} }; + } + function mergeEventStores(store0, store1) { + return { + defs: __assign({}, store0.defs, store1.defs), + instances: __assign({}, store0.instances, store1.instances) + }; + } + function filterEventStoreDefs(eventStore, filterFunc) { + var defs = filterHash(eventStore.defs, filterFunc); + var instances = filterHash(eventStore.instances, function (instance) { + return defs[instance.defId]; // still exists? + }); + return { defs: defs, instances: instances }; + } + + function parseRange(input, dateEnv) { + var start = null; + var end = null; + if (input.start) { + start = dateEnv.createMarker(input.start); + } + if (input.end) { + end = dateEnv.createMarker(input.end); + } + if (!start && !end) { + return null; + } + if (start && end && end < start) { + return null; + } + return { start: start, end: end }; + } + // SIDE-EFFECT: will mutate ranges. + // Will return a new array result. + function invertRanges(ranges, constraintRange) { + var invertedRanges = []; + var start = constraintRange.start; // the end of the previous range. the start of the new range + var i; + var dateRange; + // ranges need to be in order. required for our date-walking algorithm + ranges.sort(compareRanges); + for (i = 0; i < ranges.length; i++) { + dateRange = ranges[i]; + // add the span of time before the event (if there is any) + if (dateRange.start > start) { // compare millisecond time (skip any ambig logic) + invertedRanges.push({ start: start, end: dateRange.start }); + } + if (dateRange.end > start) { + start = dateRange.end; + } + } + // add the span of time after the last event (if there is any) + if (start < constraintRange.end) { // compare millisecond time (skip any ambig logic) + invertedRanges.push({ start: start, end: constraintRange.end }); + } + return invertedRanges; + } + function compareRanges(range0, range1) { + return range0.start.valueOf() - range1.start.valueOf(); // earlier ranges go first + } + function intersectRanges(range0, range1) { + var start = range0.start; + var end = range0.end; + var newRange = null; + if (range1.start !== null) { + if (start === null) { + start = range1.start; + } + else { + start = new Date(Math.max(start.valueOf(), range1.start.valueOf())); + } + } + if (range1.end != null) { + if (end === null) { + end = range1.end; + } + else { + end = new Date(Math.min(end.valueOf(), range1.end.valueOf())); + } + } + if (start === null || end === null || start < end) { + newRange = { start: start, end: end }; + } + return newRange; + } + function rangesEqual(range0, range1) { + return (range0.start === null ? null : range0.start.valueOf()) === (range1.start === null ? null : range1.start.valueOf()) && + (range0.end === null ? null : range0.end.valueOf()) === (range1.end === null ? null : range1.end.valueOf()); + } + function rangesIntersect(range0, range1) { + return (range0.end === null || range1.start === null || range0.end > range1.start) && + (range0.start === null || range1.end === null || range0.start < range1.end); + } + function rangeContainsRange(outerRange, innerRange) { + return (outerRange.start === null || (innerRange.start !== null && innerRange.start >= outerRange.start)) && + (outerRange.end === null || (innerRange.end !== null && innerRange.end <= outerRange.end)); + } + function rangeContainsMarker(range, date) { + return (range.start === null || date >= range.start) && + (range.end === null || date < range.end); + } + // If the given date is not within the given range, move it inside. + // (If it's past the end, make it one millisecond before the end). + function constrainMarkerToRange(date, range) { + if (range.start != null && date < range.start) { + return range.start; + } + if (range.end != null && date >= range.end) { + return new Date(range.end.valueOf() - 1); + } + return date; + } + + function removeExact(array, exactVal) { + var removeCnt = 0; + var i = 0; + while (i < array.length) { + if (array[i] === exactVal) { + array.splice(i, 1); + removeCnt++; + } + else { + i++; + } + } + return removeCnt; + } + function isArraysEqual(a0, a1) { + var len = a0.length; + var i; + if (len !== a1.length) { // not array? or not same length? + return false; + } + for (i = 0; i < len; i++) { + if (a0[i] !== a1[i]) { + return false; + } + } + return true; + } + + function memoize(workerFunc) { + var args; + var res; + return function () { + if (!args || !isArraysEqual(args, arguments)) { + args = arguments; + res = workerFunc.apply(this, arguments); + } + return res; + }; + } + /* + always executes the workerFunc, but if the result is equal to the previous result, + return the previous result instead. + */ + function memoizeOutput(workerFunc, equalityFunc) { + var cachedRes = null; + return function () { + var newRes = workerFunc.apply(this, arguments); + if (cachedRes === null || !(cachedRes === newRes || equalityFunc(cachedRes, newRes))) { + cachedRes = newRes; + } + return cachedRes; + }; + } + + var EXTENDED_SETTINGS_AND_SEVERITIES = { + week: 3, + separator: 0, + omitZeroMinute: 0, + meridiem: 0, + omitCommas: 0 + }; + var STANDARD_DATE_PROP_SEVERITIES = { + timeZoneName: 7, + era: 6, + year: 5, + month: 4, + day: 2, + weekday: 2, + hour: 1, + minute: 1, + second: 1 + }; + var MERIDIEM_RE = /\s*([ap])\.?m\.?/i; // eats up leading spaces too + var COMMA_RE = /,/g; // we need re for globalness + var MULTI_SPACE_RE = /\s+/g; + var LTR_RE = /\u200e/g; // control character + var UTC_RE = /UTC|GMT/; + var NativeFormatter = /** @class */ (function () { + function NativeFormatter(formatSettings) { + var standardDateProps = {}; + var extendedSettings = {}; + var severity = 0; + for (var name_1 in formatSettings) { + if (name_1 in EXTENDED_SETTINGS_AND_SEVERITIES) { + extendedSettings[name_1] = formatSettings[name_1]; + severity = Math.max(EXTENDED_SETTINGS_AND_SEVERITIES[name_1], severity); + } + else { + standardDateProps[name_1] = formatSettings[name_1]; + if (name_1 in STANDARD_DATE_PROP_SEVERITIES) { + severity = Math.max(STANDARD_DATE_PROP_SEVERITIES[name_1], severity); + } + } + } + this.standardDateProps = standardDateProps; + this.extendedSettings = extendedSettings; + this.severity = severity; + this.buildFormattingFunc = memoize(buildFormattingFunc); + } + NativeFormatter.prototype.format = function (date, context) { + return this.buildFormattingFunc(this.standardDateProps, this.extendedSettings, context)(date); + }; + NativeFormatter.prototype.formatRange = function (start, end, context) { + var _a = this, standardDateProps = _a.standardDateProps, extendedSettings = _a.extendedSettings; + var diffSeverity = computeMarkerDiffSeverity(start.marker, end.marker, context.calendarSystem); + if (!diffSeverity) { + return this.format(start, context); + } + var biggestUnitForPartial = diffSeverity; + if (biggestUnitForPartial > 1 && // the two dates are different in a way that's larger scale than time + (standardDateProps.year === 'numeric' || standardDateProps.year === '2-digit') && + (standardDateProps.month === 'numeric' || standardDateProps.month === '2-digit') && + (standardDateProps.day === 'numeric' || standardDateProps.day === '2-digit')) { + biggestUnitForPartial = 1; // make it look like the dates are only different in terms of time + } + var full0 = this.format(start, context); + var full1 = this.format(end, context); + if (full0 === full1) { + return full0; + } + var partialDateProps = computePartialFormattingOptions(standardDateProps, biggestUnitForPartial); + var partialFormattingFunc = buildFormattingFunc(partialDateProps, extendedSettings, context); + var partial0 = partialFormattingFunc(start); + var partial1 = partialFormattingFunc(end); + var insertion = findCommonInsertion(full0, partial0, full1, partial1); + var separator = extendedSettings.separator || ''; + if (insertion) { + return insertion.before + partial0 + separator + partial1 + insertion.after; + } + return full0 + separator + full1; + }; + NativeFormatter.prototype.getLargestUnit = function () { + switch (this.severity) { + case 7: + case 6: + case 5: + return 'year'; + case 4: + return 'month'; + case 3: + return 'week'; + default: + return 'day'; + } + }; + return NativeFormatter; + }()); + function buildFormattingFunc(standardDateProps, extendedSettings, context) { + var standardDatePropCnt = Object.keys(standardDateProps).length; + if (standardDatePropCnt === 1 && standardDateProps.timeZoneName === 'short') { + return function (date) { + return formatTimeZoneOffset(date.timeZoneOffset); + }; + } + if (standardDatePropCnt === 0 && extendedSettings.week) { + return function (date) { + return formatWeekNumber(context.computeWeekNumber(date.marker), context.weekLabel, context.locale, extendedSettings.week); + }; + } + return buildNativeFormattingFunc(standardDateProps, extendedSettings, context); + } + function buildNativeFormattingFunc(standardDateProps, extendedSettings, context) { + standardDateProps = __assign({}, standardDateProps); // copy + extendedSettings = __assign({}, extendedSettings); // copy + sanitizeSettings(standardDateProps, extendedSettings); + standardDateProps.timeZone = 'UTC'; // we leverage the only guaranteed timeZone for our UTC markers + var normalFormat = new Intl.DateTimeFormat(context.locale.codes, standardDateProps); + var zeroFormat; // needed? + if (extendedSettings.omitZeroMinute) { + var zeroProps = __assign({}, standardDateProps); + delete zeroProps.minute; // seconds and ms were already considered in sanitizeSettings + zeroFormat = new Intl.DateTimeFormat(context.locale.codes, zeroProps); + } + return function (date) { + var marker = date.marker; + var format; + if (zeroFormat && !marker.getUTCMinutes()) { + format = zeroFormat; + } + else { + format = normalFormat; + } + var s = format.format(marker); + return postProcess(s, date, standardDateProps, extendedSettings, context); + }; + } + function sanitizeSettings(standardDateProps, extendedSettings) { + // deal with a browser inconsistency where formatting the timezone + // requires that the hour/minute be present. + if (standardDateProps.timeZoneName) { + if (!standardDateProps.hour) { + standardDateProps.hour = '2-digit'; + } + if (!standardDateProps.minute) { + standardDateProps.minute = '2-digit'; + } + } + // only support short timezone names + if (standardDateProps.timeZoneName === 'long') { + standardDateProps.timeZoneName = 'short'; + } + // if requesting to display seconds, MUST display minutes + if (extendedSettings.omitZeroMinute && (standardDateProps.second || standardDateProps.millisecond)) { + delete extendedSettings.omitZeroMinute; + } + } + function postProcess(s, date, standardDateProps, extendedSettings, context) { + s = s.replace(LTR_RE, ''); // remove left-to-right control chars. do first. good for other regexes + if (standardDateProps.timeZoneName === 'short') { + s = injectTzoStr(s, (context.timeZone === 'UTC' || date.timeZoneOffset == null) ? + 'UTC' : // important to normalize for IE, which does "GMT" + formatTimeZoneOffset(date.timeZoneOffset)); + } + if (extendedSettings.omitCommas) { + s = s.replace(COMMA_RE, '').trim(); + } + if (extendedSettings.omitZeroMinute) { + s = s.replace(':00', ''); // zeroFormat doesn't always achieve this + } + // ^ do anything that might create adjacent spaces before this point, + // because MERIDIEM_RE likes to eat up loading spaces + if (extendedSettings.meridiem === false) { + s = s.replace(MERIDIEM_RE, '').trim(); + } + else if (extendedSettings.meridiem === 'narrow') { // a/p + s = s.replace(MERIDIEM_RE, function (m0, m1) { + return m1.toLocaleLowerCase(); + }); + } + else if (extendedSettings.meridiem === 'short') { // am/pm + s = s.replace(MERIDIEM_RE, function (m0, m1) { + return m1.toLocaleLowerCase() + 'm'; + }); + } + else if (extendedSettings.meridiem === 'lowercase') { // other meridiem transformers already converted to lowercase + s = s.replace(MERIDIEM_RE, function (m0) { + return m0.toLocaleLowerCase(); + }); + } + s = s.replace(MULTI_SPACE_RE, ' '); + s = s.trim(); + return s; + } + function injectTzoStr(s, tzoStr) { + var replaced = false; + s = s.replace(UTC_RE, function () { + replaced = true; + return tzoStr; + }); + // IE11 doesn't include UTC/GMT in the original string, so append to end + if (!replaced) { + s += ' ' + tzoStr; + } + return s; + } + function formatWeekNumber(num, weekLabel, locale, display) { + var parts = []; + if (display === 'narrow') { + parts.push(weekLabel); + } + else if (display === 'short') { + parts.push(weekLabel, ' '); + } + // otherwise, considered 'numeric' + parts.push(locale.simpleNumberFormat.format(num)); + if (locale.options.isRtl) { // TODO: use control characters instead? + parts.reverse(); + } + return parts.join(''); + } + // Range Formatting Utils + // 0 = exactly the same + // 1 = different by time + // and bigger + function computeMarkerDiffSeverity(d0, d1, ca) { + if (ca.getMarkerYear(d0) !== ca.getMarkerYear(d1)) { + return 5; + } + if (ca.getMarkerMonth(d0) !== ca.getMarkerMonth(d1)) { + return 4; + } + if (ca.getMarkerDay(d0) !== ca.getMarkerDay(d1)) { + return 2; + } + if (timeAsMs(d0) !== timeAsMs(d1)) { + return 1; + } + return 0; + } + function computePartialFormattingOptions(options, biggestUnit) { + var partialOptions = {}; + for (var name_2 in options) { + if (!(name_2 in STANDARD_DATE_PROP_SEVERITIES) || // not a date part prop (like timeZone) + STANDARD_DATE_PROP_SEVERITIES[name_2] <= biggestUnit) { + partialOptions[name_2] = options[name_2]; + } + } + return partialOptions; + } + function findCommonInsertion(full0, partial0, full1, partial1) { + var i0 = 0; + while (i0 < full0.length) { + var found0 = full0.indexOf(partial0, i0); + if (found0 === -1) { + break; + } + var before0 = full0.substr(0, found0); + i0 = found0 + partial0.length; + var after0 = full0.substr(i0); + var i1 = 0; + while (i1 < full1.length) { + var found1 = full1.indexOf(partial1, i1); + if (found1 === -1) { + break; + } + var before1 = full1.substr(0, found1); + i1 = found1 + partial1.length; + var after1 = full1.substr(i1); + if (before0 === before1 && after0 === after1) { + return { + before: before0, + after: after0 + }; + } + } + } + return null; + } + + /* + TODO: fix the terminology of "formatter" vs "formatting func" + */ + /* + At the time of instantiation, this object does not know which cmd-formatting system it will use. + It receives this at the time of formatting, as a setting. + */ + var CmdFormatter = /** @class */ (function () { + function CmdFormatter(cmdStr, separator) { + this.cmdStr = cmdStr; + this.separator = separator; + } + CmdFormatter.prototype.format = function (date, context) { + return context.cmdFormatter(this.cmdStr, createVerboseFormattingArg(date, null, context, this.separator)); + }; + CmdFormatter.prototype.formatRange = function (start, end, context) { + return context.cmdFormatter(this.cmdStr, createVerboseFormattingArg(start, end, context, this.separator)); + }; + return CmdFormatter; + }()); + + var FuncFormatter = /** @class */ (function () { + function FuncFormatter(func) { + this.func = func; + } + FuncFormatter.prototype.format = function (date, context) { + return this.func(createVerboseFormattingArg(date, null, context)); + }; + FuncFormatter.prototype.formatRange = function (start, end, context) { + return this.func(createVerboseFormattingArg(start, end, context)); + }; + return FuncFormatter; + }()); + + // Formatter Object Creation + function createFormatter(input, defaultSeparator) { + if (typeof input === 'object' && input) { // non-null object + if (typeof defaultSeparator === 'string') { + input = __assign({ separator: defaultSeparator }, input); + } + return new NativeFormatter(input); + } + else if (typeof input === 'string') { + return new CmdFormatter(input, defaultSeparator); + } + else if (typeof input === 'function') { + return new FuncFormatter(input); + } + } + // String Utils + // timeZoneOffset is in minutes + function buildIsoString(marker, timeZoneOffset, stripZeroTime) { + if (stripZeroTime === void 0) { stripZeroTime = false; } + var s = marker.toISOString(); + s = s.replace('.000', ''); + if (stripZeroTime) { + s = s.replace('T00:00:00Z', ''); + } + if (s.length > 10) { // time part wasn't stripped, can add timezone info + if (timeZoneOffset == null) { + s = s.replace('Z', ''); + } + else if (timeZoneOffset !== 0) { + s = s.replace('Z', formatTimeZoneOffset(timeZoneOffset, true)); + } + // otherwise, its UTC-0 and we want to keep the Z + } + return s; + } + function formatIsoTimeString(marker) { + return padStart(marker.getUTCHours(), 2) + ':' + + padStart(marker.getUTCMinutes(), 2) + ':' + + padStart(marker.getUTCSeconds(), 2); + } + function formatTimeZoneOffset(minutes, doIso) { + if (doIso === void 0) { doIso = false; } + var sign = minutes < 0 ? '-' : '+'; + var abs = Math.abs(minutes); + var hours = Math.floor(abs / 60); + var mins = Math.round(abs % 60); + if (doIso) { + return sign + padStart(hours, 2) + ':' + padStart(mins, 2); + } + else { + return 'GMT' + sign + hours + (mins ? ':' + padStart(mins, 2) : ''); + } + } + // Arg Utils + function createVerboseFormattingArg(start, end, context, separator) { + var startInfo = expandZonedMarker(start, context.calendarSystem); + var endInfo = end ? expandZonedMarker(end, context.calendarSystem) : null; + return { + date: startInfo, + start: startInfo, + end: endInfo, + timeZone: context.timeZone, + localeCodes: context.locale.codes, + separator: separator + }; + } + function expandZonedMarker(dateInfo, calendarSystem) { + var a = calendarSystem.markerToArray(dateInfo.marker); + return { + marker: dateInfo.marker, + timeZoneOffset: dateInfo.timeZoneOffset, + array: a, + year: a[0], + month: a[1], + day: a[2], + hour: a[3], + minute: a[4], + second: a[5], + millisecond: a[6] + }; + } + + var EventSourceApi = /** @class */ (function () { + function EventSourceApi(calendar, internalEventSource) { + this.calendar = calendar; + this.internalEventSource = internalEventSource; + } + EventSourceApi.prototype.remove = function () { + this.calendar.dispatch({ + type: 'REMOVE_EVENT_SOURCE', + sourceId: this.internalEventSource.sourceId + }); + }; + EventSourceApi.prototype.refetch = function () { + this.calendar.dispatch({ + type: 'FETCH_EVENT_SOURCES', + sourceIds: [this.internalEventSource.sourceId] + }); + }; + Object.defineProperty(EventSourceApi.prototype, "id", { + get: function () { + return this.internalEventSource.publicId; + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(EventSourceApi.prototype, "url", { + // only relevant to json-feed event sources + get: function () { + return this.internalEventSource.meta.url; + }, + enumerable: true, + configurable: true + }); + return EventSourceApi; + }()); + + var EventApi = /** @class */ (function () { + function EventApi(calendar, def, instance) { + this._calendar = calendar; + this._def = def; + this._instance = instance || null; + } + /* + TODO: make event struct more responsible for this + */ + EventApi.prototype.setProp = function (name, val) { + var _a, _b; + if (name in DATE_PROPS) ; + else if (name in NON_DATE_PROPS) { + if (typeof NON_DATE_PROPS[name] === 'function') { + val = NON_DATE_PROPS[name](val); + } + this.mutate({ + standardProps: (_a = {}, _a[name] = val, _a) + }); + } + else if (name in UNSCOPED_EVENT_UI_PROPS) { + var ui = void 0; + if (typeof UNSCOPED_EVENT_UI_PROPS[name] === 'function') { + val = UNSCOPED_EVENT_UI_PROPS[name](val); + } + if (name === 'color') { + ui = { backgroundColor: val, borderColor: val }; + } + else if (name === 'editable') { + ui = { startEditable: val, durationEditable: val }; + } + else { + ui = (_b = {}, _b[name] = val, _b); + } + this.mutate({ + standardProps: { ui: ui } + }); + } + }; + EventApi.prototype.setExtendedProp = function (name, val) { + var _a; + this.mutate({ + extendedProps: (_a = {}, _a[name] = val, _a) + }); + }; + EventApi.prototype.setStart = function (startInput, options) { + if (options === void 0) { options = {}; } + var dateEnv = this._calendar.dateEnv; + var start = dateEnv.createMarker(startInput); + if (start && this._instance) { // TODO: warning if parsed bad + var instanceRange = this._instance.range; + var startDelta = diffDates(instanceRange.start, start, dateEnv, options.granularity); // what if parsed bad!? + if (options.maintainDuration) { + this.mutate({ datesDelta: startDelta }); + } + else { + this.mutate({ startDelta: startDelta }); + } + } + }; + EventApi.prototype.setEnd = function (endInput, options) { + if (options === void 0) { options = {}; } + var dateEnv = this._calendar.dateEnv; + var end; + if (endInput != null) { + end = dateEnv.createMarker(endInput); + if (!end) { + return; // TODO: warning if parsed bad + } + } + if (this._instance) { + if (end) { + var endDelta = diffDates(this._instance.range.end, end, dateEnv, options.granularity); + this.mutate({ endDelta: endDelta }); + } + else { + this.mutate({ standardProps: { hasEnd: false } }); + } + } + }; + EventApi.prototype.setDates = function (startInput, endInput, options) { + if (options === void 0) { options = {}; } + var dateEnv = this._calendar.dateEnv; + var standardProps = { allDay: options.allDay }; + var start = dateEnv.createMarker(startInput); + var end; + if (!start) { + return; // TODO: warning if parsed bad + } + if (endInput != null) { + end = dateEnv.createMarker(endInput); + if (!end) { // TODO: warning if parsed bad + return; + } + } + if (this._instance) { + var instanceRange = this._instance.range; + // when computing the diff for an event being converted to all-day, + // compute diff off of the all-day values the way event-mutation does. + if (options.allDay === true) { + instanceRange = computeAlignedDayRange(instanceRange); + } + var startDelta = diffDates(instanceRange.start, start, dateEnv, options.granularity); + if (end) { + var endDelta = diffDates(instanceRange.end, end, dateEnv, options.granularity); + if (durationsEqual(startDelta, endDelta)) { + this.mutate({ datesDelta: startDelta, standardProps: standardProps }); + } + else { + this.mutate({ startDelta: startDelta, endDelta: endDelta, standardProps: standardProps }); + } + } + else { // means "clear the end" + standardProps.hasEnd = false; + this.mutate({ datesDelta: startDelta, standardProps: standardProps }); + } + } + }; + EventApi.prototype.moveStart = function (deltaInput) { + var delta = createDuration(deltaInput); + if (delta) { // TODO: warning if parsed bad + this.mutate({ startDelta: delta }); + } + }; + EventApi.prototype.moveEnd = function (deltaInput) { + var delta = createDuration(deltaInput); + if (delta) { // TODO: warning if parsed bad + this.mutate({ endDelta: delta }); + } + }; + EventApi.prototype.moveDates = function (deltaInput) { + var delta = createDuration(deltaInput); + if (delta) { // TODO: warning if parsed bad + this.mutate({ datesDelta: delta }); + } + }; + EventApi.prototype.setAllDay = function (allDay, options) { + if (options === void 0) { options = {}; } + var standardProps = { allDay: allDay }; + var maintainDuration = options.maintainDuration; + if (maintainDuration == null) { + maintainDuration = this._calendar.opt('allDayMaintainDuration'); + } + if (this._def.allDay !== allDay) { + standardProps.hasEnd = maintainDuration; + } + this.mutate({ standardProps: standardProps }); + }; + EventApi.prototype.formatRange = function (formatInput) { + var dateEnv = this._calendar.dateEnv; + var instance = this._instance; + var formatter = createFormatter(formatInput, this._calendar.opt('defaultRangeSeparator')); + if (this._def.hasEnd) { + return dateEnv.formatRange(instance.range.start, instance.range.end, formatter, { + forcedStartTzo: instance.forcedStartTzo, + forcedEndTzo: instance.forcedEndTzo + }); + } + else { + return dateEnv.format(instance.range.start, formatter, { + forcedTzo: instance.forcedStartTzo + }); + } + }; + EventApi.prototype.mutate = function (mutation) { + var def = this._def; + var instance = this._instance; + if (instance) { + this._calendar.dispatch({ + type: 'MUTATE_EVENTS', + instanceId: instance.instanceId, + mutation: mutation, + fromApi: true + }); + var eventStore = this._calendar.state.eventStore; + this._def = eventStore.defs[def.defId]; + this._instance = eventStore.instances[instance.instanceId]; + } + }; + EventApi.prototype.remove = function () { + this._calendar.dispatch({ + type: 'REMOVE_EVENT_DEF', + defId: this._def.defId + }); + }; + Object.defineProperty(EventApi.prototype, "source", { + get: function () { + var sourceId = this._def.sourceId; + if (sourceId) { + return new EventSourceApi(this._calendar, this._calendar.state.eventSources[sourceId]); + } + return null; + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "start", { + get: function () { + return this._instance ? + this._calendar.dateEnv.toDate(this._instance.range.start) : + null; + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "end", { + get: function () { + return (this._instance && this._def.hasEnd) ? + this._calendar.dateEnv.toDate(this._instance.range.end) : + null; + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "id", { + // computable props that all access the def + // TODO: find a TypeScript-compatible way to do this at scale + get: function () { return this._def.publicId; }, + enumerable: true, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "groupId", { + get: function () { return this._def.groupId; }, + enumerable: true, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "allDay", { + get: function () { return this._def.allDay; }, + enumerable: true, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "title", { + get: function () { return this._def.title; }, + enumerable: true, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "url", { + get: function () { return this._def.url; }, + enumerable: true, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "rendering", { + get: function () { return this._def.rendering; }, + enumerable: true, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "startEditable", { + get: function () { return this._def.ui.startEditable; }, + enumerable: true, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "durationEditable", { + get: function () { return this._def.ui.durationEditable; }, + enumerable: true, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "constraint", { + get: function () { return this._def.ui.constraints[0] || null; }, + enumerable: true, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "overlap", { + get: function () { return this._def.ui.overlap; }, + enumerable: true, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "allow", { + get: function () { return this._def.ui.allows[0] || null; }, + enumerable: true, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "backgroundColor", { + get: function () { return this._def.ui.backgroundColor; }, + enumerable: true, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "borderColor", { + get: function () { return this._def.ui.borderColor; }, + enumerable: true, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "textColor", { + get: function () { return this._def.ui.textColor; }, + enumerable: true, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "classNames", { + // NOTE: user can't modify these because Object.freeze was called in event-def parsing + get: function () { return this._def.ui.classNames; }, + enumerable: true, + configurable: true + }); + Object.defineProperty(EventApi.prototype, "extendedProps", { + get: function () { return this._def.extendedProps; }, + enumerable: true, + configurable: true + }); + return EventApi; + }()); + + /* + Specifying nextDayThreshold signals that all-day ranges should be sliced. + */ + function sliceEventStore(eventStore, eventUiBases, framingRange, nextDayThreshold) { + var inverseBgByGroupId = {}; + var inverseBgByDefId = {}; + var defByGroupId = {}; + var bgRanges = []; + var fgRanges = []; + var eventUis = compileEventUis(eventStore.defs, eventUiBases); + for (var defId in eventStore.defs) { + var def = eventStore.defs[defId]; + if (def.rendering === 'inverse-background') { + if (def.groupId) { + inverseBgByGroupId[def.groupId] = []; + if (!defByGroupId[def.groupId]) { + defByGroupId[def.groupId] = def; + } + } + else { + inverseBgByDefId[defId] = []; + } + } + } + for (var instanceId in eventStore.instances) { + var instance = eventStore.instances[instanceId]; + var def = eventStore.defs[instance.defId]; + var ui = eventUis[def.defId]; + var origRange = instance.range; + var normalRange = (!def.allDay && nextDayThreshold) ? + computeVisibleDayRange(origRange, nextDayThreshold) : + origRange; + var slicedRange = intersectRanges(normalRange, framingRange); + if (slicedRange) { + if (def.rendering === 'inverse-background') { + if (def.groupId) { + inverseBgByGroupId[def.groupId].push(slicedRange); + } + else { + inverseBgByDefId[instance.defId].push(slicedRange); + } + } + else { + (def.rendering === 'background' ? bgRanges : fgRanges).push({ + def: def, + ui: ui, + instance: instance, + range: slicedRange, + isStart: normalRange.start && normalRange.start.valueOf() === slicedRange.start.valueOf(), + isEnd: normalRange.end && normalRange.end.valueOf() === slicedRange.end.valueOf() + }); + } + } + } + for (var groupId in inverseBgByGroupId) { // BY GROUP + var ranges = inverseBgByGroupId[groupId]; + var invertedRanges = invertRanges(ranges, framingRange); + for (var _i = 0, invertedRanges_1 = invertedRanges; _i < invertedRanges_1.length; _i++) { + var invertedRange = invertedRanges_1[_i]; + var def = defByGroupId[groupId]; + var ui = eventUis[def.defId]; + bgRanges.push({ + def: def, + ui: ui, + instance: null, + range: invertedRange, + isStart: false, + isEnd: false + }); + } + } + for (var defId in inverseBgByDefId) { + var ranges = inverseBgByDefId[defId]; + var invertedRanges = invertRanges(ranges, framingRange); + for (var _a = 0, invertedRanges_2 = invertedRanges; _a < invertedRanges_2.length; _a++) { + var invertedRange = invertedRanges_2[_a]; + bgRanges.push({ + def: eventStore.defs[defId], + ui: eventUis[defId], + instance: null, + range: invertedRange, + isStart: false, + isEnd: false + }); + } + } + return { bg: bgRanges, fg: fgRanges }; + } + function hasBgRendering(def) { + return def.rendering === 'background' || def.rendering === 'inverse-background'; + } + function filterSegsViaEls(context, segs, isMirror) { + var calendar = context.calendar, view = context.view; + if (calendar.hasPublicHandlers('eventRender')) { + segs = segs.filter(function (seg) { + var custom = calendar.publiclyTrigger('eventRender', [ + { + event: new EventApi(calendar, seg.eventRange.def, seg.eventRange.instance), + isMirror: isMirror, + isStart: seg.isStart, + isEnd: seg.isEnd, + // TODO: include seg.range once all components consistently generate it + el: seg.el, + view: view + } + ]); + if (custom === false) { // means don't render at all + return false; + } + else if (custom && custom !== true) { + seg.el = custom; + } + return true; + }); + } + for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) { + var seg = segs_1[_i]; + setElSeg(seg.el, seg); + } + return segs; + } + function setElSeg(el, seg) { + el.fcSeg = seg; + } + function getElSeg(el) { + return el.fcSeg || null; + } + // event ui computation + function compileEventUis(eventDefs, eventUiBases) { + return mapHash(eventDefs, function (eventDef) { + return compileEventUi(eventDef, eventUiBases); + }); + } + function compileEventUi(eventDef, eventUiBases) { + var uis = []; + if (eventUiBases['']) { + uis.push(eventUiBases['']); + } + if (eventUiBases[eventDef.defId]) { + uis.push(eventUiBases[eventDef.defId]); + } + uis.push(eventDef.ui); + return combineEventUis(uis); + } + // triggers + function triggerRenderedSegs(context, segs, isMirrors) { + var calendar = context.calendar, view = context.view; + if (calendar.hasPublicHandlers('eventPositioned')) { + for (var _i = 0, segs_2 = segs; _i < segs_2.length; _i++) { + var seg = segs_2[_i]; + calendar.publiclyTriggerAfterSizing('eventPositioned', [ + { + event: new EventApi(calendar, seg.eventRange.def, seg.eventRange.instance), + isMirror: isMirrors, + isStart: seg.isStart, + isEnd: seg.isEnd, + el: seg.el, + view: view + } + ]); + } + } + if (!calendar.state.loadingLevel) { // avoid initial empty state while pending + calendar.afterSizingTriggers._eventsPositioned = [null]; // fire once + } + } + function triggerWillRemoveSegs(context, segs, isMirrors) { + var calendar = context.calendar, view = context.view; + for (var _i = 0, segs_3 = segs; _i < segs_3.length; _i++) { + var seg = segs_3[_i]; + calendar.trigger('eventElRemove', seg.el); + } + if (calendar.hasPublicHandlers('eventDestroy')) { + for (var _a = 0, segs_4 = segs; _a < segs_4.length; _a++) { + var seg = segs_4[_a]; + calendar.publiclyTrigger('eventDestroy', [ + { + event: new EventApi(calendar, seg.eventRange.def, seg.eventRange.instance), + isMirror: isMirrors, + el: seg.el, + view: view + } + ]); + } + } + } + // is-interactable + function computeEventDraggable(context, eventDef, eventUi) { + var calendar = context.calendar, view = context.view; + var transformers = calendar.pluginSystem.hooks.isDraggableTransformers; + var val = eventUi.startEditable; + for (var _i = 0, transformers_1 = transformers; _i < transformers_1.length; _i++) { + var transformer = transformers_1[_i]; + val = transformer(val, eventDef, eventUi, view); + } + return val; + } + function computeEventStartResizable(context, eventDef, eventUi) { + return eventUi.durationEditable && context.options.eventResizableFromStart; + } + function computeEventEndResizable(context, eventDef, eventUi) { + return eventUi.durationEditable; + } + + // applies the mutation to ALL defs/instances within the event store + function applyMutationToEventStore(eventStore, eventConfigBase, mutation, calendar) { + var eventConfigs = compileEventUis(eventStore.defs, eventConfigBase); + var dest = createEmptyEventStore(); + for (var defId in eventStore.defs) { + var def = eventStore.defs[defId]; + dest.defs[defId] = applyMutationToEventDef(def, eventConfigs[defId], mutation, calendar.pluginSystem.hooks.eventDefMutationAppliers, calendar); + } + for (var instanceId in eventStore.instances) { + var instance = eventStore.instances[instanceId]; + var def = dest.defs[instance.defId]; // important to grab the newly modified def + dest.instances[instanceId] = applyMutationToEventInstance(instance, def, eventConfigs[instance.defId], mutation, calendar); + } + return dest; + } + function applyMutationToEventDef(eventDef, eventConfig, mutation, appliers, calendar) { + var standardProps = mutation.standardProps || {}; + // if hasEnd has not been specified, guess a good value based on deltas. + // if duration will change, there's no way the default duration will persist, + // and thus, we need to mark the event as having a real end + if (standardProps.hasEnd == null && + eventConfig.durationEditable && + (mutation.startDelta || mutation.endDelta)) { + standardProps.hasEnd = true; // TODO: is this mutation okay? + } + var copy = __assign({}, eventDef, standardProps, { ui: __assign({}, eventDef.ui, standardProps.ui) }); + if (mutation.extendedProps) { + copy.extendedProps = __assign({}, copy.extendedProps, mutation.extendedProps); + } + for (var _i = 0, appliers_1 = appliers; _i < appliers_1.length; _i++) { + var applier = appliers_1[_i]; + applier(copy, mutation, calendar); + } + if (!copy.hasEnd && calendar.opt('forceEventDuration')) { + copy.hasEnd = true; + } + return copy; + } + function applyMutationToEventInstance(eventInstance, eventDef, // must first be modified by applyMutationToEventDef + eventConfig, mutation, calendar) { + var dateEnv = calendar.dateEnv; + var forceAllDay = mutation.standardProps && mutation.standardProps.allDay === true; + var clearEnd = mutation.standardProps && mutation.standardProps.hasEnd === false; + var copy = __assign({}, eventInstance); + if (forceAllDay) { + copy.range = computeAlignedDayRange(copy.range); + } + if (mutation.datesDelta && eventConfig.startEditable) { + copy.range = { + start: dateEnv.add(copy.range.start, mutation.datesDelta), + end: dateEnv.add(copy.range.end, mutation.datesDelta) + }; + } + if (mutation.startDelta && eventConfig.durationEditable) { + copy.range = { + start: dateEnv.add(copy.range.start, mutation.startDelta), + end: copy.range.end + }; + } + if (mutation.endDelta && eventConfig.durationEditable) { + copy.range = { + start: copy.range.start, + end: dateEnv.add(copy.range.end, mutation.endDelta) + }; + } + if (clearEnd) { + copy.range = { + start: copy.range.start, + end: calendar.getDefaultEventEnd(eventDef.allDay, copy.range.start) + }; + } + // in case event was all-day but the supplied deltas were not + // better util for this? + if (eventDef.allDay) { + copy.range = { + start: startOfDay(copy.range.start), + end: startOfDay(copy.range.end) + }; + } + // handle invalid durations + if (copy.range.end < copy.range.start) { + copy.range.end = calendar.getDefaultEventEnd(eventDef.allDay, copy.range.start); + } + return copy; + } + + function reduceEventStore (eventStore, action, eventSources, dateProfile, calendar) { + switch (action.type) { + case 'RECEIVE_EVENTS': // raw + return receiveRawEvents(eventStore, eventSources[action.sourceId], action.fetchId, action.fetchRange, action.rawEvents, calendar); + case 'ADD_EVENTS': // already parsed, but not expanded + return addEvent(eventStore, action.eventStore, // new ones + dateProfile ? dateProfile.activeRange : null, calendar); + case 'MERGE_EVENTS': // already parsed and expanded + return mergeEventStores(eventStore, action.eventStore); + case 'PREV': // TODO: how do we track all actions that affect dateProfile :( + case 'NEXT': + case 'SET_DATE': + case 'SET_VIEW_TYPE': + if (dateProfile) { + return expandRecurring(eventStore, dateProfile.activeRange, calendar); + } + else { + return eventStore; + } + case 'CHANGE_TIMEZONE': + return rezoneDates(eventStore, action.oldDateEnv, calendar.dateEnv); + case 'MUTATE_EVENTS': + return applyMutationToRelated(eventStore, action.instanceId, action.mutation, action.fromApi, calendar); + case 'REMOVE_EVENT_INSTANCES': + return excludeInstances(eventStore, action.instances); + case 'REMOVE_EVENT_DEF': + return filterEventStoreDefs(eventStore, function (eventDef) { + return eventDef.defId !== action.defId; + }); + case 'REMOVE_EVENT_SOURCE': + return excludeEventsBySourceId(eventStore, action.sourceId); + case 'REMOVE_ALL_EVENT_SOURCES': + return filterEventStoreDefs(eventStore, function (eventDef) { + return !eventDef.sourceId; // only keep events with no source id + }); + case 'REMOVE_ALL_EVENTS': + return createEmptyEventStore(); + case 'RESET_EVENTS': + return { + defs: eventStore.defs, + instances: eventStore.instances + }; + default: + return eventStore; + } + } + function receiveRawEvents(eventStore, eventSource, fetchId, fetchRange, rawEvents, calendar) { + if (eventSource && // not already removed + fetchId === eventSource.latestFetchId // TODO: wish this logic was always in event-sources + ) { + var subset = parseEvents(transformRawEvents(rawEvents, eventSource, calendar), eventSource.sourceId, calendar); + if (fetchRange) { + subset = expandRecurring(subset, fetchRange, calendar); + } + return mergeEventStores(excludeEventsBySourceId(eventStore, eventSource.sourceId), subset); + } + return eventStore; + } + function addEvent(eventStore, subset, expandRange, calendar) { + if (expandRange) { + subset = expandRecurring(subset, expandRange, calendar); + } + return mergeEventStores(eventStore, subset); + } + function rezoneDates(eventStore, oldDateEnv, newDateEnv) { + var defs = eventStore.defs; + var instances = mapHash(eventStore.instances, function (instance) { + var def = defs[instance.defId]; + if (def.allDay || def.recurringDef) { + return instance; // isn't dependent on timezone + } + else { + return __assign({}, instance, { range: { + start: newDateEnv.createMarker(oldDateEnv.toDate(instance.range.start, instance.forcedStartTzo)), + end: newDateEnv.createMarker(oldDateEnv.toDate(instance.range.end, instance.forcedEndTzo)) + }, forcedStartTzo: newDateEnv.canComputeOffset ? null : instance.forcedStartTzo, forcedEndTzo: newDateEnv.canComputeOffset ? null : instance.forcedEndTzo }); + } + }); + return { defs: defs, instances: instances }; + } + function applyMutationToRelated(eventStore, instanceId, mutation, fromApi, calendar) { + var relevant = getRelevantEvents(eventStore, instanceId); + var eventConfigBase = fromApi ? + { '': { + startEditable: true, + durationEditable: true, + constraints: [], + overlap: null, + allows: [], + backgroundColor: '', + borderColor: '', + textColor: '', + classNames: [] + } } : + calendar.eventUiBases; + relevant = applyMutationToEventStore(relevant, eventConfigBase, mutation, calendar); + return mergeEventStores(eventStore, relevant); + } + function excludeEventsBySourceId(eventStore, sourceId) { + return filterEventStoreDefs(eventStore, function (eventDef) { + return eventDef.sourceId !== sourceId; + }); + } + // QUESTION: why not just return instances? do a general object-property-exclusion util + function excludeInstances(eventStore, removals) { + return { + defs: eventStore.defs, + instances: filterHash(eventStore.instances, function (instance) { + return !removals[instance.instanceId]; + }) + }; + } + + // high-level segmenting-aware tester functions + // ------------------------------------------------------------------------------------------------------------------------ + function isInteractionValid(interaction, calendar) { + return isNewPropsValid({ eventDrag: interaction }, calendar); // HACK: the eventDrag props is used for ALL interactions + } + function isDateSelectionValid(dateSelection, calendar) { + return isNewPropsValid({ dateSelection: dateSelection }, calendar); + } + function isNewPropsValid(newProps, calendar) { + var view = calendar.view; + var props = __assign({ businessHours: view ? view.props.businessHours : createEmptyEventStore(), dateSelection: '', eventStore: calendar.state.eventStore, eventUiBases: calendar.eventUiBases, eventSelection: '', eventDrag: null, eventResize: null }, newProps); + return (calendar.pluginSystem.hooks.isPropsValid || isPropsValid)(props, calendar); + } + function isPropsValid(state, calendar, dateSpanMeta, filterConfig) { + if (dateSpanMeta === void 0) { dateSpanMeta = {}; } + if (state.eventDrag && !isInteractionPropsValid(state, calendar, dateSpanMeta, filterConfig)) { + return false; + } + if (state.dateSelection && !isDateSelectionPropsValid(state, calendar, dateSpanMeta, filterConfig)) { + return false; + } + return true; + } + // Moving Event Validation + // ------------------------------------------------------------------------------------------------------------------------ + function isInteractionPropsValid(state, calendar, dateSpanMeta, filterConfig) { + var interaction = state.eventDrag; // HACK: the eventDrag props is used for ALL interactions + var subjectEventStore = interaction.mutatedEvents; + var subjectDefs = subjectEventStore.defs; + var subjectInstances = subjectEventStore.instances; + var subjectConfigs = compileEventUis(subjectDefs, interaction.isEvent ? + state.eventUiBases : + { '': calendar.selectionConfig } // if not a real event, validate as a selection + ); + if (filterConfig) { + subjectConfigs = mapHash(subjectConfigs, filterConfig); + } + var otherEventStore = excludeInstances(state.eventStore, interaction.affectedEvents.instances); // exclude the subject events. TODO: exclude defs too? + var otherDefs = otherEventStore.defs; + var otherInstances = otherEventStore.instances; + var otherConfigs = compileEventUis(otherDefs, state.eventUiBases); + for (var subjectInstanceId in subjectInstances) { + var subjectInstance = subjectInstances[subjectInstanceId]; + var subjectRange = subjectInstance.range; + var subjectConfig = subjectConfigs[subjectInstance.defId]; + var subjectDef = subjectDefs[subjectInstance.defId]; + // constraint + if (!allConstraintsPass(subjectConfig.constraints, subjectRange, otherEventStore, state.businessHours, calendar)) { + return false; + } + // overlap + var overlapFunc = calendar.opt('eventOverlap'); + if (typeof overlapFunc !== 'function') { + overlapFunc = null; + } + for (var otherInstanceId in otherInstances) { + var otherInstance = otherInstances[otherInstanceId]; + // intersect! evaluate + if (rangesIntersect(subjectRange, otherInstance.range)) { + var otherOverlap = otherConfigs[otherInstance.defId].overlap; + // consider the other event's overlap. only do this if the subject event is a "real" event + if (otherOverlap === false && interaction.isEvent) { + return false; + } + if (subjectConfig.overlap === false) { + return false; + } + if (overlapFunc && !overlapFunc(new EventApi(calendar, otherDefs[otherInstance.defId], otherInstance), // still event + new EventApi(calendar, subjectDef, subjectInstance) // moving event + )) { + return false; + } + } + } + // allow (a function) + var calendarEventStore = calendar.state.eventStore; // need global-to-calendar, not local to component (splittable)state + for (var _i = 0, _a = subjectConfig.allows; _i < _a.length; _i++) { + var subjectAllow = _a[_i]; + var subjectDateSpan = __assign({}, dateSpanMeta, { range: subjectInstance.range, allDay: subjectDef.allDay }); + var origDef = calendarEventStore.defs[subjectDef.defId]; + var origInstance = calendarEventStore.instances[subjectInstanceId]; + var eventApi = void 0; + if (origDef) { // was previously in the calendar + eventApi = new EventApi(calendar, origDef, origInstance); + } + else { // was an external event + eventApi = new EventApi(calendar, subjectDef); // no instance, because had no dates + } + if (!subjectAllow(calendar.buildDateSpanApi(subjectDateSpan), eventApi)) { + return false; + } + } + } + return true; + } + // Date Selection Validation + // ------------------------------------------------------------------------------------------------------------------------ + function isDateSelectionPropsValid(state, calendar, dateSpanMeta, filterConfig) { + var relevantEventStore = state.eventStore; + var relevantDefs = relevantEventStore.defs; + var relevantInstances = relevantEventStore.instances; + var selection = state.dateSelection; + var selectionRange = selection.range; + var selectionConfig = calendar.selectionConfig; + if (filterConfig) { + selectionConfig = filterConfig(selectionConfig); + } + // constraint + if (!allConstraintsPass(selectionConfig.constraints, selectionRange, relevantEventStore, state.businessHours, calendar)) { + return false; + } + // overlap + var overlapFunc = calendar.opt('selectOverlap'); + if (typeof overlapFunc !== 'function') { + overlapFunc = null; + } + for (var relevantInstanceId in relevantInstances) { + var relevantInstance = relevantInstances[relevantInstanceId]; + // intersect! evaluate + if (rangesIntersect(selectionRange, relevantInstance.range)) { + if (selectionConfig.overlap === false) { + return false; + } + if (overlapFunc && !overlapFunc(new EventApi(calendar, relevantDefs[relevantInstance.defId], relevantInstance))) { + return false; + } + } + } + // allow (a function) + for (var _i = 0, _a = selectionConfig.allows; _i < _a.length; _i++) { + var selectionAllow = _a[_i]; + var fullDateSpan = __assign({}, dateSpanMeta, selection); + if (!selectionAllow(calendar.buildDateSpanApi(fullDateSpan), null)) { + return false; + } + } + return true; + } + // Constraint Utils + // ------------------------------------------------------------------------------------------------------------------------ + function allConstraintsPass(constraints, subjectRange, otherEventStore, businessHoursUnexpanded, calendar) { + for (var _i = 0, constraints_1 = constraints; _i < constraints_1.length; _i++) { + var constraint = constraints_1[_i]; + if (!anyRangesContainRange(constraintToRanges(constraint, subjectRange, otherEventStore, businessHoursUnexpanded, calendar), subjectRange)) { + return false; + } + } + return true; + } + function constraintToRanges(constraint, subjectRange, // for expanding a recurring constraint, or expanding business hours + otherEventStore, // for if constraint is an even group ID + businessHoursUnexpanded, // for if constraint is 'businessHours' + calendar // for expanding businesshours + ) { + if (constraint === 'businessHours') { + return eventStoreToRanges(expandRecurring(businessHoursUnexpanded, subjectRange, calendar)); + } + else if (typeof constraint === 'string') { // an group ID + return eventStoreToRanges(filterEventStoreDefs(otherEventStore, function (eventDef) { + return eventDef.groupId === constraint; + })); + } + else if (typeof constraint === 'object' && constraint) { // non-null object + return eventStoreToRanges(expandRecurring(constraint, subjectRange, calendar)); + } + return []; // if it's false + } + // TODO: move to event-store file? + function eventStoreToRanges(eventStore) { + var instances = eventStore.instances; + var ranges = []; + for (var instanceId in instances) { + ranges.push(instances[instanceId].range); + } + return ranges; + } + // TODO: move to geom file? + function anyRangesContainRange(outerRanges, innerRange) { + for (var _i = 0, outerRanges_1 = outerRanges; _i < outerRanges_1.length; _i++) { + var outerRange = outerRanges_1[_i]; + if (rangeContainsRange(outerRange, innerRange)) { + return true; + } + } + return false; + } + // Parsing + // ------------------------------------------------------------------------------------------------------------------------ + function normalizeConstraint(input, calendar) { + if (Array.isArray(input)) { + return parseEvents(input, '', calendar, true); // allowOpenRange=true + } + else if (typeof input === 'object' && input) { // non-null object + return parseEvents([input], '', calendar, true); // allowOpenRange=true + } + else if (input != null) { + return String(input); + } + else { + return null; + } + } + + function htmlEscape(s) { + return (s + '').replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/'/g, ''') + .replace(/"/g, '"') + .replace(/\n/g, '<br />'); + } + // 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 = []; + for (var name_1 in cssProps) { + var val = cssProps[name_1]; + if (val != null && val !== '') { + statements.push(name_1 + ':' + val); + } + } + return statements.join(';'); + } + // Given an object hash of HTML attribute names to values, + // generates a string that can be injected between < > in HTML + function attrsToStr(attrs) { + var parts = []; + for (var name_2 in attrs) { + var val = attrs[name_2]; + if (val != null) { + parts.push(name_2 + '="' + htmlEscape(val) + '"'); + } + } + return parts.join(' '); + } + function parseClassName(raw) { + if (Array.isArray(raw)) { + return raw; + } + else if (typeof raw === 'string') { + return raw.split(/\s+/); + } + else { + return []; + } + } + + var UNSCOPED_EVENT_UI_PROPS = { + editable: Boolean, + startEditable: Boolean, + durationEditable: Boolean, + constraint: null, + overlap: null, + allow: null, + className: parseClassName, + classNames: parseClassName, + color: String, + backgroundColor: String, + borderColor: String, + textColor: String + }; + function processUnscopedUiProps(rawProps, calendar, leftovers) { + var props = refineProps(rawProps, UNSCOPED_EVENT_UI_PROPS, {}, leftovers); + var constraint = normalizeConstraint(props.constraint, calendar); + return { + startEditable: props.startEditable != null ? props.startEditable : props.editable, + durationEditable: props.durationEditable != null ? props.durationEditable : props.editable, + constraints: constraint != null ? [constraint] : [], + overlap: props.overlap, + allows: props.allow != null ? [props.allow] : [], + backgroundColor: props.backgroundColor || props.color, + borderColor: props.borderColor || props.color, + textColor: props.textColor, + classNames: props.classNames.concat(props.className) + }; + } + function processScopedUiProps(prefix, rawScoped, calendar, leftovers) { + var rawUnscoped = {}; + var wasFound = {}; + for (var key in UNSCOPED_EVENT_UI_PROPS) { + var scopedKey = prefix + capitaliseFirstLetter(key); + rawUnscoped[key] = rawScoped[scopedKey]; + wasFound[scopedKey] = true; + } + if (prefix === 'event') { + rawUnscoped.editable = rawScoped.editable; // special case. there is no 'eventEditable', just 'editable' + } + if (leftovers) { + for (var key in rawScoped) { + if (!wasFound[key]) { + leftovers[key] = rawScoped[key]; + } + } + } + return processUnscopedUiProps(rawUnscoped, calendar); + } + var EMPTY_EVENT_UI = { + startEditable: null, + durationEditable: null, + constraints: [], + overlap: null, + allows: [], + backgroundColor: '', + borderColor: '', + textColor: '', + classNames: [] + }; + // prevent against problems with <2 args! + function combineEventUis(uis) { + return uis.reduce(combineTwoEventUis, EMPTY_EVENT_UI); + } + function combineTwoEventUis(item0, item1) { + return { + startEditable: item1.startEditable != null ? item1.startEditable : item0.startEditable, + durationEditable: item1.durationEditable != null ? item1.durationEditable : item0.durationEditable, + constraints: item0.constraints.concat(item1.constraints), + overlap: typeof item1.overlap === 'boolean' ? item1.overlap : item0.overlap, + allows: item0.allows.concat(item1.allows), + backgroundColor: item1.backgroundColor || item0.backgroundColor, + borderColor: item1.borderColor || item0.borderColor, + textColor: item1.textColor || item0.textColor, + classNames: item0.classNames.concat(item1.classNames) + }; + } + + var NON_DATE_PROPS = { + id: String, + groupId: String, + title: String, + url: String, + rendering: String, + extendedProps: null + }; + var DATE_PROPS = { + start: null, + date: null, + end: null, + allDay: null + }; + var uid = 0; + function parseEvent(raw, sourceId, calendar, allowOpenRange) { + var allDayDefault = computeIsAllDayDefault(sourceId, calendar); + var leftovers0 = {}; + var recurringRes = parseRecurring(raw, // raw, but with single-event stuff stripped out + allDayDefault, calendar.dateEnv, calendar.pluginSystem.hooks.recurringTypes, leftovers0 // will populate with non-recurring props + ); + if (recurringRes) { + var def = parseEventDef(leftovers0, sourceId, recurringRes.allDay, Boolean(recurringRes.duration), calendar); + def.recurringDef = { + typeId: recurringRes.typeId, + typeData: recurringRes.typeData, + duration: recurringRes.duration + }; + return { def: def, instance: null }; + } + else { + var leftovers1 = {}; + var singleRes = parseSingle(raw, allDayDefault, calendar, leftovers1, allowOpenRange); + if (singleRes) { + var def = parseEventDef(leftovers1, sourceId, singleRes.allDay, singleRes.hasEnd, calendar); + var instance = createEventInstance(def.defId, singleRes.range, singleRes.forcedStartTzo, singleRes.forcedEndTzo); + return { def: def, instance: instance }; + } + } + return null; + } + /* + Will NOT populate extendedProps with the leftover properties. + Will NOT populate date-related props. + The EventNonDateInput has been normalized (id => publicId, etc). + */ + function parseEventDef(raw, sourceId, allDay, hasEnd, calendar) { + var leftovers = {}; + var def = pluckNonDateProps(raw, calendar, leftovers); + def.defId = String(uid++); + def.sourceId = sourceId; + def.allDay = allDay; + def.hasEnd = hasEnd; + for (var _i = 0, _a = calendar.pluginSystem.hooks.eventDefParsers; _i < _a.length; _i++) { + var eventDefParser = _a[_i]; + var newLeftovers = {}; + eventDefParser(def, leftovers, newLeftovers); + leftovers = newLeftovers; + } + def.extendedProps = __assign(leftovers, def.extendedProps || {}); + // help out EventApi from having user modify props + Object.freeze(def.ui.classNames); + Object.freeze(def.extendedProps); + return def; + } + function createEventInstance(defId, range, forcedStartTzo, forcedEndTzo) { + return { + instanceId: String(uid++), + defId: defId, + range: range, + forcedStartTzo: forcedStartTzo == null ? null : forcedStartTzo, + forcedEndTzo: forcedEndTzo == null ? null : forcedEndTzo + }; + } + function parseSingle(raw, allDayDefault, calendar, leftovers, allowOpenRange) { + var props = pluckDateProps(raw, leftovers); + var allDay = props.allDay; + var startMeta; + var startMarker = null; + var hasEnd = false; + var endMeta; + var endMarker = null; + startMeta = calendar.dateEnv.createMarkerMeta(props.start); + if (startMeta) { + startMarker = startMeta.marker; + } + else if (!allowOpenRange) { + return null; + } + if (props.end != null) { + endMeta = calendar.dateEnv.createMarkerMeta(props.end); + } + if (allDay == null) { + if (allDayDefault != null) { + allDay = allDayDefault; + } + else { + // fall back to the date props LAST + allDay = (!startMeta || startMeta.isTimeUnspecified) && + (!endMeta || endMeta.isTimeUnspecified); + } + } + if (allDay && startMarker) { + startMarker = startOfDay(startMarker); + } + if (endMeta) { + endMarker = endMeta.marker; + if (allDay) { + endMarker = startOfDay(endMarker); + } + if (startMarker && endMarker <= startMarker) { + endMarker = null; + } + } + if (endMarker) { + hasEnd = true; + } + else if (!allowOpenRange) { + hasEnd = calendar.opt('forceEventDuration') || false; + endMarker = calendar.dateEnv.add(startMarker, allDay ? + calendar.defaultAllDayEventDuration : + calendar.defaultTimedEventDuration); + } + return { + allDay: allDay, + hasEnd: hasEnd, + range: { start: startMarker, end: endMarker }, + forcedStartTzo: startMeta ? startMeta.forcedTzo : null, + forcedEndTzo: endMeta ? endMeta.forcedTzo : null + }; + } + function pluckDateProps(raw, leftovers) { + var props = refineProps(raw, DATE_PROPS, {}, leftovers); + props.start = (props.start !== null) ? props.start : props.date; + delete props.date; + return props; + } + function pluckNonDateProps(raw, calendar, leftovers) { + var preLeftovers = {}; + var props = refineProps(raw, NON_DATE_PROPS, {}, preLeftovers); + var ui = processUnscopedUiProps(preLeftovers, calendar, leftovers); + props.publicId = props.id; + delete props.id; + props.ui = ui; + return props; + } + function computeIsAllDayDefault(sourceId, calendar) { + var res = null; + if (sourceId) { + var source = calendar.state.eventSources[sourceId]; + res = source.allDayDefault; + } + if (res == null) { + res = calendar.opt('allDayDefault'); + } + return res; + } + + var DEF_DEFAULTS = { + startTime: '09:00', + endTime: '17:00', + daysOfWeek: [1, 2, 3, 4, 5], + rendering: 'inverse-background', + classNames: 'fc-nonbusiness', + groupId: '_businessHours' // so multiple defs get grouped + }; + /* + TODO: pass around as EventDefHash!!! + */ + function parseBusinessHours(input, calendar) { + return parseEvents(refineInputs(input), '', calendar); + } + function refineInputs(input) { + var rawDefs; + if (input === true) { + rawDefs = [{}]; // will get DEF_DEFAULTS verbatim + } + else if (Array.isArray(input)) { + // if specifying an array, every sub-definition NEEDS a day-of-week + rawDefs = input.filter(function (rawDef) { + return rawDef.daysOfWeek; + }); + } + else if (typeof input === 'object' && input) { // non-null object + rawDefs = [input]; + } + else { // is probably false + rawDefs = []; + } + rawDefs = rawDefs.map(function (rawDef) { + return __assign({}, DEF_DEFAULTS, rawDef); + }); + return rawDefs; + } + + function memoizeRendering(renderFunc, unrenderFunc, dependencies) { + if (dependencies === void 0) { dependencies = []; } + var dependents = []; + var thisContext; + var prevArgs; + function unrender() { + if (prevArgs) { + for (var _i = 0, dependents_1 = dependents; _i < dependents_1.length; _i++) { + var dependent = dependents_1[_i]; + dependent.unrender(); + } + if (unrenderFunc) { + unrenderFunc.apply(thisContext, prevArgs); + } + prevArgs = null; + } + } + function res() { + if (!prevArgs || !isArraysEqual(prevArgs, arguments)) { + unrender(); + thisContext = this; + prevArgs = arguments; + renderFunc.apply(this, arguments); + } + } + res.dependents = dependents; + res.unrender = unrender; + for (var _i = 0, dependencies_1 = dependencies; _i < dependencies_1.length; _i++) { + var dependency = dependencies_1[_i]; + dependency.dependents.push(res); + } + return res; + } + + var EMPTY_EVENT_STORE = createEmptyEventStore(); // for purecomponents. TODO: keep elsewhere + var Splitter = /** @class */ (function () { + function Splitter() { + this.getKeysForEventDefs = memoize(this._getKeysForEventDefs); + this.splitDateSelection = memoize(this._splitDateSpan); + this.splitEventStore = memoize(this._splitEventStore); + this.splitIndividualUi = memoize(this._splitIndividualUi); + this.splitEventDrag = memoize(this._splitInteraction); + this.splitEventResize = memoize(this._splitInteraction); + this.eventUiBuilders = {}; // TODO: typescript protection + } + Splitter.prototype.splitProps = function (props) { + var _this = this; + var keyInfos = this.getKeyInfo(props); + var defKeys = this.getKeysForEventDefs(props.eventStore); + var dateSelections = this.splitDateSelection(props.dateSelection); + var individualUi = this.splitIndividualUi(props.eventUiBases, defKeys); // the individual *bases* + var eventStores = this.splitEventStore(props.eventStore, defKeys); + var eventDrags = this.splitEventDrag(props.eventDrag); + var eventResizes = this.splitEventResize(props.eventResize); + var splitProps = {}; + this.eventUiBuilders = mapHash(keyInfos, function (info, key) { + return _this.eventUiBuilders[key] || memoize(buildEventUiForKey); + }); + for (var key in keyInfos) { + var keyInfo = keyInfos[key]; + var eventStore = eventStores[key] || EMPTY_EVENT_STORE; + var buildEventUi = this.eventUiBuilders[key]; + splitProps[key] = { + businessHours: keyInfo.businessHours || props.businessHours, + dateSelection: dateSelections[key] || null, + eventStore: eventStore, + eventUiBases: buildEventUi(props.eventUiBases[''], keyInfo.ui, individualUi[key]), + eventSelection: eventStore.instances[props.eventSelection] ? props.eventSelection : '', + eventDrag: eventDrags[key] || null, + eventResize: eventResizes[key] || null + }; + } + return splitProps; + }; + Splitter.prototype._splitDateSpan = function (dateSpan) { + var dateSpans = {}; + if (dateSpan) { + var keys = this.getKeysForDateSpan(dateSpan); + for (var _i = 0, keys_1 = keys; _i < keys_1.length; _i++) { + var key = keys_1[_i]; + dateSpans[key] = dateSpan; + } + } + return dateSpans; + }; + Splitter.prototype._getKeysForEventDefs = function (eventStore) { + var _this = this; + return mapHash(eventStore.defs, function (eventDef) { + return _this.getKeysForEventDef(eventDef); + }); + }; + Splitter.prototype._splitEventStore = function (eventStore, defKeys) { + var defs = eventStore.defs, instances = eventStore.instances; + var splitStores = {}; + for (var defId in defs) { + for (var _i = 0, _a = defKeys[defId]; _i < _a.length; _i++) { + var key = _a[_i]; + if (!splitStores[key]) { + splitStores[key] = createEmptyEventStore(); + } + splitStores[key].defs[defId] = defs[defId]; + } + } + for (var instanceId in instances) { + var instance = instances[instanceId]; + for (var _b = 0, _c = defKeys[instance.defId]; _b < _c.length; _b++) { + var key = _c[_b]; + if (splitStores[key]) { // must have already been created + splitStores[key].instances[instanceId] = instance; + } + } + } + return splitStores; + }; + Splitter.prototype._splitIndividualUi = function (eventUiBases, defKeys) { + var splitHashes = {}; + for (var defId in eventUiBases) { + if (defId) { // not the '' key + for (var _i = 0, _a = defKeys[defId]; _i < _a.length; _i++) { + var key = _a[_i]; + if (!splitHashes[key]) { + splitHashes[key] = {}; + } + splitHashes[key][defId] = eventUiBases[defId]; + } + } + } + return splitHashes; + }; + Splitter.prototype._splitInteraction = function (interaction) { + var splitStates = {}; + if (interaction) { + var affectedStores_1 = this._splitEventStore(interaction.affectedEvents, this._getKeysForEventDefs(interaction.affectedEvents) // can't use cached. might be events from other calendar + ); + // can't rely on defKeys because event data is mutated + var mutatedKeysByDefId = this._getKeysForEventDefs(interaction.mutatedEvents); + var mutatedStores_1 = this._splitEventStore(interaction.mutatedEvents, mutatedKeysByDefId); + var populate = function (key) { + if (!splitStates[key]) { + splitStates[key] = { + affectedEvents: affectedStores_1[key] || EMPTY_EVENT_STORE, + mutatedEvents: mutatedStores_1[key] || EMPTY_EVENT_STORE, + isEvent: interaction.isEvent, + origSeg: interaction.origSeg + }; + } + }; + for (var key in affectedStores_1) { + populate(key); + } + for (var key in mutatedStores_1) { + populate(key); + } + } + return splitStates; + }; + return Splitter; + }()); + function buildEventUiForKey(allUi, eventUiForKey, individualUi) { + var baseParts = []; + if (allUi) { + baseParts.push(allUi); + } + if (eventUiForKey) { + baseParts.push(eventUiForKey); + } + var stuff = { + '': combineEventUis(baseParts) + }; + if (individualUi) { + __assign(stuff, individualUi); + } + return stuff; + } + + // Generates HTML for an anchor to another view into the calendar. + // Will either generate an <a> tag or a non-clickable <span> tag, depending on enabled settings. + // `gotoOptions` can either be a DateMarker, or an object with the form: + // { date, type, forceOff } + // `type` is a view-type like "day" or "week". default value is "day". + // `attrs` and `innerHtml` are use to generate the rest of the HTML tag. + function buildGotoAnchorHtml(allOptions, dateEnv, gotoOptions, attrs, innerHtml) { + var date; + var type; + var forceOff; + var finalOptions; + if (gotoOptions instanceof Date) { + date = gotoOptions; // a single date-like input + } + else { + date = gotoOptions.date; + type = gotoOptions.type; + forceOff = gotoOptions.forceOff; + } + finalOptions = { + date: dateEnv.formatIso(date, { omitTime: true }), + type: type || 'day' + }; + if (typeof attrs === 'string') { + innerHtml = attrs; + attrs = null; + } + attrs = attrs ? ' ' + attrsToStr(attrs) : ''; // will have a leading space + innerHtml = innerHtml || ''; + if (!forceOff && allOptions.navLinks) { + return '<a' + attrs + + ' data-goto="' + htmlEscape(JSON.stringify(finalOptions)) + '">' + + innerHtml + + '</a>'; + } + else { + return '<span' + attrs + '>' + + innerHtml + + '</span>'; + } + } + function getAllDayHtml(allOptions) { + return allOptions.allDayHtml || htmlEscape(allOptions.allDayText); + } + // Computes HTML classNames for a single-day element + function getDayClasses(date, dateProfile, context, noThemeHighlight) { + var calendar = context.calendar, options = context.options, theme = context.theme, dateEnv = context.dateEnv; + var classes = []; + var todayStart; + var todayEnd; + if (!rangeContainsMarker(dateProfile.activeRange, date)) { + classes.push('fc-disabled-day'); + } + else { + classes.push('fc-' + DAY_IDS[date.getUTCDay()]); + if (options.monthMode && + dateEnv.getMonth(date) !== dateEnv.getMonth(dateProfile.currentRange.start)) { + classes.push('fc-other-month'); + } + todayStart = startOfDay(calendar.getNow()); + todayEnd = addDays(todayStart, 1); + if (date < todayStart) { + classes.push('fc-past'); + } + else if (date >= todayEnd) { + classes.push('fc-future'); + } + else { + classes.push('fc-today'); + if (noThemeHighlight !== true) { + classes.push(theme.getClass('today')); + } + } + } + return classes; + } + + // given a function that resolves a result asynchronously. + // the function can either call passed-in success and failure callbacks, + // or it can return a promise. + // if you need to pass additional params to func, bind them first. + function unpromisify(func, success, failure) { + // guard against success/failure callbacks being called more than once + // and guard against a promise AND callback being used together. + var isResolved = false; + var wrappedSuccess = function () { + if (!isResolved) { + isResolved = true; + success.apply(this, arguments); + } + }; + var wrappedFailure = function () { + if (!isResolved) { + isResolved = true; + if (failure) { + failure.apply(this, arguments); + } + } + }; + var res = func(wrappedSuccess, wrappedFailure); + if (res && typeof res.then === 'function') { + res.then(wrappedSuccess, wrappedFailure); + } + } + + var Mixin = /** @class */ (function () { + function Mixin() { + } + // mix into a CLASS + Mixin.mixInto = function (destClass) { + this.mixIntoObj(destClass.prototype); + }; + // mix into ANY object + Mixin.mixIntoObj = function (destObj) { + var _this = this; + Object.getOwnPropertyNames(this.prototype).forEach(function (name) { + if (!destObj[name]) { // if destination doesn't already define it + destObj[name] = _this.prototype[name]; + } + }); + }; + /* + will override existing methods + TODO: remove! not used anymore + */ + Mixin.mixOver = function (destClass) { + var _this = this; + Object.getOwnPropertyNames(this.prototype).forEach(function (name) { + destClass.prototype[name] = _this.prototype[name]; + }); + }; + return Mixin; + }()); + + /* + USAGE: + import { default as EmitterMixin, EmitterInterface } from './EmitterMixin' + in class: + on: EmitterInterface['on'] + one: EmitterInterface['one'] + off: EmitterInterface['off'] + trigger: EmitterInterface['trigger'] + triggerWith: EmitterInterface['triggerWith'] + hasHandlers: EmitterInterface['hasHandlers'] + after class: + EmitterMixin.mixInto(TheClass) + */ + var EmitterMixin = /** @class */ (function (_super) { + __extends(EmitterMixin, _super); + function EmitterMixin() { + return _super !== null && _super.apply(this, arguments) || this; + } + EmitterMixin.prototype.on = function (type, handler) { + addToHash(this._handlers || (this._handlers = {}), type, handler); + return this; // for chaining + }; + // todo: add comments + EmitterMixin.prototype.one = function (type, handler) { + addToHash(this._oneHandlers || (this._oneHandlers = {}), type, handler); + return this; // for chaining + }; + EmitterMixin.prototype.off = function (type, handler) { + if (this._handlers) { + removeFromHash(this._handlers, type, handler); + } + if (this._oneHandlers) { + removeFromHash(this._oneHandlers, type, handler); + } + return this; // for chaining + }; + EmitterMixin.prototype.trigger = function (type) { + var args = []; + for (var _i = 1; _i < arguments.length; _i++) { + args[_i - 1] = arguments[_i]; + } + this.triggerWith(type, this, args); + return this; // for chaining + }; + EmitterMixin.prototype.triggerWith = function (type, context, args) { + if (this._handlers) { + applyAll(this._handlers[type], context, args); + } + if (this._oneHandlers) { + applyAll(this._oneHandlers[type], context, args); + delete this._oneHandlers[type]; // will never fire again + } + return this; // for chaining + }; + EmitterMixin.prototype.hasHandlers = function (type) { + return (this._handlers && this._handlers[type] && this._handlers[type].length) || + (this._oneHandlers && this._oneHandlers[type] && this._oneHandlers[type].length); + }; + return EmitterMixin; + }(Mixin)); + function addToHash(hash, type, handler) { + (hash[type] || (hash[type] = [])) + .push(handler); + } + function removeFromHash(hash, type, handler) { + if (handler) { + if (hash[type]) { + hash[type] = hash[type].filter(function (func) { + return func !== handler; + }); + } + } + else { + delete hash[type]; // remove all handler funcs for this type + } + } + + /* + Records offset information for a set of elements, relative to an origin element. + Can record the left/right OR the top/bottom OR both. + Provides methods for querying the cache by position. + */ + var PositionCache = /** @class */ (function () { + function PositionCache(originEl, els, isHorizontal, isVertical) { + this.originEl = originEl; + this.els = els; + this.isHorizontal = isHorizontal; + this.isVertical = isVertical; + } + // Queries the els for coordinates and stores them. + // Call this method before using and of the get* methods below. + PositionCache.prototype.build = function () { + var originEl = this.originEl; + var originClientRect = this.originClientRect = + originEl.getBoundingClientRect(); // relative to viewport top-left + if (this.isHorizontal) { + this.buildElHorizontals(originClientRect.left); + } + if (this.isVertical) { + this.buildElVerticals(originClientRect.top); + } + }; + // Populates the left/right internal coordinate arrays + PositionCache.prototype.buildElHorizontals = function (originClientLeft) { + var lefts = []; + var rights = []; + for (var _i = 0, _a = this.els; _i < _a.length; _i++) { + var el = _a[_i]; + var rect = el.getBoundingClientRect(); + lefts.push(rect.left - originClientLeft); + rights.push(rect.right - originClientLeft); + } + this.lefts = lefts; + this.rights = rights; + }; + // Populates the top/bottom internal coordinate arrays + PositionCache.prototype.buildElVerticals = function (originClientTop) { + var tops = []; + var bottoms = []; + for (var _i = 0, _a = this.els; _i < _a.length; _i++) { + var el = _a[_i]; + var rect = el.getBoundingClientRect(); + tops.push(rect.top - originClientTop); + bottoms.push(rect.bottom - originClientTop); + } + 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, returns undefined. + PositionCache.prototype.leftToIndex = function (leftPosition) { + var lefts = this.lefts; + var rights = this.rights; + var len = lefts.length; + var i; + for (i = 0; i < len; i++) { + if (leftPosition >= lefts[i] && leftPosition < 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, returns undefined. + PositionCache.prototype.topToIndex = function (topPosition) { + var tops = this.tops; + var bottoms = this.bottoms; + var len = tops.length; + var i; + for (i = 0; i < len; i++) { + if (topPosition >= tops[i] && topPosition < bottoms[i]) { + return i; + } + } + }; + // Gets the width of the element at the given index + PositionCache.prototype.getWidth = function (leftIndex) { + return this.rights[leftIndex] - this.lefts[leftIndex]; + }; + // Gets the height of the element at the given index + PositionCache.prototype.getHeight = function (topIndex) { + return this.bottoms[topIndex] - this.tops[topIndex]; + }; + return PositionCache; + }()); + + /* + An object for getting/setting scroll-related information for an element. + Internally, this is done very differently for window versus DOM element, + so this object serves as a common interface. + */ + var ScrollController = /** @class */ (function () { + function ScrollController() { + } + ScrollController.prototype.getMaxScrollTop = function () { + return this.getScrollHeight() - this.getClientHeight(); + }; + ScrollController.prototype.getMaxScrollLeft = function () { + return this.getScrollWidth() - this.getClientWidth(); + }; + ScrollController.prototype.canScrollVertically = function () { + return this.getMaxScrollTop() > 0; + }; + ScrollController.prototype.canScrollHorizontally = function () { + return this.getMaxScrollLeft() > 0; + }; + ScrollController.prototype.canScrollUp = function () { + return this.getScrollTop() > 0; + }; + ScrollController.prototype.canScrollDown = function () { + return this.getScrollTop() < this.getMaxScrollTop(); + }; + ScrollController.prototype.canScrollLeft = function () { + return this.getScrollLeft() > 0; + }; + ScrollController.prototype.canScrollRight = function () { + return this.getScrollLeft() < this.getMaxScrollLeft(); + }; + return ScrollController; + }()); + var ElementScrollController = /** @class */ (function (_super) { + __extends(ElementScrollController, _super); + function ElementScrollController(el) { + var _this = _super.call(this) || this; + _this.el = el; + return _this; + } + ElementScrollController.prototype.getScrollTop = function () { + return this.el.scrollTop; + }; + ElementScrollController.prototype.getScrollLeft = function () { + return this.el.scrollLeft; + }; + ElementScrollController.prototype.setScrollTop = function (top) { + this.el.scrollTop = top; + }; + ElementScrollController.prototype.setScrollLeft = function (left) { + this.el.scrollLeft = left; + }; + ElementScrollController.prototype.getScrollWidth = function () { + return this.el.scrollWidth; + }; + ElementScrollController.prototype.getScrollHeight = function () { + return this.el.scrollHeight; + }; + ElementScrollController.prototype.getClientHeight = function () { + return this.el.clientHeight; + }; + ElementScrollController.prototype.getClientWidth = function () { + return this.el.clientWidth; + }; + return ElementScrollController; + }(ScrollController)); + var WindowScrollController = /** @class */ (function (_super) { + __extends(WindowScrollController, _super); + function WindowScrollController() { + return _super !== null && _super.apply(this, arguments) || this; + } + WindowScrollController.prototype.getScrollTop = function () { + return window.pageYOffset; + }; + WindowScrollController.prototype.getScrollLeft = function () { + return window.pageXOffset; + }; + WindowScrollController.prototype.setScrollTop = function (n) { + window.scroll(window.pageXOffset, n); + }; + WindowScrollController.prototype.setScrollLeft = function (n) { + window.scroll(n, window.pageYOffset); + }; + WindowScrollController.prototype.getScrollWidth = function () { + return document.documentElement.scrollWidth; + }; + WindowScrollController.prototype.getScrollHeight = function () { + return document.documentElement.scrollHeight; + }; + WindowScrollController.prototype.getClientHeight = function () { + return document.documentElement.clientHeight; + }; + WindowScrollController.prototype.getClientWidth = function () { + return document.documentElement.clientWidth; + }; + return WindowScrollController; + }(ScrollController)); + + /* + Embodies a div that has potential scrollbars + */ + var ScrollComponent = /** @class */ (function (_super) { + __extends(ScrollComponent, _super); + function ScrollComponent(overflowX, overflowY) { + var _this = _super.call(this, createElement('div', { + className: 'fc-scroller' + })) || this; + _this.overflowX = overflowX; + _this.overflowY = overflowY; + _this.applyOverflow(); + return _this; + } + // sets to natural height, unlocks overflow + ScrollComponent.prototype.clear = function () { + this.setHeight('auto'); + this.applyOverflow(); + }; + ScrollComponent.prototype.destroy = function () { + removeElement(this.el); + }; + // Overflow + // ----------------------------------------------------------------------------------------------------------------- + ScrollComponent.prototype.applyOverflow = function () { + applyStyle(this.el, { + overflowX: this.overflowX, + overflowY: 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. + ScrollComponent.prototype.lockOverflow = function (scrollbarWidths) { + var overflowX = this.overflowX; + var overflowY = this.overflowY; + scrollbarWidths = scrollbarWidths || this.getScrollbarWidths(); + if (overflowX === 'auto') { + overflowX = (scrollbarWidths.bottom || // horizontal scrollbars? + this.canScrollHorizontally() // OR scrolling pane with massless scrollbars? + ) ? 'scroll' : 'hidden'; + } + if (overflowY === 'auto') { + overflowY = (scrollbarWidths.left || scrollbarWidths.right || // horizontal scrollbars? + this.canScrollVertically() // OR scrolling pane with massless scrollbars? + ) ? 'scroll' : 'hidden'; + } + applyStyle(this.el, { overflowX: overflowX, overflowY: overflowY }); + }; + ScrollComponent.prototype.setHeight = function (height) { + applyStyleProp(this.el, 'height', height); + }; + ScrollComponent.prototype.getScrollbarWidths = function () { + var edges = computeEdges(this.el); + return { + left: edges.scrollbarLeft, + right: edges.scrollbarRight, + bottom: edges.scrollbarBottom + }; + }; + return ScrollComponent; + }(ElementScrollController)); + + var Theme = /** @class */ (function () { + function Theme(calendarOptions) { + this.calendarOptions = calendarOptions; + this.processIconOverride(); + } + Theme.prototype.processIconOverride = function () { + if (this.iconOverrideOption) { + this.setIconOverride(this.calendarOptions[this.iconOverrideOption]); + } + }; + Theme.prototype.setIconOverride = function (iconOverrideHash) { + var iconClassesCopy; + var buttonName; + if (typeof iconOverrideHash === 'object' && iconOverrideHash) { // non-null object + iconClassesCopy = __assign({}, this.iconClasses); + for (buttonName in iconOverrideHash) { + iconClassesCopy[buttonName] = this.applyIconOverridePrefix(iconOverrideHash[buttonName]); + } + this.iconClasses = iconClassesCopy; + } + else if (iconOverrideHash === false) { + this.iconClasses = {}; + } + }; + Theme.prototype.applyIconOverridePrefix = function (className) { + var prefix = this.iconOverridePrefix; + if (prefix && className.indexOf(prefix) !== 0) { // if not already present + className = prefix + className; + } + return className; + }; + Theme.prototype.getClass = function (key) { + return this.classes[key] || ''; + }; + Theme.prototype.getIconClass = function (buttonName) { + var className = this.iconClasses[buttonName]; + if (className) { + return this.baseIconClass + ' ' + className; + } + return ''; + }; + Theme.prototype.getCustomButtonIconClass = function (customButtonProps) { + var className; + if (this.iconOverrideCustomButtonOption) { + className = customButtonProps[this.iconOverrideCustomButtonOption]; + if (className) { + return this.baseIconClass + ' ' + this.applyIconOverridePrefix(className); + } + } + return ''; + }; + return Theme; + }()); + Theme.prototype.classes = {}; + Theme.prototype.iconClasses = {}; + Theme.prototype.baseIconClass = ''; + Theme.prototype.iconOverridePrefix = ''; + + var guid = 0; + var ComponentContext = /** @class */ (function () { + function ComponentContext(calendar, theme, dateEnv, options, view) { + this.calendar = calendar; + this.theme = theme; + this.dateEnv = dateEnv; + this.options = options; + this.view = view; + this.isRtl = options.dir === 'rtl'; + this.eventOrderSpecs = parseFieldSpecs(options.eventOrder); + this.nextDayThreshold = createDuration(options.nextDayThreshold); + } + ComponentContext.prototype.extend = function (options, view) { + return new ComponentContext(this.calendar, this.theme, this.dateEnv, options || this.options, view || this.view); + }; + return ComponentContext; + }()); + var Component = /** @class */ (function () { + function Component() { + this.uid = String(guid++); + } + Component.addEqualityFuncs = function (newFuncs) { + this.prototype.equalityFuncs = __assign({}, this.prototype.equalityFuncs, newFuncs); + }; + Component.prototype.receiveProps = function (props, context) { + var oldContext = this.context; + this.context = context; + if (!oldContext) { + this.firstContext(context); + } + var _a = recycleProps(this.props || {}, props, this.equalityFuncs), anyChanges = _a.anyChanges, comboProps = _a.comboProps; + this.props = comboProps; + if (anyChanges) { + if (oldContext) { + this.beforeUpdate(); + } + this.render(comboProps, context); + if (oldContext) { + this.afterUpdate(); + } + } + }; + Component.prototype.render = function (props, context) { + }; + Component.prototype.firstContext = function (context) { + }; + Component.prototype.beforeUpdate = function () { + }; + Component.prototype.afterUpdate = function () { + }; + // after destroy is called, this component won't ever be used again + Component.prototype.destroy = function () { + }; + return Component; + }()); + Component.prototype.equalityFuncs = {}; + /* + Reuses old values when equal. If anything is unequal, returns newProps as-is. + Great for PureComponent, but won't be feasible with React, so just eliminate and use React's DOM diffing. + */ + function recycleProps(oldProps, newProps, equalityFuncs) { + var comboProps = {}; // some old, some new + var anyChanges = false; + for (var key in newProps) { + if (key in oldProps && (oldProps[key] === newProps[key] || + (equalityFuncs[key] && equalityFuncs[key](oldProps[key], newProps[key])))) { + // equal to old? use old prop + comboProps[key] = oldProps[key]; + } + else { + comboProps[key] = newProps[key]; + anyChanges = true; + } + } + for (var key in oldProps) { + if (!(key in newProps)) { + anyChanges = true; + break; + } + } + return { anyChanges: anyChanges, comboProps: comboProps }; + } + + /* + PURPOSES: + - hook up to fg, fill, and mirror renderers + - interface for dragging and hits + */ + var DateComponent = /** @class */ (function (_super) { + __extends(DateComponent, _super); + function DateComponent(el) { + var _this = _super.call(this) || this; + _this.el = el; + return _this; + } + DateComponent.prototype.destroy = function () { + _super.prototype.destroy.call(this); + removeElement(this.el); + }; + // Hit System + // ----------------------------------------------------------------------------------------------------------------- + DateComponent.prototype.buildPositionCaches = function () { + }; + DateComponent.prototype.queryHit = function (positionLeft, positionTop, elWidth, elHeight) { + return null; // this should be abstract + }; + // Validation + // ----------------------------------------------------------------------------------------------------------------- + DateComponent.prototype.isInteractionValid = function (interaction) { + var calendar = this.context.calendar; + var dateProfile = this.props.dateProfile; // HACK + var instances = interaction.mutatedEvents.instances; + if (dateProfile) { // HACK for DayTile + for (var instanceId in instances) { + if (!rangeContainsRange(dateProfile.validRange, instances[instanceId].range)) { + return false; + } + } + } + return isInteractionValid(interaction, calendar); + }; + DateComponent.prototype.isDateSelectionValid = function (selection) { + var calendar = this.context.calendar; + var dateProfile = this.props.dateProfile; // HACK + if (dateProfile && // HACK for DayTile + !rangeContainsRange(dateProfile.validRange, selection.range)) { + return false; + } + return isDateSelectionValid(selection, calendar); + }; + // Pointer Interaction Utils + // ----------------------------------------------------------------------------------------------------------------- + DateComponent.prototype.isValidSegDownEl = function (el) { + return !this.props.eventDrag && // HACK + !this.props.eventResize && // HACK + !elementClosest(el, '.fc-mirror') && + (this.isPopover() || !this.isInPopover(el)); + // ^above line ensures we don't detect a seg interaction within a nested component. + // it's a HACK because it only supports a popover as the nested component. + }; + DateComponent.prototype.isValidDateDownEl = function (el) { + var segEl = elementClosest(el, this.fgSegSelector); + return (!segEl || segEl.classList.contains('fc-mirror')) && + !elementClosest(el, '.fc-more') && // a "more.." link + !elementClosest(el, 'a[data-goto]') && // a clickable nav link + !this.isInPopover(el); + }; + DateComponent.prototype.isPopover = function () { + return this.el.classList.contains('fc-popover'); + }; + DateComponent.prototype.isInPopover = function (el) { + return Boolean(elementClosest(el, '.fc-popover')); + }; + return DateComponent; + }(Component)); + DateComponent.prototype.fgSegSelector = '.fc-event-container > *'; + DateComponent.prototype.bgSegSelector = '.fc-bgevent:not(.fc-nonbusiness)'; + + var uid$1 = 0; + function createPlugin(input) { + return { + id: String(uid$1++), + deps: input.deps || [], + reducers: input.reducers || [], + eventDefParsers: input.eventDefParsers || [], + isDraggableTransformers: input.isDraggableTransformers || [], + eventDragMutationMassagers: input.eventDragMutationMassagers || [], + eventDefMutationAppliers: input.eventDefMutationAppliers || [], + dateSelectionTransformers: input.dateSelectionTransformers || [], + datePointTransforms: input.datePointTransforms || [], + dateSpanTransforms: input.dateSpanTransforms || [], + views: input.views || {}, + viewPropsTransformers: input.viewPropsTransformers || [], + isPropsValid: input.isPropsValid || null, + externalDefTransforms: input.externalDefTransforms || [], + eventResizeJoinTransforms: input.eventResizeJoinTransforms || [], + viewContainerModifiers: input.viewContainerModifiers || [], + eventDropTransformers: input.eventDropTransformers || [], + componentInteractions: input.componentInteractions || [], + calendarInteractions: input.calendarInteractions || [], + themeClasses: input.themeClasses || {}, + eventSourceDefs: input.eventSourceDefs || [], + cmdFormatter: input.cmdFormatter, + recurringTypes: input.recurringTypes || [], + namedTimeZonedImpl: input.namedTimeZonedImpl, + defaultView: input.defaultView || '', + elementDraggingImpl: input.elementDraggingImpl, + optionChangeHandlers: input.optionChangeHandlers || {} + }; + } + var PluginSystem = /** @class */ (function () { + function PluginSystem() { + this.hooks = { + reducers: [], + eventDefParsers: [], + isDraggableTransformers: [], + eventDragMutationMassagers: [], + eventDefMutationAppliers: [], + dateSelectionTransformers: [], + datePointTransforms: [], + dateSpanTransforms: [], + views: {}, + viewPropsTransformers: [], + isPropsValid: null, + externalDefTransforms: [], + eventResizeJoinTransforms: [], + viewContainerModifiers: [], + eventDropTransformers: [], + componentInteractions: [], + calendarInteractions: [], + themeClasses: {}, + eventSourceDefs: [], + cmdFormatter: null, + recurringTypes: [], + namedTimeZonedImpl: null, + defaultView: '', + elementDraggingImpl: null, + optionChangeHandlers: {} + }; + this.addedHash = {}; + } + PluginSystem.prototype.add = function (plugin) { + if (!this.addedHash[plugin.id]) { + this.addedHash[plugin.id] = true; + for (var _i = 0, _a = plugin.deps; _i < _a.length; _i++) { + var dep = _a[_i]; + this.add(dep); + } + this.hooks = combineHooks(this.hooks, plugin); + } + }; + return PluginSystem; + }()); + function combineHooks(hooks0, hooks1) { + return { + reducers: hooks0.reducers.concat(hooks1.reducers), + eventDefParsers: hooks0.eventDefParsers.concat(hooks1.eventDefParsers), + isDraggableTransformers: hooks0.isDraggableTransformers.concat(hooks1.isDraggableTransformers), + eventDragMutationMassagers: hooks0.eventDragMutationMassagers.concat(hooks1.eventDragMutationMassagers), + eventDefMutationAppliers: hooks0.eventDefMutationAppliers.concat(hooks1.eventDefMutationAppliers), + dateSelectionTransformers: hooks0.dateSelectionTransformers.concat(hooks1.dateSelectionTransformers), + datePointTransforms: hooks0.datePointTransforms.concat(hooks1.datePointTransforms), + dateSpanTransforms: hooks0.dateSpanTransforms.concat(hooks1.dateSpanTransforms), + views: __assign({}, hooks0.views, hooks1.views), + viewPropsTransformers: hooks0.viewPropsTransformers.concat(hooks1.viewPropsTransformers), + isPropsValid: hooks1.isPropsValid || hooks0.isPropsValid, + externalDefTransforms: hooks0.externalDefTransforms.concat(hooks1.externalDefTransforms), + eventResizeJoinTransforms: hooks0.eventResizeJoinTransforms.concat(hooks1.eventResizeJoinTransforms), + viewContainerModifiers: hooks0.viewContainerModifiers.concat(hooks1.viewContainerModifiers), + eventDropTransformers: hooks0.eventDropTransformers.concat(hooks1.eventDropTransformers), + calendarInteractions: hooks0.calendarInteractions.concat(hooks1.calendarInteractions), + componentInteractions: hooks0.componentInteractions.concat(hooks1.componentInteractions), + themeClasses: __assign({}, hooks0.themeClasses, hooks1.themeClasses), + eventSourceDefs: hooks0.eventSourceDefs.concat(hooks1.eventSourceDefs), + cmdFormatter: hooks1.cmdFormatter || hooks0.cmdFormatter, + recurringTypes: hooks0.recurringTypes.concat(hooks1.recurringTypes), + namedTimeZonedImpl: hooks1.namedTimeZonedImpl || hooks0.namedTimeZonedImpl, + defaultView: hooks0.defaultView || hooks1.defaultView, + elementDraggingImpl: hooks0.elementDraggingImpl || hooks1.elementDraggingImpl, + optionChangeHandlers: __assign({}, hooks0.optionChangeHandlers, hooks1.optionChangeHandlers) + }; + } + + var eventSourceDef = { + ignoreRange: true, + parseMeta: function (raw) { + if (Array.isArray(raw)) { // short form + return raw; + } + else if (Array.isArray(raw.events)) { + return raw.events; + } + return null; + }, + fetch: function (arg, success) { + success({ + rawEvents: arg.eventSource.meta + }); + } + }; + var ArrayEventSourcePlugin = createPlugin({ + eventSourceDefs: [eventSourceDef] + }); + + var eventSourceDef$1 = { + parseMeta: function (raw) { + if (typeof raw === 'function') { // short form + return raw; + } + else if (typeof raw.events === 'function') { + return raw.events; + } + return null; + }, + fetch: function (arg, success, failure) { + var dateEnv = arg.calendar.dateEnv; + var func = arg.eventSource.meta; + unpromisify(func.bind(null, { + start: dateEnv.toDate(arg.range.start), + end: dateEnv.toDate(arg.range.end), + startStr: dateEnv.formatIso(arg.range.start), + endStr: dateEnv.formatIso(arg.range.end), + timeZone: dateEnv.timeZone + }), function (rawEvents) { + success({ rawEvents: rawEvents }); // needs an object response + }, failure // send errorObj directly to failure callback + ); + } + }; + var FuncEventSourcePlugin = createPlugin({ + eventSourceDefs: [eventSourceDef$1] + }); + + function requestJson(method, url, params, successCallback, failureCallback) { + method = method.toUpperCase(); + var body = null; + if (method === 'GET') { + url = injectQueryStringParams(url, params); + } + else { + body = encodeParams(params); + } + var xhr = new XMLHttpRequest(); + xhr.open(method, url, true); + if (method !== 'GET') { + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + } + xhr.onload = function () { + if (xhr.status >= 200 && xhr.status < 400) { + try { + var res = JSON.parse(xhr.responseText); + successCallback(res, xhr); + } + catch (err) { + failureCallback('Failure parsing JSON', xhr); + } + } + else { + failureCallback('Request failed', xhr); + } + }; + xhr.onerror = function () { + failureCallback('Request failed', xhr); + }; + xhr.send(body); + } + function injectQueryStringParams(url, params) { + return url + + (url.indexOf('?') === -1 ? '?' : '&') + + encodeParams(params); + } + function encodeParams(params) { + var parts = []; + for (var key in params) { + parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key])); + } + return parts.join('&'); + } + + var eventSourceDef$2 = { + parseMeta: function (raw) { + if (typeof raw === 'string') { // short form + raw = { url: raw }; + } + else if (!raw || typeof raw !== 'object' || !raw.url) { + return null; + } + return { + url: raw.url, + method: (raw.method || 'GET').toUpperCase(), + extraParams: raw.extraParams, + startParam: raw.startParam, + endParam: raw.endParam, + timeZoneParam: raw.timeZoneParam + }; + }, + fetch: function (arg, success, failure) { + var meta = arg.eventSource.meta; + var requestParams = buildRequestParams(meta, arg.range, arg.calendar); + requestJson(meta.method, meta.url, requestParams, function (rawEvents, xhr) { + success({ rawEvents: rawEvents, xhr: xhr }); + }, function (errorMessage, xhr) { + failure({ message: errorMessage, xhr: xhr }); + }); + } + }; + var JsonFeedEventSourcePlugin = createPlugin({ + eventSourceDefs: [eventSourceDef$2] + }); + function buildRequestParams(meta, range, calendar) { + var dateEnv = calendar.dateEnv; + var startParam; + var endParam; + var timeZoneParam; + var customRequestParams; + var params = {}; + startParam = meta.startParam; + if (startParam == null) { + startParam = calendar.opt('startParam'); + } + endParam = meta.endParam; + if (endParam == null) { + endParam = calendar.opt('endParam'); + } + timeZoneParam = meta.timeZoneParam; + if (timeZoneParam == null) { + timeZoneParam = calendar.opt('timeZoneParam'); + } + // retrieve any outbound GET/POST data from the options + if (typeof meta.extraParams === 'function') { + // supplied as a function that returns a key/value object + customRequestParams = meta.extraParams(); + } + else { + // probably supplied as a straight key/value object + customRequestParams = meta.extraParams || {}; + } + __assign(params, customRequestParams); + params[startParam] = dateEnv.formatIso(range.start); + params[endParam] = dateEnv.formatIso(range.end); + if (dateEnv.timeZone !== 'local') { + params[timeZoneParam] = dateEnv.timeZone; + } + return params; + } + + var recurring = { + parse: function (rawEvent, leftoverProps, dateEnv) { + var createMarker = dateEnv.createMarker.bind(dateEnv); + var processors = { + daysOfWeek: null, + startTime: createDuration, + endTime: createDuration, + startRecur: createMarker, + endRecur: createMarker + }; + var props = refineProps(rawEvent, processors, {}, leftoverProps); + var anyValid = false; + for (var propName in props) { + if (props[propName] != null) { + anyValid = true; + break; + } + } + if (anyValid) { + var duration = null; + if ('duration' in leftoverProps) { + duration = createDuration(leftoverProps.duration); + delete leftoverProps.duration; + } + if (!duration && props.startTime && props.endTime) { + duration = subtractDurations(props.endTime, props.startTime); + } + return { + allDayGuess: Boolean(!props.startTime && !props.endTime), + duration: duration, + typeData: props // doesn't need endTime anymore but oh well + }; + } + return null; + }, + expand: function (typeData, framingRange, dateEnv) { + var clippedFramingRange = intersectRanges(framingRange, { start: typeData.startRecur, end: typeData.endRecur }); + if (clippedFramingRange) { + return expandRanges(typeData.daysOfWeek, typeData.startTime, clippedFramingRange, dateEnv); + } + else { + return []; + } + } + }; + var SimpleRecurrencePlugin = createPlugin({ + recurringTypes: [recurring] + }); + function expandRanges(daysOfWeek, startTime, framingRange, dateEnv) { + var dowHash = daysOfWeek ? arrayToHash(daysOfWeek) : null; + var dayMarker = startOfDay(framingRange.start); + var endMarker = framingRange.end; + var instanceStarts = []; + while (dayMarker < endMarker) { + var instanceStart + // if everyday, or this particular day-of-week + = void 0; + // if everyday, or this particular day-of-week + if (!dowHash || dowHash[dayMarker.getUTCDay()]) { + if (startTime) { + instanceStart = dateEnv.add(dayMarker, startTime); + } + else { + instanceStart = dayMarker; + } + instanceStarts.push(instanceStart); + } + dayMarker = addDays(dayMarker, 1); + } + return instanceStarts; + } + + var DefaultOptionChangeHandlers = createPlugin({ + optionChangeHandlers: { + events: function (events, calendar, deepEqual) { + handleEventSources([events], calendar, deepEqual); + }, + eventSources: handleEventSources, + plugins: handlePlugins + } + }); + function handleEventSources(inputs, calendar, deepEqual) { + var unfoundSources = hashValuesToArray(calendar.state.eventSources); + var newInputs = []; + for (var _i = 0, inputs_1 = inputs; _i < inputs_1.length; _i++) { + var input = inputs_1[_i]; + var inputFound = false; + for (var i = 0; i < unfoundSources.length; i++) { + if (deepEqual(unfoundSources[i]._raw, input)) { + unfoundSources.splice(i, 1); // delete + inputFound = true; + break; + } + } + if (!inputFound) { + newInputs.push(input); + } + } + for (var _a = 0, unfoundSources_1 = unfoundSources; _a < unfoundSources_1.length; _a++) { + var unfoundSource = unfoundSources_1[_a]; + calendar.dispatch({ + type: 'REMOVE_EVENT_SOURCE', + sourceId: unfoundSource.sourceId + }); + } + for (var _b = 0, newInputs_1 = newInputs; _b < newInputs_1.length; _b++) { + var newInput = newInputs_1[_b]; + calendar.addEventSource(newInput); + } + } + // shortcoming: won't remove plugins + function handlePlugins(inputs, calendar) { + calendar.addPluginInputs(inputs); // will gracefully handle duplicates + } + + var config = {}; // TODO: make these options + var globalDefaults = { + defaultRangeSeparator: ' - ', + titleRangeSeparator: ' \u2013 ', + defaultTimedEventDuration: '01:00:00', + defaultAllDayEventDuration: { day: 1 }, + forceEventDuration: false, + nextDayThreshold: '00:00:00', + // display + columnHeader: true, + defaultView: '', + aspectRatio: 1.35, + header: { + left: 'title', + center: '', + right: 'today prev,next' + }, + weekends: true, + weekNumbers: false, + weekNumberCalculation: 'local', + editable: false, + // nowIndicator: false, + scrollTime: '06:00:00', + minTime: '00:00:00', + maxTime: '24:00:00', + showNonCurrentDates: true, + // event ajax + lazyFetching: true, + startParam: 'start', + endParam: 'end', + timeZoneParam: 'timeZone', + timeZone: 'local', + // allDayDefault: undefined, + // locale + locales: [], + locale: '', + // dir: will get this from the default locale + // buttonIcons: null, + // allows setting a min-height to the event segment to prevent short events overlapping each other + timeGridEventMinHeight: 0, + themeSystem: 'standard', + // eventResizableFromStart: false, + dragRevertDuration: 500, + dragScroll: true, + allDayMaintainDuration: false, + // selectable: false, + unselectAuto: true, + // selectMinDistance: 0, + dropAccept: '*', + eventOrder: 'start,-duration,allDay,title', + // ^ if start tie, longer events go before shorter. final tie-breaker is title text + // rerenderDelay: null, + eventLimit: false, + eventLimitClick: 'popover', + dayPopoverFormat: { month: 'long', day: 'numeric', year: 'numeric' }, + handleWindowResize: true, + windowResizeDelay: 100, + longPressDelay: 1000, + eventDragMinDistance: 5 // only applies to mouse + }; + var rtlDefaults = { + header: { + left: 'next,prev today', + center: '', + right: 'title' + }, + buttonIcons: { + // TODO: make RTL support the responibility of the theme + prev: 'fc-icon-chevron-right', + next: 'fc-icon-chevron-left', + prevYear: 'fc-icon-chevrons-right', + nextYear: 'fc-icon-chevrons-left' + } + }; + var complexOptions = [ + 'header', + 'footer', + 'buttonText', + 'buttonIcons' + ]; + // Merges an array of option objects into a single object + function mergeOptions(optionObjs) { + return mergeProps(optionObjs, complexOptions); + } + // TODO: move this stuff to a "plugin"-related file... + var INTERNAL_PLUGINS = [ + ArrayEventSourcePlugin, + FuncEventSourcePlugin, + JsonFeedEventSourcePlugin, + SimpleRecurrencePlugin, + DefaultOptionChangeHandlers + ]; + function refinePluginDefs(pluginInputs) { + var plugins = []; + for (var _i = 0, pluginInputs_1 = pluginInputs; _i < pluginInputs_1.length; _i++) { + var pluginInput = pluginInputs_1[_i]; + if (typeof pluginInput === 'string') { + var globalName = 'FullCalendar' + capitaliseFirstLetter(pluginInput); + if (!window[globalName]) { + console.warn('Plugin file not loaded for ' + pluginInput); + } + else { + plugins.push(window[globalName].default); // is an ES6 module + } + } + else { + plugins.push(pluginInput); + } + } + return INTERNAL_PLUGINS.concat(plugins); + } + + var RAW_EN_LOCALE = { + code: 'en', + week: { + dow: 0, + doy: 4 // 4 days need to be within the year to be considered the first week + }, + dir: 'ltr', + buttonText: { + prev: 'prev', + next: 'next', + prevYear: 'prev year', + nextYear: 'next year', + year: 'year', + today: 'today', + month: 'month', + week: 'week', + day: 'day', + list: 'list' + }, + weekLabel: 'W', + allDayText: 'all-day', + eventLimitText: 'more', + noEventsMessage: 'No events to display' + }; + function parseRawLocales(explicitRawLocales) { + var defaultCode = explicitRawLocales.length > 0 ? explicitRawLocales[0].code : 'en'; + var globalArray = window['FullCalendarLocalesAll'] || []; // from locales-all.js + var globalObject = window['FullCalendarLocales'] || {}; // from locales/*.js. keys are meaningless + var allRawLocales = globalArray.concat(// globalArray is low prio + hashValuesToArray(globalObject), // medium prio + explicitRawLocales // highest prio + ); + var rawLocaleMap = { + en: RAW_EN_LOCALE // necessary? + }; + for (var _i = 0, allRawLocales_1 = allRawLocales; _i < allRawLocales_1.length; _i++) { + var rawLocale = allRawLocales_1[_i]; + rawLocaleMap[rawLocale.code] = rawLocale; + } + return { + map: rawLocaleMap, + defaultCode: defaultCode + }; + } + function buildLocale(inputSingular, available) { + if (typeof inputSingular === 'object' && !Array.isArray(inputSingular)) { + return parseLocale(inputSingular.code, [inputSingular.code], inputSingular); + } + else { + return queryLocale(inputSingular, available); + } + } + function queryLocale(codeArg, available) { + var codes = [].concat(codeArg || []); // will convert to array + var raw = queryRawLocale(codes, available) || RAW_EN_LOCALE; + return parseLocale(codeArg, codes, raw); + } + function queryRawLocale(codes, available) { + for (var i = 0; i < codes.length; i++) { + var parts = codes[i].toLocaleLowerCase().split('-'); + for (var j = parts.length; j > 0; j--) { + var simpleId = parts.slice(0, j).join('-'); + if (available[simpleId]) { + return available[simpleId]; + } + } + } + return null; + } + function parseLocale(codeArg, codes, raw) { + var merged = mergeProps([RAW_EN_LOCALE, raw], ['buttonText']); + delete merged.code; // don't want this part of the options + var week = merged.week; + delete merged.week; + return { + codeArg: codeArg, + codes: codes, + week: week, + simpleNumberFormat: new Intl.NumberFormat(codeArg), + options: merged + }; + } + + var OptionsManager = /** @class */ (function () { + function OptionsManager(overrides) { + this.overrides = __assign({}, overrides); // make a copy + this.dynamicOverrides = {}; + this.compute(); + } + OptionsManager.prototype.mutate = function (updates, removals, isDynamic) { + if (!Object.keys(updates).length && !removals.length) { + return; + } + var overrideHash = isDynamic ? this.dynamicOverrides : this.overrides; + __assign(overrideHash, updates); + for (var _i = 0, removals_1 = removals; _i < removals_1.length; _i++) { + var propName = removals_1[_i]; + delete overrideHash[propName]; + } + this.compute(); + }; + // Computes the flattened options hash for the calendar and assigns to `this.options`. + // Assumes this.overrides and this.dynamicOverrides have already been initialized. + OptionsManager.prototype.compute = function () { + // TODO: not a very efficient system + var locales = firstDefined(// explicit locale option given? + this.dynamicOverrides.locales, this.overrides.locales, globalDefaults.locales); + var locale = firstDefined(// explicit locales option given? + this.dynamicOverrides.locale, this.overrides.locale, globalDefaults.locale); + var available = parseRawLocales(locales); + var localeDefaults = buildLocale(locale || available.defaultCode, available.map).options; + var dir = firstDefined(// based on options computed so far, is direction RTL? + this.dynamicOverrides.dir, this.overrides.dir, localeDefaults.dir); + var dirDefaults = dir === 'rtl' ? rtlDefaults : {}; + this.dirDefaults = dirDefaults; + this.localeDefaults = localeDefaults; + this.computed = mergeOptions([ + globalDefaults, + dirDefaults, + localeDefaults, + this.overrides, + this.dynamicOverrides + ]); + }; + return OptionsManager; + }()); + + var calendarSystemClassMap = {}; + function registerCalendarSystem(name, theClass) { + calendarSystemClassMap[name] = theClass; + } + function createCalendarSystem(name) { + return new calendarSystemClassMap[name](); + } + var GregorianCalendarSystem = /** @class */ (function () { + function GregorianCalendarSystem() { + } + GregorianCalendarSystem.prototype.getMarkerYear = function (d) { + return d.getUTCFullYear(); + }; + GregorianCalendarSystem.prototype.getMarkerMonth = function (d) { + return d.getUTCMonth(); + }; + GregorianCalendarSystem.prototype.getMarkerDay = function (d) { + return d.getUTCDate(); + }; + GregorianCalendarSystem.prototype.arrayToMarker = function (arr) { + return arrayToUtcDate(arr); + }; + GregorianCalendarSystem.prototype.markerToArray = function (marker) { + return dateToUtcArray(marker); + }; + return GregorianCalendarSystem; + }()); + registerCalendarSystem('gregory', GregorianCalendarSystem); + + var ISO_RE = /^\s*(\d{4})(-(\d{2})(-(\d{2})([T ](\d{2}):(\d{2})(:(\d{2})(\.(\d+))?)?(Z|(([-+])(\d{2})(:?(\d{2}))?))?)?)?)?$/; + function parse(str) { + var m = ISO_RE.exec(str); + if (m) { + var marker = new Date(Date.UTC(Number(m[1]), m[3] ? Number(m[3]) - 1 : 0, Number(m[5] || 1), Number(m[7] || 0), Number(m[8] || 0), Number(m[10] || 0), m[12] ? Number('0.' + m[12]) * 1000 : 0)); + if (isValidDate(marker)) { + var timeZoneOffset = null; + if (m[13]) { + timeZoneOffset = (m[15] === '-' ? -1 : 1) * (Number(m[16] || 0) * 60 + + Number(m[18] || 0)); + } + return { + marker: marker, + isTimeUnspecified: !m[6], + timeZoneOffset: timeZoneOffset + }; + } + } + return null; + } + + var DateEnv = /** @class */ (function () { + function DateEnv(settings) { + var timeZone = this.timeZone = settings.timeZone; + var isNamedTimeZone = timeZone !== 'local' && timeZone !== 'UTC'; + if (settings.namedTimeZoneImpl && isNamedTimeZone) { + this.namedTimeZoneImpl = new settings.namedTimeZoneImpl(timeZone); + } + this.canComputeOffset = Boolean(!isNamedTimeZone || this.namedTimeZoneImpl); + this.calendarSystem = createCalendarSystem(settings.calendarSystem); + this.locale = settings.locale; + this.weekDow = settings.locale.week.dow; + this.weekDoy = settings.locale.week.doy; + if (settings.weekNumberCalculation === 'ISO') { + this.weekDow = 1; + this.weekDoy = 4; + } + if (typeof settings.firstDay === 'number') { + this.weekDow = settings.firstDay; + } + if (typeof settings.weekNumberCalculation === 'function') { + this.weekNumberFunc = settings.weekNumberCalculation; + } + this.weekLabel = settings.weekLabel != null ? settings.weekLabel : settings.locale.options.weekLabel; + this.cmdFormatter = settings.cmdFormatter; + } + // Creating / Parsing + DateEnv.prototype.createMarker = function (input) { + var meta = this.createMarkerMeta(input); + if (meta === null) { + return null; + } + return meta.marker; + }; + DateEnv.prototype.createNowMarker = function () { + if (this.canComputeOffset) { + return this.timestampToMarker(new Date().valueOf()); + } + else { + // if we can't compute the current date val for a timezone, + // better to give the current local date vals than UTC + return arrayToUtcDate(dateToLocalArray(new Date())); + } + }; + DateEnv.prototype.createMarkerMeta = function (input) { + if (typeof input === 'string') { + return this.parse(input); + } + var marker = null; + if (typeof input === 'number') { + marker = this.timestampToMarker(input); + } + else if (input instanceof Date) { + input = input.valueOf(); + if (!isNaN(input)) { + marker = this.timestampToMarker(input); + } + } + else if (Array.isArray(input)) { + marker = arrayToUtcDate(input); + } + if (marker === null || !isValidDate(marker)) { + return null; + } + return { marker: marker, isTimeUnspecified: false, forcedTzo: null }; + }; + DateEnv.prototype.parse = function (s) { + var parts = parse(s); + if (parts === null) { + return null; + } + var marker = parts.marker; + var forcedTzo = null; + if (parts.timeZoneOffset !== null) { + if (this.canComputeOffset) { + marker = this.timestampToMarker(marker.valueOf() - parts.timeZoneOffset * 60 * 1000); + } + else { + forcedTzo = parts.timeZoneOffset; + } + } + return { marker: marker, isTimeUnspecified: parts.isTimeUnspecified, forcedTzo: forcedTzo }; + }; + // Accessors + DateEnv.prototype.getYear = function (marker) { + return this.calendarSystem.getMarkerYear(marker); + }; + DateEnv.prototype.getMonth = function (marker) { + return this.calendarSystem.getMarkerMonth(marker); + }; + // Adding / Subtracting + DateEnv.prototype.add = function (marker, dur) { + var a = this.calendarSystem.markerToArray(marker); + a[0] += dur.years; + a[1] += dur.months; + a[2] += dur.days; + a[6] += dur.milliseconds; + return this.calendarSystem.arrayToMarker(a); + }; + DateEnv.prototype.subtract = function (marker, dur) { + var a = this.calendarSystem.markerToArray(marker); + a[0] -= dur.years; + a[1] -= dur.months; + a[2] -= dur.days; + a[6] -= dur.milliseconds; + return this.calendarSystem.arrayToMarker(a); + }; + DateEnv.prototype.addYears = function (marker, n) { + var a = this.calendarSystem.markerToArray(marker); + a[0] += n; + return this.calendarSystem.arrayToMarker(a); + }; + DateEnv.prototype.addMonths = function (marker, n) { + var a = this.calendarSystem.markerToArray(marker); + a[1] += n; + return this.calendarSystem.arrayToMarker(a); + }; + // Diffing Whole Units + DateEnv.prototype.diffWholeYears = function (m0, m1) { + var calendarSystem = this.calendarSystem; + if (timeAsMs(m0) === timeAsMs(m1) && + calendarSystem.getMarkerDay(m0) === calendarSystem.getMarkerDay(m1) && + calendarSystem.getMarkerMonth(m0) === calendarSystem.getMarkerMonth(m1)) { + return calendarSystem.getMarkerYear(m1) - calendarSystem.getMarkerYear(m0); + } + return null; + }; + DateEnv.prototype.diffWholeMonths = function (m0, m1) { + var calendarSystem = this.calendarSystem; + if (timeAsMs(m0) === timeAsMs(m1) && + calendarSystem.getMarkerDay(m0) === calendarSystem.getMarkerDay(m1)) { + return (calendarSystem.getMarkerMonth(m1) - calendarSystem.getMarkerMonth(m0)) + + (calendarSystem.getMarkerYear(m1) - calendarSystem.getMarkerYear(m0)) * 12; + } + return null; + }; + // Range / Duration + DateEnv.prototype.greatestWholeUnit = function (m0, m1) { + var n = this.diffWholeYears(m0, m1); + if (n !== null) { + return { unit: 'year', value: n }; + } + n = this.diffWholeMonths(m0, m1); + if (n !== null) { + return { unit: 'month', value: n }; + } + n = diffWholeWeeks(m0, m1); + if (n !== null) { + return { unit: 'week', value: n }; + } + n = diffWholeDays(m0, m1); + if (n !== null) { + return { unit: 'day', value: n }; + } + n = diffHours(m0, m1); + if (isInt(n)) { + return { unit: 'hour', value: n }; + } + n = diffMinutes(m0, m1); + if (isInt(n)) { + return { unit: 'minute', value: n }; + } + n = diffSeconds(m0, m1); + if (isInt(n)) { + return { unit: 'second', value: n }; + } + return { unit: 'millisecond', value: m1.valueOf() - m0.valueOf() }; + }; + DateEnv.prototype.countDurationsBetween = function (m0, m1, d) { + // TODO: can use greatestWholeUnit + var diff; + if (d.years) { + diff = this.diffWholeYears(m0, m1); + if (diff !== null) { + return diff / asRoughYears(d); + } + } + if (d.months) { + diff = this.diffWholeMonths(m0, m1); + if (diff !== null) { + return diff / asRoughMonths(d); + } + } + if (d.days) { + diff = diffWholeDays(m0, m1); + if (diff !== null) { + return diff / asRoughDays(d); + } + } + return (m1.valueOf() - m0.valueOf()) / asRoughMs(d); + }; + // Start-Of + DateEnv.prototype.startOf = function (m, unit) { + if (unit === 'year') { + return this.startOfYear(m); + } + else if (unit === 'month') { + return this.startOfMonth(m); + } + else if (unit === 'week') { + return this.startOfWeek(m); + } + else if (unit === 'day') { + return startOfDay(m); + } + else if (unit === 'hour') { + return startOfHour(m); + } + else if (unit === 'minute') { + return startOfMinute(m); + } + else if (unit === 'second') { + return startOfSecond(m); + } + }; + DateEnv.prototype.startOfYear = function (m) { + return this.calendarSystem.arrayToMarker([ + this.calendarSystem.getMarkerYear(m) + ]); + }; + DateEnv.prototype.startOfMonth = function (m) { + return this.calendarSystem.arrayToMarker([ + this.calendarSystem.getMarkerYear(m), + this.calendarSystem.getMarkerMonth(m) + ]); + }; + DateEnv.prototype.startOfWeek = function (m) { + return this.calendarSystem.arrayToMarker([ + this.calendarSystem.getMarkerYear(m), + this.calendarSystem.getMarkerMonth(m), + m.getUTCDate() - ((m.getUTCDay() - this.weekDow + 7) % 7) + ]); + }; + // Week Number + DateEnv.prototype.computeWeekNumber = function (marker) { + if (this.weekNumberFunc) { + return this.weekNumberFunc(this.toDate(marker)); + } + else { + return weekOfYear(marker, this.weekDow, this.weekDoy); + } + }; + // TODO: choke on timeZoneName: long + DateEnv.prototype.format = function (marker, formatter, dateOptions) { + if (dateOptions === void 0) { dateOptions = {}; } + return formatter.format({ + marker: marker, + timeZoneOffset: dateOptions.forcedTzo != null ? + dateOptions.forcedTzo : + this.offsetForMarker(marker) + }, this); + }; + DateEnv.prototype.formatRange = function (start, end, formatter, dateOptions) { + if (dateOptions === void 0) { dateOptions = {}; } + if (dateOptions.isEndExclusive) { + end = addMs(end, -1); + } + return formatter.formatRange({ + marker: start, + timeZoneOffset: dateOptions.forcedStartTzo != null ? + dateOptions.forcedStartTzo : + this.offsetForMarker(start) + }, { + marker: end, + timeZoneOffset: dateOptions.forcedEndTzo != null ? + dateOptions.forcedEndTzo : + this.offsetForMarker(end) + }, this); + }; + DateEnv.prototype.formatIso = function (marker, extraOptions) { + if (extraOptions === void 0) { extraOptions = {}; } + var timeZoneOffset = null; + if (!extraOptions.omitTimeZoneOffset) { + if (extraOptions.forcedTzo != null) { + timeZoneOffset = extraOptions.forcedTzo; + } + else { + timeZoneOffset = this.offsetForMarker(marker); + } + } + return buildIsoString(marker, timeZoneOffset, extraOptions.omitTime); + }; + // TimeZone + DateEnv.prototype.timestampToMarker = function (ms) { + if (this.timeZone === 'local') { + return arrayToUtcDate(dateToLocalArray(new Date(ms))); + } + else if (this.timeZone === 'UTC' || !this.namedTimeZoneImpl) { + return new Date(ms); + } + else { + return arrayToUtcDate(this.namedTimeZoneImpl.timestampToArray(ms)); + } + }; + DateEnv.prototype.offsetForMarker = function (m) { + if (this.timeZone === 'local') { + return -arrayToLocalDate(dateToUtcArray(m)).getTimezoneOffset(); // convert "inverse" offset to "normal" offset + } + else if (this.timeZone === 'UTC') { + return 0; + } + else if (this.namedTimeZoneImpl) { + return this.namedTimeZoneImpl.offsetForArray(dateToUtcArray(m)); + } + return null; + }; + // Conversion + DateEnv.prototype.toDate = function (m, forcedTzo) { + if (this.timeZone === 'local') { + return arrayToLocalDate(dateToUtcArray(m)); + } + else if (this.timeZone === 'UTC') { + return new Date(m.valueOf()); // make sure it's a copy + } + else if (!this.namedTimeZoneImpl) { + return new Date(m.valueOf() - (forcedTzo || 0)); + } + else { + return new Date(m.valueOf() - + this.namedTimeZoneImpl.offsetForArray(dateToUtcArray(m)) * 1000 * 60 // convert minutes -> ms + ); + } + }; + return DateEnv; + }()); + + var SIMPLE_SOURCE_PROPS = { + id: String, + allDayDefault: Boolean, + eventDataTransform: Function, + success: Function, + failure: Function + }; + var uid$2 = 0; + function doesSourceNeedRange(eventSource, calendar) { + var defs = calendar.pluginSystem.hooks.eventSourceDefs; + return !defs[eventSource.sourceDefId].ignoreRange; + } + function parseEventSource(raw, calendar) { + var defs = calendar.pluginSystem.hooks.eventSourceDefs; + for (var i = defs.length - 1; i >= 0; i--) { // later-added plugins take precedence + var def = defs[i]; + var meta = def.parseMeta(raw); + if (meta) { + var res = parseEventSourceProps(typeof raw === 'object' ? raw : {}, meta, i, calendar); + res._raw = raw; + return res; + } + } + return null; + } + function parseEventSourceProps(raw, meta, sourceDefId, calendar) { + var leftovers0 = {}; + var props = refineProps(raw, SIMPLE_SOURCE_PROPS, {}, leftovers0); + var leftovers1 = {}; + var ui = processUnscopedUiProps(leftovers0, calendar, leftovers1); + props.isFetching = false; + props.latestFetchId = ''; + props.fetchRange = null; + props.publicId = String(raw.id || ''); + props.sourceId = String(uid$2++); + props.sourceDefId = sourceDefId; + props.meta = meta; + props.ui = ui; + props.extendedProps = leftovers1; + return props; + } + + function reduceEventSources (eventSources, action, dateProfile, calendar) { + switch (action.type) { + case 'ADD_EVENT_SOURCES': // already parsed + return addSources(eventSources, action.sources, dateProfile ? dateProfile.activeRange : null, calendar); + case 'REMOVE_EVENT_SOURCE': + return removeSource(eventSources, action.sourceId); + case 'PREV': // TODO: how do we track all actions that affect dateProfile :( + case 'NEXT': + case 'SET_DATE': + case 'SET_VIEW_TYPE': + if (dateProfile) { + return fetchDirtySources(eventSources, dateProfile.activeRange, calendar); + } + else { + return eventSources; + } + case 'FETCH_EVENT_SOURCES': + case 'CHANGE_TIMEZONE': + return fetchSourcesByIds(eventSources, action.sourceIds ? + arrayToHash(action.sourceIds) : + excludeStaticSources(eventSources, calendar), dateProfile ? dateProfile.activeRange : null, calendar); + case 'RECEIVE_EVENTS': + case 'RECEIVE_EVENT_ERROR': + return receiveResponse(eventSources, action.sourceId, action.fetchId, action.fetchRange); + case 'REMOVE_ALL_EVENT_SOURCES': + return {}; + default: + return eventSources; + } + } + var uid$3 = 0; + function addSources(eventSourceHash, sources, fetchRange, calendar) { + var hash = {}; + for (var _i = 0, sources_1 = sources; _i < sources_1.length; _i++) { + var source = sources_1[_i]; + hash[source.sourceId] = source; + } + if (fetchRange) { + hash = fetchDirtySources(hash, fetchRange, calendar); + } + return __assign({}, eventSourceHash, hash); + } + function removeSource(eventSourceHash, sourceId) { + return filterHash(eventSourceHash, function (eventSource) { + return eventSource.sourceId !== sourceId; + }); + } + function fetchDirtySources(sourceHash, fetchRange, calendar) { + return fetchSourcesByIds(sourceHash, filterHash(sourceHash, function (eventSource) { + return isSourceDirty(eventSource, fetchRange, calendar); + }), fetchRange, calendar); + } + function isSourceDirty(eventSource, fetchRange, calendar) { + if (!doesSourceNeedRange(eventSource, calendar)) { + return !eventSource.latestFetchId; + } + else { + return !calendar.opt('lazyFetching') || + !eventSource.fetchRange || + eventSource.isFetching || // always cancel outdated in-progress fetches + fetchRange.start < eventSource.fetchRange.start || + fetchRange.end > eventSource.fetchRange.end; + } + } + function fetchSourcesByIds(prevSources, sourceIdHash, fetchRange, calendar) { + var nextSources = {}; + for (var sourceId in prevSources) { + var source = prevSources[sourceId]; + if (sourceIdHash[sourceId]) { + nextSources[sourceId] = fetchSource(source, fetchRange, calendar); + } + else { + nextSources[sourceId] = source; + } + } + return nextSources; + } + function fetchSource(eventSource, fetchRange, calendar) { + var sourceDef = calendar.pluginSystem.hooks.eventSourceDefs[eventSource.sourceDefId]; + var fetchId = String(uid$3++); + sourceDef.fetch({ + eventSource: eventSource, + calendar: calendar, + range: fetchRange + }, function (res) { + var rawEvents = res.rawEvents; + var calSuccess = calendar.opt('eventSourceSuccess'); + var calSuccessRes; + var sourceSuccessRes; + if (eventSource.success) { + sourceSuccessRes = eventSource.success(rawEvents, res.xhr); + } + if (calSuccess) { + calSuccessRes = calSuccess(rawEvents, res.xhr); + } + rawEvents = sourceSuccessRes || calSuccessRes || rawEvents; + calendar.dispatch({ + type: 'RECEIVE_EVENTS', + sourceId: eventSource.sourceId, + fetchId: fetchId, + fetchRange: fetchRange, + rawEvents: rawEvents + }); + }, function (error) { + var callFailure = calendar.opt('eventSourceFailure'); + console.warn(error.message, error); + if (eventSource.failure) { + eventSource.failure(error); + } + if (callFailure) { + callFailure(error); + } + calendar.dispatch({ + type: 'RECEIVE_EVENT_ERROR', + sourceId: eventSource.sourceId, + fetchId: fetchId, + fetchRange: fetchRange, + error: error + }); + }); + return __assign({}, eventSource, { isFetching: true, latestFetchId: fetchId }); + } + function receiveResponse(sourceHash, sourceId, fetchId, fetchRange) { + var _a; + var eventSource = sourceHash[sourceId]; + if (eventSource && // not already removed + fetchId === eventSource.latestFetchId) { + return __assign({}, sourceHash, (_a = {}, _a[sourceId] = __assign({}, eventSource, { isFetching: false, fetchRange: fetchRange // also serves as a marker that at least one fetch has completed + }), _a)); + } + return sourceHash; + } + function excludeStaticSources(eventSources, calendar) { + return filterHash(eventSources, function (eventSource) { + return doesSourceNeedRange(eventSource, calendar); + }); + } + + var DateProfileGenerator = /** @class */ (function () { + function DateProfileGenerator(viewSpec, calendar) { + this.viewSpec = viewSpec; + this.options = viewSpec.options; + this.dateEnv = calendar.dateEnv; + this.calendar = calendar; + this.initHiddenDays(); + } + /* Date Range Computation + ------------------------------------------------------------------------------------------------------------------*/ + // Builds a structure with info about what the dates/ranges will be for the "prev" view. + DateProfileGenerator.prototype.buildPrev = function (currentDateProfile, currentDate) { + var dateEnv = this.dateEnv; + var prevDate = dateEnv.subtract(dateEnv.startOf(currentDate, currentDateProfile.currentRangeUnit), // important for start-of-month + currentDateProfile.dateIncrement); + return this.build(prevDate, -1); + }; + // Builds a structure with info about what the dates/ranges will be for the "next" view. + DateProfileGenerator.prototype.buildNext = function (currentDateProfile, currentDate) { + var dateEnv = this.dateEnv; + var nextDate = dateEnv.add(dateEnv.startOf(currentDate, currentDateProfile.currentRangeUnit), // important for start-of-month + currentDateProfile.dateIncrement); + return this.build(nextDate, 1); + }; + // Builds a structure holding dates/ranges for rendering around the given date. + // Optional direction param indicates whether the date is being incremented/decremented + // from its previous value. decremented = -1, incremented = 1 (default). + DateProfileGenerator.prototype.build = function (currentDate, direction, forceToValid) { + if (forceToValid === void 0) { forceToValid = false; } + var validRange; + var minTime = null; + var maxTime = null; + var currentInfo; + var isRangeAllDay; + var renderRange; + var activeRange; + var isValid; + validRange = this.buildValidRange(); + validRange = this.trimHiddenDays(validRange); + if (forceToValid) { + currentDate = constrainMarkerToRange(currentDate, validRange); + } + currentInfo = this.buildCurrentRangeInfo(currentDate, direction); + isRangeAllDay = /^(year|month|week|day)$/.test(currentInfo.unit); + renderRange = this.buildRenderRange(this.trimHiddenDays(currentInfo.range), currentInfo.unit, isRangeAllDay); + renderRange = this.trimHiddenDays(renderRange); + activeRange = renderRange; + if (!this.options.showNonCurrentDates) { + activeRange = intersectRanges(activeRange, currentInfo.range); + } + minTime = createDuration(this.options.minTime); + maxTime = createDuration(this.options.maxTime); + activeRange = this.adjustActiveRange(activeRange, minTime, maxTime); + activeRange = intersectRanges(activeRange, validRange); // might return null + // it's invalid if the originally requested date is not contained, + // or if the range is completely outside of the valid range. + isValid = rangesIntersect(currentInfo.range, validRange); + return { + // constraint for where prev/next operations can go and where events can be dragged/resized to. + // an object with optional start and end properties. + validRange: validRange, + // range the view is formally responsible for. + // for example, a month view might have 1st-31st, excluding padded dates + currentRange: currentInfo.range, + // name of largest unit being displayed, like "month" or "week" + currentRangeUnit: currentInfo.unit, + isRangeAllDay: isRangeAllDay, + // dates that display events and accept drag-n-drop + // will be `null` if no dates accept events + activeRange: activeRange, + // date range with a rendered skeleton + // includes not-active days that need some sort of DOM + renderRange: renderRange, + // Duration object that denotes the first visible time of any given day + minTime: minTime, + // Duration object that denotes the exclusive visible end time of any given day + maxTime: maxTime, + isValid: isValid, + // how far the current date will move for a prev/next operation + dateIncrement: this.buildDateIncrement(currentInfo.duration) + // pass a fallback (might be null) ^ + }; + }; + // Builds an object with optional start/end properties. + // Indicates the minimum/maximum dates to display. + // not responsible for trimming hidden days. + DateProfileGenerator.prototype.buildValidRange = function () { + return this.getRangeOption('validRange', this.calendar.getNow()) || + { start: null, end: null }; // completely open-ended + }; + // Builds a structure with info about the "current" range, the range that is + // highlighted as being the current month for example. + // See build() for a description of `direction`. + // Guaranteed to have `range` and `unit` properties. `duration` is optional. + DateProfileGenerator.prototype.buildCurrentRangeInfo = function (date, direction) { + var _a = this, viewSpec = _a.viewSpec, dateEnv = _a.dateEnv; + var duration = null; + var unit = null; + var range = null; + var dayCount; + if (viewSpec.duration) { + duration = viewSpec.duration; + unit = viewSpec.durationUnit; + range = this.buildRangeFromDuration(date, direction, duration, unit); + } + else if ((dayCount = this.options.dayCount)) { + unit = 'day'; + range = this.buildRangeFromDayCount(date, direction, dayCount); + } + else if ((range = this.buildCustomVisibleRange(date))) { + unit = dateEnv.greatestWholeUnit(range.start, range.end).unit; + } + else { + duration = this.getFallbackDuration(); + unit = greatestDurationDenominator(duration).unit; + range = this.buildRangeFromDuration(date, direction, duration, unit); + } + return { duration: duration, unit: unit, range: range }; + }; + DateProfileGenerator.prototype.getFallbackDuration = function () { + return createDuration({ day: 1 }); + }; + // Returns a new activeRange to have time values (un-ambiguate) + // minTime or maxTime causes the range to expand. + DateProfileGenerator.prototype.adjustActiveRange = function (range, minTime, maxTime) { + var dateEnv = this.dateEnv; + var start = range.start; + var end = range.end; + if (this.viewSpec.class.prototype.usesMinMaxTime) { + // expand active range if minTime is negative (why not when positive?) + if (asRoughDays(minTime) < 0) { + start = startOfDay(start); // necessary? + start = dateEnv.add(start, minTime); + } + // expand active range if maxTime is beyond one day (why not when positive?) + if (asRoughDays(maxTime) > 1) { + end = startOfDay(end); // necessary? + end = addDays(end, -1); + end = dateEnv.add(end, maxTime); + } + } + return { start: start, end: end }; + }; + // Builds the "current" range when it is specified as an explicit duration. + // `unit` is the already-computed greatestDurationDenominator unit of duration. + DateProfileGenerator.prototype.buildRangeFromDuration = function (date, direction, duration, unit) { + var dateEnv = this.dateEnv; + var alignment = this.options.dateAlignment; + var dateIncrementInput; + var dateIncrementDuration; + var start; + var end; + var res; + // compute what the alignment should be + if (!alignment) { + dateIncrementInput = this.options.dateIncrement; + if (dateIncrementInput) { + dateIncrementDuration = createDuration(dateIncrementInput); + // use the smaller of the two units + if (asRoughMs(dateIncrementDuration) < asRoughMs(duration)) { + alignment = greatestDurationDenominator(dateIncrementDuration, !getWeeksFromInput(dateIncrementInput)).unit; + } + else { + alignment = unit; + } + } + else { + alignment = unit; + } + } + // if the view displays a single day or smaller + if (asRoughDays(duration) <= 1) { + if (this.isHiddenDay(start)) { + start = this.skipHiddenDays(start, direction); + start = startOfDay(start); + } + } + function computeRes() { + start = dateEnv.startOf(date, alignment); + end = dateEnv.add(start, duration); + res = { start: start, end: end }; + } + computeRes(); + // if range is completely enveloped by hidden days, go past the hidden days + if (!this.trimHiddenDays(res)) { + date = this.skipHiddenDays(date, direction); + computeRes(); + } + return res; + }; + // Builds the "current" range when a dayCount is specified. + DateProfileGenerator.prototype.buildRangeFromDayCount = function (date, direction, dayCount) { + var dateEnv = this.dateEnv; + var customAlignment = this.options.dateAlignment; + var runningCount = 0; + var start = date; + var end; + if (customAlignment) { + start = dateEnv.startOf(start, customAlignment); + } + start = startOfDay(start); + start = this.skipHiddenDays(start, direction); + end = start; + do { + end = addDays(end, 1); + if (!this.isHiddenDay(end)) { + runningCount++; + } + } while (runningCount < dayCount); + return { start: start, end: end }; + }; + // Builds a normalized range object for the "visible" range, + // which is a way to define the currentRange and activeRange at the same time. + DateProfileGenerator.prototype.buildCustomVisibleRange = function (date) { + var dateEnv = this.dateEnv; + var visibleRange = this.getRangeOption('visibleRange', dateEnv.toDate(date)); + if (visibleRange && (visibleRange.start == null || visibleRange.end == null)) { + return null; + } + return visibleRange; + }; + // Computes the range that will represent the element/cells for *rendering*, + // but which may have voided days/times. + // not responsible for trimming hidden days. + DateProfileGenerator.prototype.buildRenderRange = function (currentRange, currentRangeUnit, isRangeAllDay) { + return currentRange; + }; + // Compute the duration value that should be added/substracted to the current date + // when a prev/next operation happens. + DateProfileGenerator.prototype.buildDateIncrement = function (fallback) { + var dateIncrementInput = this.options.dateIncrement; + var customAlignment; + if (dateIncrementInput) { + return createDuration(dateIncrementInput); + } + else if ((customAlignment = this.options.dateAlignment)) { + return createDuration(1, customAlignment); + } + else if (fallback) { + return fallback; + } + else { + return createDuration({ days: 1 }); + } + }; + // Arguments after name will be forwarded to a hypothetical function value + // WARNING: passed-in arguments will be given to generator functions as-is and can cause side-effects. + // Always clone your objects if you fear mutation. + DateProfileGenerator.prototype.getRangeOption = function (name) { + var otherArgs = []; + for (var _i = 1; _i < arguments.length; _i++) { + otherArgs[_i - 1] = arguments[_i]; + } + var val = this.options[name]; + if (typeof val === 'function') { + val = val.apply(null, otherArgs); + } + if (val) { + val = parseRange(val, this.dateEnv); + } + if (val) { + val = computeVisibleDayRange(val); + } + return val; + }; + /* Hidden Days + ------------------------------------------------------------------------------------------------------------------*/ + // Initializes internal variables related to calculating hidden days-of-week + DateProfileGenerator.prototype.initHiddenDays = function () { + var hiddenDays = this.options.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.options.weekends === false) { + hiddenDays.push(0, 6); // 0=sunday, 6=saturday + } + for (i = 0; i < 7; i++) { + if (!(isHiddenDayHash[i] = hiddenDays.indexOf(i) !== -1)) { + dayCnt++; + } + } + if (!dayCnt) { + throw new Error('invalid hiddenDays'); // all days were hidden? bad. + } + this.isHiddenDayHash = isHiddenDayHash; + }; + // Remove days from the beginning and end of the range that are computed as hidden. + // If the whole range is trimmed off, returns null + DateProfileGenerator.prototype.trimHiddenDays = function (range) { + var start = range.start; + var end = range.end; + if (start) { + start = this.skipHiddenDays(start); + } + if (end) { + end = this.skipHiddenDays(end, -1, true); + } + if (start == null || end == null || start < end) { + return { start: start, end: end }; + } + return null; + }; + // Is the current day hidden? + // `day` is a day-of-week index (0-6), or a Date (used for UTC) + DateProfileGenerator.prototype.isHiddenDay = function (day) { + if (day instanceof Date) { + day = day.getUTCDay(); + } + return this.isHiddenDayHash[day]; + }; + // Incrementing the current day until it is no longer a hidden day, returning a copy. + // DOES NOT CONSIDER validRange! + // 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) + DateProfileGenerator.prototype.skipHiddenDays = function (date, inc, isExclusive) { + if (inc === void 0) { inc = 1; } + if (isExclusive === void 0) { isExclusive = false; } + while (this.isHiddenDayHash[(date.getUTCDay() + (isExclusive ? inc : 0) + 7) % 7]) { + date = addDays(date, inc); + } + return date; + }; + return DateProfileGenerator; + }()); + // TODO: find a way to avoid comparing DateProfiles. it's tedious + function isDateProfilesEqual(p0, p1) { + return rangesEqual(p0.validRange, p1.validRange) && + rangesEqual(p0.activeRange, p1.activeRange) && + rangesEqual(p0.renderRange, p1.renderRange) && + durationsEqual(p0.minTime, p1.minTime) && + durationsEqual(p0.maxTime, p1.maxTime); + /* + TODO: compare more? + currentRange: DateRange + currentRangeUnit: string + isRangeAllDay: boolean + isValid: boolean + dateIncrement: Duration + */ + } + + function reduce (state, action, calendar) { + var viewType = reduceViewType(state.viewType, action); + var dateProfile = reduceDateProfile(state.dateProfile, action, state.currentDate, viewType, calendar); + var eventSources = reduceEventSources(state.eventSources, action, dateProfile, calendar); + var nextState = __assign({}, state, { viewType: viewType, + dateProfile: dateProfile, currentDate: reduceCurrentDate(state.currentDate, action, dateProfile), eventSources: eventSources, eventStore: reduceEventStore(state.eventStore, action, eventSources, dateProfile, calendar), dateSelection: reduceDateSelection(state.dateSelection, action, calendar), eventSelection: reduceSelectedEvent(state.eventSelection, action), eventDrag: reduceEventDrag(state.eventDrag, action, eventSources, calendar), eventResize: reduceEventResize(state.eventResize, action, eventSources, calendar), eventSourceLoadingLevel: computeLoadingLevel(eventSources), loadingLevel: computeLoadingLevel(eventSources) }); + for (var _i = 0, _a = calendar.pluginSystem.hooks.reducers; _i < _a.length; _i++) { + var reducerFunc = _a[_i]; + nextState = reducerFunc(nextState, action, calendar); + } + // console.log(action.type, nextState) + return nextState; + } + function reduceViewType(currentViewType, action) { + switch (action.type) { + case 'SET_VIEW_TYPE': + return action.viewType; + default: + return currentViewType; + } + } + function reduceDateProfile(currentDateProfile, action, currentDate, viewType, calendar) { + var newDateProfile; + switch (action.type) { + case 'PREV': + newDateProfile = calendar.dateProfileGenerators[viewType].buildPrev(currentDateProfile, currentDate); + break; + case 'NEXT': + newDateProfile = calendar.dateProfileGenerators[viewType].buildNext(currentDateProfile, currentDate); + break; + case 'SET_DATE': + if (!currentDateProfile.activeRange || + !rangeContainsMarker(currentDateProfile.currentRange, action.dateMarker)) { + newDateProfile = calendar.dateProfileGenerators[viewType].build(action.dateMarker, undefined, true // forceToValid + ); + } + break; + case 'SET_VIEW_TYPE': + var generator = calendar.dateProfileGenerators[viewType]; + if (!generator) { + throw new Error(viewType ? + 'The FullCalendar view "' + viewType + '" does not exist. Make sure your plugins are loaded correctly.' : + 'No available FullCalendar view plugins.'); + } + newDateProfile = generator.build(action.dateMarker || currentDate, undefined, true // forceToValid + ); + break; + } + if (newDateProfile && + newDateProfile.isValid && + !(currentDateProfile && isDateProfilesEqual(currentDateProfile, newDateProfile))) { + return newDateProfile; + } + else { + return currentDateProfile; + } + } + function reduceCurrentDate(currentDate, action, dateProfile) { + switch (action.type) { + case 'PREV': + case 'NEXT': + if (!rangeContainsMarker(dateProfile.currentRange, currentDate)) { + return dateProfile.currentRange.start; + } + else { + return currentDate; + } + case 'SET_DATE': + case 'SET_VIEW_TYPE': + var newDate = action.dateMarker || currentDate; + if (dateProfile.activeRange && !rangeContainsMarker(dateProfile.activeRange, newDate)) { + return dateProfile.currentRange.start; + } + else { + return newDate; + } + default: + return currentDate; + } + } + function reduceDateSelection(currentSelection, action, calendar) { + switch (action.type) { + case 'SELECT_DATES': + return action.selection; + case 'UNSELECT_DATES': + return null; + default: + return currentSelection; + } + } + function reduceSelectedEvent(currentInstanceId, action) { + switch (action.type) { + case 'SELECT_EVENT': + return action.eventInstanceId; + case 'UNSELECT_EVENT': + return ''; + default: + return currentInstanceId; + } + } + function reduceEventDrag(currentDrag, action, sources, calendar) { + switch (action.type) { + case 'SET_EVENT_DRAG': + var newDrag = action.state; + return { + affectedEvents: newDrag.affectedEvents, + mutatedEvents: newDrag.mutatedEvents, + isEvent: newDrag.isEvent, + origSeg: newDrag.origSeg + }; + case 'UNSET_EVENT_DRAG': + return null; + default: + return currentDrag; + } + } + function reduceEventResize(currentResize, action, sources, calendar) { + switch (action.type) { + case 'SET_EVENT_RESIZE': + var newResize = action.state; + return { + affectedEvents: newResize.affectedEvents, + mutatedEvents: newResize.mutatedEvents, + isEvent: newResize.isEvent, + origSeg: newResize.origSeg + }; + case 'UNSET_EVENT_RESIZE': + return null; + default: + return currentResize; + } + } + function computeLoadingLevel(eventSources) { + var cnt = 0; + for (var sourceId in eventSources) { + if (eventSources[sourceId].isFetching) { + cnt++; + } + } + return cnt; + } + + var STANDARD_PROPS = { + start: null, + end: null, + allDay: Boolean + }; + function parseDateSpan(raw, dateEnv, defaultDuration) { + var span = parseOpenDateSpan(raw, dateEnv); + var range = span.range; + if (!range.start) { + return null; + } + if (!range.end) { + if (defaultDuration == null) { + return null; + } + else { + range.end = dateEnv.add(range.start, defaultDuration); + } + } + return span; + } + /* + TODO: somehow combine with parseRange? + Will return null if the start/end props were present but parsed invalidly. + */ + function parseOpenDateSpan(raw, dateEnv) { + var leftovers = {}; + var standardProps = refineProps(raw, STANDARD_PROPS, {}, leftovers); + var startMeta = standardProps.start ? dateEnv.createMarkerMeta(standardProps.start) : null; + var endMeta = standardProps.end ? dateEnv.createMarkerMeta(standardProps.end) : null; + var allDay = standardProps.allDay; + if (allDay == null) { + allDay = (startMeta && startMeta.isTimeUnspecified) && + (!endMeta || endMeta.isTimeUnspecified); + } + // use this leftover object as the selection object + leftovers.range = { + start: startMeta ? startMeta.marker : null, + end: endMeta ? endMeta.marker : null + }; + leftovers.allDay = allDay; + return leftovers; + } + function isDateSpansEqual(span0, span1) { + return rangesEqual(span0.range, span1.range) && + span0.allDay === span1.allDay && + isSpanPropsEqual(span0, span1); + } + // the NON-DATE-RELATED props + function isSpanPropsEqual(span0, span1) { + for (var propName in span1) { + if (propName !== 'range' && propName !== 'allDay') { + if (span0[propName] !== span1[propName]) { + return false; + } + } + } + // are there any props that span0 has that span1 DOESN'T have? + // both have range/allDay, so no need to special-case. + for (var propName in span0) { + if (!(propName in span1)) { + return false; + } + } + return true; + } + function buildDateSpanApi(span, dateEnv) { + return { + start: dateEnv.toDate(span.range.start), + end: dateEnv.toDate(span.range.end), + startStr: dateEnv.formatIso(span.range.start, { omitTime: span.allDay }), + endStr: dateEnv.formatIso(span.range.end, { omitTime: span.allDay }), + allDay: span.allDay + }; + } + function buildDatePointApi(span, dateEnv) { + return { + date: dateEnv.toDate(span.range.start), + dateStr: dateEnv.formatIso(span.range.start, { omitTime: span.allDay }), + allDay: span.allDay + }; + } + function fabricateEventRange(dateSpan, eventUiBases, calendar) { + var def = parseEventDef({ editable: false }, '', // sourceId + dateSpan.allDay, true, // hasEnd + calendar); + return { + def: def, + ui: compileEventUi(def, eventUiBases), + instance: createEventInstance(def.defId, dateSpan.range), + range: dateSpan.range, + isStart: true, + isEnd: true + }; + } + + function compileViewDefs(defaultConfigs, overrideConfigs) { + var hash = {}; + var viewType; + for (viewType in defaultConfigs) { + ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs); + } + for (viewType in overrideConfigs) { + ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs); + } + return hash; + } + function ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs) { + if (hash[viewType]) { + return hash[viewType]; + } + var viewDef = buildViewDef(viewType, hash, defaultConfigs, overrideConfigs); + if (viewDef) { + hash[viewType] = viewDef; + } + return viewDef; + } + function buildViewDef(viewType, hash, defaultConfigs, overrideConfigs) { + var defaultConfig = defaultConfigs[viewType]; + var overrideConfig = overrideConfigs[viewType]; + var queryProp = function (name) { + return (defaultConfig && defaultConfig[name] !== null) ? defaultConfig[name] : + ((overrideConfig && overrideConfig[name] !== null) ? overrideConfig[name] : null); + }; + var theClass = queryProp('class'); + var superType = queryProp('superType'); + if (!superType && theClass) { + superType = + findViewNameBySubclass(theClass, overrideConfigs) || + findViewNameBySubclass(theClass, defaultConfigs); + } + var superDef = null; + if (superType) { + if (superType === viewType) { + throw new Error('Can\'t have a custom view type that references itself'); + } + superDef = ensureViewDef(superType, hash, defaultConfigs, overrideConfigs); + } + if (!theClass && superDef) { + theClass = superDef.class; + } + if (!theClass) { + return null; // don't throw a warning, might be settings for a single-unit view + } + return { + type: viewType, + class: theClass, + defaults: __assign({}, (superDef ? superDef.defaults : {}), (defaultConfig ? defaultConfig.options : {})), + overrides: __assign({}, (superDef ? superDef.overrides : {}), (overrideConfig ? overrideConfig.options : {})) + }; + } + function findViewNameBySubclass(viewSubclass, configs) { + var superProto = Object.getPrototypeOf(viewSubclass.prototype); + for (var viewType in configs) { + var parsed = configs[viewType]; + // need DIRECT subclass, so instanceof won't do it + if (parsed.class && parsed.class.prototype === superProto) { + return viewType; + } + } + return ''; + } + + function parseViewConfigs(inputs) { + return mapHash(inputs, parseViewConfig); + } + var VIEW_DEF_PROPS = { + type: String, + class: null + }; + function parseViewConfig(input) { + if (typeof input === 'function') { + input = { class: input }; + } + var options = {}; + var props = refineProps(input, VIEW_DEF_PROPS, {}, options); + return { + superType: props.type, + class: props.class, + options: options + }; + } + + function buildViewSpecs(defaultInputs, optionsManager) { + var defaultConfigs = parseViewConfigs(defaultInputs); + var overrideConfigs = parseViewConfigs(optionsManager.overrides.views); + var viewDefs = compileViewDefs(defaultConfigs, overrideConfigs); + return mapHash(viewDefs, function (viewDef) { + return buildViewSpec(viewDef, overrideConfigs, optionsManager); + }); + } + function buildViewSpec(viewDef, overrideConfigs, optionsManager) { + var durationInput = viewDef.overrides.duration || + viewDef.defaults.duration || + optionsManager.dynamicOverrides.duration || + optionsManager.overrides.duration; + var duration = null; + var durationUnit = ''; + var singleUnit = ''; + var singleUnitOverrides = {}; + if (durationInput) { + duration = createDuration(durationInput); + if (duration) { // valid? + var denom = greatestDurationDenominator(duration, !getWeeksFromInput(durationInput)); + durationUnit = denom.unit; + if (denom.value === 1) { + singleUnit = durationUnit; + singleUnitOverrides = overrideConfigs[durationUnit] ? overrideConfigs[durationUnit].options : {}; + } + } + } + var queryButtonText = function (options) { + var buttonTextMap = options.buttonText || {}; + var buttonTextKey = viewDef.defaults.buttonTextKey; + if (buttonTextKey != null && buttonTextMap[buttonTextKey] != null) { + return buttonTextMap[buttonTextKey]; + } + if (buttonTextMap[viewDef.type] != null) { + return buttonTextMap[viewDef.type]; + } + if (buttonTextMap[singleUnit] != null) { + return buttonTextMap[singleUnit]; + } + }; + return { + type: viewDef.type, + class: viewDef.class, + duration: duration, + durationUnit: durationUnit, + singleUnit: singleUnit, + options: __assign({}, globalDefaults, viewDef.defaults, optionsManager.dirDefaults, optionsManager.localeDefaults, optionsManager.overrides, singleUnitOverrides, viewDef.overrides, optionsManager.dynamicOverrides), + buttonTextOverride: queryButtonText(optionsManager.dynamicOverrides) || + queryButtonText(optionsManager.overrides) || // constructor-specified buttonText lookup hash takes precedence + viewDef.overrides.buttonText, + buttonTextDefault: queryButtonText(optionsManager.localeDefaults) || + queryButtonText(optionsManager.dirDefaults) || + viewDef.defaults.buttonText || + queryButtonText(globalDefaults) || + viewDef.type // fall back to given view name + }; + } + + var Toolbar = /** @class */ (function (_super) { + __extends(Toolbar, _super); + function Toolbar(extraClassName) { + var _this = _super.call(this) || this; + _this._renderLayout = memoizeRendering(_this.renderLayout, _this.unrenderLayout); + _this._updateTitle = memoizeRendering(_this.updateTitle, null, [_this._renderLayout]); + _this._updateActiveButton = memoizeRendering(_this.updateActiveButton, null, [_this._renderLayout]); + _this._updateToday = memoizeRendering(_this.updateToday, null, [_this._renderLayout]); + _this._updatePrev = memoizeRendering(_this.updatePrev, null, [_this._renderLayout]); + _this._updateNext = memoizeRendering(_this.updateNext, null, [_this._renderLayout]); + _this.el = createElement('div', { className: 'fc-toolbar ' + extraClassName }); + return _this; + } + Toolbar.prototype.destroy = function () { + _super.prototype.destroy.call(this); + this._renderLayout.unrender(); // should unrender everything else + removeElement(this.el); + }; + Toolbar.prototype.render = function (props) { + this._renderLayout(props.layout); + this._updateTitle(props.title); + this._updateActiveButton(props.activeButton); + this._updateToday(props.isTodayEnabled); + this._updatePrev(props.isPrevEnabled); + this._updateNext(props.isNextEnabled); + }; + Toolbar.prototype.renderLayout = function (layout) { + var el = this.el; + this.viewsWithButtons = []; + appendToElement(el, this.renderSection('left', layout.left)); + appendToElement(el, this.renderSection('center', layout.center)); + appendToElement(el, this.renderSection('right', layout.right)); + }; + Toolbar.prototype.unrenderLayout = function () { + this.el.innerHTML = ''; + }; + Toolbar.prototype.renderSection = function (position, buttonStr) { + var _this = this; + var _a = this.context, theme = _a.theme, calendar = _a.calendar; + var optionsManager = calendar.optionsManager; + var viewSpecs = calendar.viewSpecs; + var sectionEl = createElement('div', { className: 'fc-' + position }); + var calendarCustomButtons = optionsManager.computed.customButtons || {}; + var calendarButtonTextOverrides = optionsManager.overrides.buttonText || {}; + var calendarButtonText = optionsManager.computed.buttonText || {}; + if (buttonStr) { + buttonStr.split(' ').forEach(function (buttonGroupStr, i) { + var groupChildren = []; + var isOnlyButtons = true; + var groupEl; + buttonGroupStr.split(',').forEach(function (buttonName, j) { + var customButtonProps; + var viewSpec; + var buttonClick; + var buttonIcon; // only one of these will be set + var buttonText; // " + var buttonInnerHtml; + var buttonClasses; + var buttonEl; + var buttonAriaAttr; + if (buttonName === 'title') { + groupChildren.push(htmlToElement('<h2> </h2>')); // we always want it to take up height + isOnlyButtons = false; + } + else { + if ((customButtonProps = calendarCustomButtons[buttonName])) { + buttonClick = function (ev) { + if (customButtonProps.click) { + customButtonProps.click.call(buttonEl, ev); + } + }; + (buttonIcon = theme.getCustomButtonIconClass(customButtonProps)) || + (buttonIcon = theme.getIconClass(buttonName)) || + (buttonText = customButtonProps.text); + } + else if ((viewSpec = viewSpecs[buttonName])) { + _this.viewsWithButtons.push(buttonName); + buttonClick = function () { + calendar.changeView(buttonName); + }; + (buttonText = viewSpec.buttonTextOverride) || + (buttonIcon = theme.getIconClass(buttonName)) || + (buttonText = viewSpec.buttonTextDefault); + } + else if (calendar[buttonName]) { // a calendar method + buttonClick = function () { + calendar[buttonName](); + }; + (buttonText = calendarButtonTextOverrides[buttonName]) || + (buttonIcon = theme.getIconClass(buttonName)) || + (buttonText = calendarButtonText[buttonName]); + // ^ everything else is considered default + } + if (buttonClick) { + buttonClasses = [ + 'fc-' + buttonName + '-button', + theme.getClass('button') + ]; + if (buttonText) { + buttonInnerHtml = htmlEscape(buttonText); + buttonAriaAttr = ''; + } + else if (buttonIcon) { + buttonInnerHtml = "<span class='" + buttonIcon + "'></span>"; + buttonAriaAttr = ' aria-label="' + buttonName + '"'; + } + buttonEl = htmlToElement(// type="button" so that it doesn't submit a form + '<button type="button" class="' + buttonClasses.join(' ') + '"' + + buttonAriaAttr + + '>' + buttonInnerHtml + '</button>'); + buttonEl.addEventListener('click', buttonClick); + groupChildren.push(buttonEl); + } + } + }); + if (groupChildren.length > 1) { + groupEl = document.createElement('div'); + var buttonGroupClassName = theme.getClass('buttonGroup'); + if (isOnlyButtons && buttonGroupClassName) { + groupEl.classList.add(buttonGroupClassName); + } + appendToElement(groupEl, groupChildren); + sectionEl.appendChild(groupEl); + } + else { + appendToElement(sectionEl, groupChildren); // 1 or 0 children + } + }); + } + return sectionEl; + }; + Toolbar.prototype.updateToday = function (isTodayEnabled) { + this.toggleButtonEnabled('today', isTodayEnabled); + }; + Toolbar.prototype.updatePrev = function (isPrevEnabled) { + this.toggleButtonEnabled('prev', isPrevEnabled); + }; + Toolbar.prototype.updateNext = function (isNextEnabled) { + this.toggleButtonEnabled('next', isNextEnabled); + }; + Toolbar.prototype.updateTitle = function (text) { + findElements(this.el, 'h2').forEach(function (titleEl) { + titleEl.innerText = text; + }); + }; + Toolbar.prototype.updateActiveButton = function (buttonName) { + var theme = this.context.theme; + var className = theme.getClass('buttonActive'); + findElements(this.el, 'button').forEach(function (buttonEl) { + if (buttonName && buttonEl.classList.contains('fc-' + buttonName + '-button')) { + buttonEl.classList.add(className); + } + else { + buttonEl.classList.remove(className); + } + }); + }; + Toolbar.prototype.toggleButtonEnabled = function (buttonName, bool) { + findElements(this.el, '.fc-' + buttonName + '-button').forEach(function (buttonEl) { + buttonEl.disabled = !bool; + }); + }; + return Toolbar; + }(Component)); + + var CalendarComponent = /** @class */ (function (_super) { + __extends(CalendarComponent, _super); + function CalendarComponent(el) { + var _this = _super.call(this) || this; + _this.elClassNames = []; + _this.renderSkeleton = memoizeRendering(_this._renderSkeleton, _this._unrenderSkeleton); + _this.renderToolbars = memoizeRendering(_this._renderToolbars, _this._unrenderToolbars, [_this.renderSkeleton]); + _this.buildComponentContext = memoize(buildComponentContext); + _this.buildViewPropTransformers = memoize(buildViewPropTransformers); + _this.el = el; + _this.computeTitle = memoize(computeTitle); + _this.parseBusinessHours = memoize(function (input) { + return parseBusinessHours(input, _this.context.calendar); + }); + return _this; + } + CalendarComponent.prototype.render = function (props, context) { + this.freezeHeight(); + var title = this.computeTitle(props.dateProfile, props.viewSpec.options); + this.renderSkeleton(context); + this.renderToolbars(props.viewSpec, props.dateProfile, props.currentDate, title); + this.renderView(props, title); + this.updateSize(); + this.thawHeight(); + }; + CalendarComponent.prototype.destroy = function () { + if (this.header) { + this.header.destroy(); + } + if (this.footer) { + this.footer.destroy(); + } + this.renderSkeleton.unrender(); // will call destroyView + _super.prototype.destroy.call(this); + }; + CalendarComponent.prototype._renderSkeleton = function (context) { + this.updateElClassNames(context); + prependToElement(this.el, this.contentEl = createElement('div', { className: 'fc-view-container' })); + var calendar = context.calendar; + for (var _i = 0, _a = calendar.pluginSystem.hooks.viewContainerModifiers; _i < _a.length; _i++) { + var modifyViewContainer = _a[_i]; + modifyViewContainer(this.contentEl, calendar); + } + }; + CalendarComponent.prototype._unrenderSkeleton = function () { + // weird to have this here + if (this.view) { + this.savedScroll = this.view.queryScroll(); + this.view.destroy(); + this.view = null; + } + removeElement(this.contentEl); + this.removeElClassNames(); + }; + CalendarComponent.prototype.removeElClassNames = function () { + var classList = this.el.classList; + for (var _i = 0, _a = this.elClassNames; _i < _a.length; _i++) { + var className = _a[_i]; + classList.remove(className); + } + this.elClassNames = []; + }; + CalendarComponent.prototype.updateElClassNames = function (context) { + this.removeElClassNames(); + var theme = context.theme, options = context.options; + this.elClassNames = [ + 'fc', + 'fc-' + options.dir, + theme.getClass('widget') + ]; + var classList = this.el.classList; + for (var _i = 0, _a = this.elClassNames; _i < _a.length; _i++) { + var className = _a[_i]; + classList.add(className); + } + }; + CalendarComponent.prototype._renderToolbars = function (viewSpec, dateProfile, currentDate, title) { + var _a = this, context = _a.context, header = _a.header, footer = _a.footer; + var options = context.options, calendar = context.calendar; + var headerLayout = options.header; + var footerLayout = options.footer; + var dateProfileGenerator = this.props.dateProfileGenerator; + var now = calendar.getNow(); + var todayInfo = dateProfileGenerator.build(now); + var prevInfo = dateProfileGenerator.buildPrev(dateProfile, currentDate); + var nextInfo = dateProfileGenerator.buildNext(dateProfile, currentDate); + var toolbarProps = { + title: title, + activeButton: viewSpec.type, + isTodayEnabled: todayInfo.isValid && !rangeContainsMarker(dateProfile.currentRange, now), + isPrevEnabled: prevInfo.isValid, + isNextEnabled: nextInfo.isValid + }; + if (headerLayout) { + if (!header) { + header = this.header = new Toolbar('fc-header-toolbar'); + prependToElement(this.el, header.el); + } + header.receiveProps(__assign({ layout: headerLayout }, toolbarProps), context); + } + else if (header) { + header.destroy(); + header = this.header = null; + } + if (footerLayout) { + if (!footer) { + footer = this.footer = new Toolbar('fc-footer-toolbar'); + appendToElement(this.el, footer.el); + } + footer.receiveProps(__assign({ layout: footerLayout }, toolbarProps), context); + } + else if (footer) { + footer.destroy(); + footer = this.footer = null; + } + }; + CalendarComponent.prototype._unrenderToolbars = function () { + if (this.header) { + this.header.destroy(); + this.header = null; + } + if (this.footer) { + this.footer.destroy(); + this.footer = null; + } + }; + CalendarComponent.prototype.renderView = function (props, title) { + var view = this.view; + var _a = this.context, calendar = _a.calendar, options = _a.options; + var viewSpec = props.viewSpec, dateProfileGenerator = props.dateProfileGenerator; + if (!view || view.viewSpec !== viewSpec) { + if (view) { + view.destroy(); + } + view = this.view = new viewSpec['class'](viewSpec, this.contentEl); + if (this.savedScroll) { + view.addScroll(this.savedScroll, true); + this.savedScroll = null; + } + } + view.title = title; // for the API + var viewProps = { + dateProfileGenerator: dateProfileGenerator, + dateProfile: props.dateProfile, + businessHours: this.parseBusinessHours(viewSpec.options.businessHours), + eventStore: props.eventStore, + eventUiBases: props.eventUiBases, + dateSelection: props.dateSelection, + eventSelection: props.eventSelection, + eventDrag: props.eventDrag, + eventResize: props.eventResize + }; + var transformers = this.buildViewPropTransformers(calendar.pluginSystem.hooks.viewPropsTransformers); + for (var _i = 0, transformers_1 = transformers; _i < transformers_1.length; _i++) { + var transformer = transformers_1[_i]; + __assign(viewProps, transformer.transform(viewProps, viewSpec, props, options)); + } + view.receiveProps(viewProps, this.buildComponentContext(this.context, viewSpec, view)); + }; + // Sizing + // ----------------------------------------------------------------------------------------------------------------- + CalendarComponent.prototype.updateSize = function (isResize) { + if (isResize === void 0) { isResize = false; } + var view = this.view; + if (!view) { + return; // why? + } + if (isResize || this.isHeightAuto == null) { + this.computeHeightVars(); + } + view.updateSize(isResize, this.viewHeight, this.isHeightAuto); + view.updateNowIndicator(); // we need to guarantee this will run after updateSize + view.popScroll(isResize); + }; + CalendarComponent.prototype.computeHeightVars = function () { + var calendar = this.context.calendar; // yuck. need to handle dynamic options + var heightInput = calendar.opt('height'); + var contentHeightInput = calendar.opt('contentHeight'); + this.isHeightAuto = heightInput === 'auto' || contentHeightInput === 'auto'; + if (typeof contentHeightInput === 'number') { // exists and not 'auto' + this.viewHeight = contentHeightInput; + } + else if (typeof contentHeightInput === 'function') { // exists and is a function + this.viewHeight = contentHeightInput(); + } + else if (typeof heightInput === 'number') { // exists and not 'auto' + this.viewHeight = heightInput - this.queryToolbarsHeight(); + } + else if (typeof heightInput === 'function') { // exists and is a function + this.viewHeight = heightInput() - this.queryToolbarsHeight(); + } + else if (heightInput === 'parent') { // set to height of parent element + var parentEl = this.el.parentNode; + this.viewHeight = parentEl.getBoundingClientRect().height - this.queryToolbarsHeight(); + } + else { + this.viewHeight = Math.round(this.contentEl.getBoundingClientRect().width / + Math.max(calendar.opt('aspectRatio'), .5)); + } + }; + CalendarComponent.prototype.queryToolbarsHeight = function () { + var height = 0; + if (this.header) { + height += computeHeightAndMargins(this.header.el); + } + if (this.footer) { + height += computeHeightAndMargins(this.footer.el); + } + return height; + }; + // Height "Freezing" + // ----------------------------------------------------------------------------------------------------------------- + CalendarComponent.prototype.freezeHeight = function () { + applyStyle(this.el, { + height: this.el.getBoundingClientRect().height, + overflow: 'hidden' + }); + }; + CalendarComponent.prototype.thawHeight = function () { + applyStyle(this.el, { + height: '', + overflow: '' + }); + }; + return CalendarComponent; + }(Component)); + // Title and Date Formatting + // ----------------------------------------------------------------------------------------------------------------- + // Computes what the title at the top of the calendar should be for this view + function computeTitle(dateProfile, viewOptions) { + var range; + // for views that span a large unit of time, show the proper interval, ignoring stray days before and after + if (/^(year|month)$/.test(dateProfile.currentRangeUnit)) { + range = dateProfile.currentRange; + } + else { // for day units or smaller, use the actual day range + range = dateProfile.activeRange; + } + return this.context.dateEnv.formatRange(range.start, range.end, createFormatter(viewOptions.titleFormat || computeTitleFormat(dateProfile), viewOptions.titleRangeSeparator), { isEndExclusive: dateProfile.isRangeAllDay }); + } + // 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`. + function computeTitleFormat(dateProfile) { + var currentRangeUnit = dateProfile.currentRangeUnit; + if (currentRangeUnit === 'year') { + return { year: 'numeric' }; + } + else if (currentRangeUnit === 'month') { + return { year: 'numeric', month: 'long' }; // like "September 2014" + } + else { + var days = diffWholeDays(dateProfile.currentRange.start, dateProfile.currentRange.end); + if (days !== null && days > 1) { + // multi-day range. shorter, like "Sep 9 - 10 2014" + return { year: 'numeric', month: 'short', day: 'numeric' }; + } + else { + // one day. longer, like "September 9 2014" + return { year: 'numeric', month: 'long', day: 'numeric' }; + } + } + } + // build a context scoped to the view + function buildComponentContext(context, viewSpec, view) { + return context.extend(viewSpec.options, view); + } + // Plugin + // ----------------------------------------------------------------------------------------------------------------- + function buildViewPropTransformers(theClasses) { + return theClasses.map(function (theClass) { + return new theClass(); + }); + } + + var Interaction = /** @class */ (function () { + function Interaction(settings) { + this.component = settings.component; + } + Interaction.prototype.destroy = function () { + }; + return Interaction; + }()); + function parseInteractionSettings(component, input) { + return { + component: component, + el: input.el, + useEventCenter: input.useEventCenter != null ? input.useEventCenter : true + }; + } + function interactionSettingsToStore(settings) { + var _a; + return _a = {}, + _a[settings.component.uid] = settings, + _a; + } + // global state + var interactionSettingsStore = {}; + + /* + Detects when the user clicks on an event within a DateComponent + */ + var EventClicking = /** @class */ (function (_super) { + __extends(EventClicking, _super); + function EventClicking(settings) { + var _this = _super.call(this, settings) || this; + _this.handleSegClick = function (ev, segEl) { + var component = _this.component; + var _a = component.context, calendar = _a.calendar, view = _a.view; + var seg = getElSeg(segEl); + if (seg && // might be the <div> surrounding the more link + component.isValidSegDownEl(ev.target)) { + // our way to simulate a link click for elements that can't be <a> tags + // grab before trigger fired in case trigger trashes DOM thru rerendering + var hasUrlContainer = elementClosest(ev.target, '.fc-has-url'); + var url = hasUrlContainer ? hasUrlContainer.querySelector('a[href]').href : ''; + calendar.publiclyTrigger('eventClick', [ + { + el: segEl, + event: new EventApi(component.context.calendar, seg.eventRange.def, seg.eventRange.instance), + jsEvent: ev, + view: view + } + ]); + if (url && !ev.defaultPrevented) { + window.location.href = url; + } + } + }; + var component = settings.component; + _this.destroy = listenBySelector(component.el, 'click', component.fgSegSelector + ',' + component.bgSegSelector, _this.handleSegClick); + return _this; + } + return EventClicking; + }(Interaction)); + + /* + Triggers events and adds/removes core classNames when the user's pointer + enters/leaves event-elements of a component. + */ + var EventHovering = /** @class */ (function (_super) { + __extends(EventHovering, _super); + function EventHovering(settings) { + var _this = _super.call(this, settings) || this; + // for simulating an eventMouseLeave when the event el is destroyed while mouse is over it + _this.handleEventElRemove = function (el) { + if (el === _this.currentSegEl) { + _this.handleSegLeave(null, _this.currentSegEl); + } + }; + _this.handleSegEnter = function (ev, segEl) { + if (getElSeg(segEl)) { // TODO: better way to make sure not hovering over more+ link or its wrapper + segEl.classList.add('fc-allow-mouse-resize'); + _this.currentSegEl = segEl; + _this.triggerEvent('eventMouseEnter', ev, segEl); + } + }; + _this.handleSegLeave = function (ev, segEl) { + if (_this.currentSegEl) { + segEl.classList.remove('fc-allow-mouse-resize'); + _this.currentSegEl = null; + _this.triggerEvent('eventMouseLeave', ev, segEl); + } + }; + var component = settings.component; + _this.removeHoverListeners = listenToHoverBySelector(component.el, component.fgSegSelector + ',' + component.bgSegSelector, _this.handleSegEnter, _this.handleSegLeave); + // how to make sure component already has context? + component.context.calendar.on('eventElRemove', _this.handleEventElRemove); + return _this; + } + EventHovering.prototype.destroy = function () { + this.removeHoverListeners(); + this.component.context.calendar.off('eventElRemove', this.handleEventElRemove); + }; + EventHovering.prototype.triggerEvent = function (publicEvName, ev, segEl) { + var component = this.component; + var _a = component.context, calendar = _a.calendar, view = _a.view; + var seg = getElSeg(segEl); + if (!ev || component.isValidSegDownEl(ev.target)) { + calendar.publiclyTrigger(publicEvName, [ + { + el: segEl, + event: new EventApi(calendar, seg.eventRange.def, seg.eventRange.instance), + jsEvent: ev, + view: view + } + ]); + } + }; + return EventHovering; + }(Interaction)); + + var StandardTheme = /** @class */ (function (_super) { + __extends(StandardTheme, _super); + function StandardTheme() { + return _super !== null && _super.apply(this, arguments) || this; + } + return StandardTheme; + }(Theme)); + StandardTheme.prototype.classes = { + widget: 'fc-unthemed', + widgetHeader: 'fc-widget-header', + widgetContent: 'fc-widget-content', + buttonGroup: 'fc-button-group', + button: 'fc-button fc-button-primary', + buttonActive: 'fc-button-active', + popoverHeader: 'fc-widget-header', + popoverContent: 'fc-widget-content', + // day grid + headerRow: 'fc-widget-header', + dayRow: 'fc-widget-content', + // list view + listView: 'fc-widget-content' + }; + StandardTheme.prototype.baseIconClass = 'fc-icon'; + StandardTheme.prototype.iconClasses = { + close: 'fc-icon-x', + prev: 'fc-icon-chevron-left', + next: 'fc-icon-chevron-right', + prevYear: 'fc-icon-chevrons-left', + nextYear: 'fc-icon-chevrons-right' + }; + StandardTheme.prototype.iconOverrideOption = 'buttonIcons'; + StandardTheme.prototype.iconOverrideCustomButtonOption = 'icon'; + StandardTheme.prototype.iconOverridePrefix = 'fc-icon-'; + + var Calendar = /** @class */ (function () { + function Calendar(el, overrides) { + var _this = this; + this.buildComponentContext = memoize(buildComponentContext$1); + this.parseRawLocales = memoize(parseRawLocales); + this.buildLocale = memoize(buildLocale); + this.buildDateEnv = memoize(buildDateEnv); + this.buildTheme = memoize(buildTheme); + this.buildEventUiSingleBase = memoize(this._buildEventUiSingleBase); + this.buildSelectionConfig = memoize(this._buildSelectionConfig); + this.buildEventUiBySource = memoizeOutput(buildEventUiBySource, isPropsEqual); + this.buildEventUiBases = memoize(buildEventUiBases); + this.interactionsStore = {}; + this.actionQueue = []; + this.isReducing = false; + // isDisplaying: boolean = false // installed in DOM? accepting renders? + this.needsRerender = false; // needs a render? + this.isRendering = false; // currently in the executeRender function? + this.renderingPauseDepth = 0; + this.buildDelayedRerender = memoize(buildDelayedRerender); + this.afterSizingTriggers = {}; + this.isViewUpdated = false; + this.isDatesUpdated = false; + this.isEventsUpdated = false; + this.el = el; + this.optionsManager = new OptionsManager(overrides || {}); + this.pluginSystem = new PluginSystem(); + // only do once. don't do in handleOptions. because can't remove plugins + this.addPluginInputs(this.optionsManager.computed.plugins || []); + this.handleOptions(this.optionsManager.computed); + this.publiclyTrigger('_init'); // for tests + this.hydrate(); + this.calendarInteractions = this.pluginSystem.hooks.calendarInteractions + .map(function (calendarInteractionClass) { + return new calendarInteractionClass(_this); + }); + } + Calendar.prototype.addPluginInputs = function (pluginInputs) { + var pluginDefs = refinePluginDefs(pluginInputs); + for (var _i = 0, pluginDefs_1 = pluginDefs; _i < pluginDefs_1.length; _i++) { + var pluginDef = pluginDefs_1[_i]; + this.pluginSystem.add(pluginDef); + } + }; + Object.defineProperty(Calendar.prototype, "view", { + // public API + get: function () { + return this.component ? this.component.view : null; + }, + enumerable: true, + configurable: true + }); + // Public API for rendering + // ----------------------------------------------------------------------------------------------------------------- + Calendar.prototype.render = function () { + if (!this.component) { + this.component = new CalendarComponent(this.el); + this.renderableEventStore = createEmptyEventStore(); + this.bindHandlers(); + this.executeRender(); + } + else { + this.requestRerender(); + } + }; + Calendar.prototype.destroy = function () { + if (this.component) { + this.unbindHandlers(); + this.component.destroy(); // don't null-out. in case API needs access + this.component = null; // umm ??? + for (var _i = 0, _a = this.calendarInteractions; _i < _a.length; _i++) { + var interaction = _a[_i]; + interaction.destroy(); + } + this.publiclyTrigger('_destroyed'); + } + }; + // Handlers + // ----------------------------------------------------------------------------------------------------------------- + Calendar.prototype.bindHandlers = function () { + var _this = this; + // event delegation for nav links + this.removeNavLinkListener = listenBySelector(this.el, 'click', 'a[data-goto]', function (ev, anchorEl) { + var gotoOptions = anchorEl.getAttribute('data-goto'); + gotoOptions = gotoOptions ? JSON.parse(gotoOptions) : {}; + var dateEnv = _this.dateEnv; + var dateMarker = dateEnv.createMarker(gotoOptions.date); + var viewType = gotoOptions.type; + // property like "navLinkDayClick". might be a string or a function + var customAction = _this.viewOpt('navLink' + capitaliseFirstLetter(viewType) + 'Click'); + if (typeof customAction === 'function') { + customAction(dateEnv.toDate(dateMarker), ev); + } + else { + if (typeof customAction === 'string') { + viewType = customAction; + } + _this.zoomTo(dateMarker, viewType); + } + }); + if (this.opt('handleWindowResize')) { + window.addEventListener('resize', this.windowResizeProxy = debounce(// prevents rapid calls + this.windowResize.bind(this), this.opt('windowResizeDelay'))); + } + }; + Calendar.prototype.unbindHandlers = function () { + this.removeNavLinkListener(); + if (this.windowResizeProxy) { + window.removeEventListener('resize', this.windowResizeProxy); + this.windowResizeProxy = null; + } + }; + // Dispatcher + // ----------------------------------------------------------------------------------------------------------------- + Calendar.prototype.hydrate = function () { + var _this = this; + this.state = this.buildInitialState(); + var rawSources = this.opt('eventSources') || []; + var singleRawSource = this.opt('events'); + var sources = []; // parsed + if (singleRawSource) { + rawSources.unshift(singleRawSource); + } + for (var _i = 0, rawSources_1 = rawSources; _i < rawSources_1.length; _i++) { + var rawSource = rawSources_1[_i]; + var source = parseEventSource(rawSource, this); + if (source) { + sources.push(source); + } + } + this.batchRendering(function () { + _this.dispatch({ type: 'INIT' }); // pass in sources here? + _this.dispatch({ type: 'ADD_EVENT_SOURCES', sources: sources }); + _this.dispatch({ + type: 'SET_VIEW_TYPE', + viewType: _this.opt('defaultView') || _this.pluginSystem.hooks.defaultView + }); + }); + }; + Calendar.prototype.buildInitialState = function () { + return { + viewType: null, + loadingLevel: 0, + eventSourceLoadingLevel: 0, + currentDate: this.getInitialDate(), + dateProfile: null, + eventSources: {}, + eventStore: createEmptyEventStore(), + dateSelection: null, + eventSelection: '', + eventDrag: null, + eventResize: null + }; + }; + Calendar.prototype.dispatch = function (action) { + this.actionQueue.push(action); + if (!this.isReducing) { + this.isReducing = true; + var oldState = this.state; + while (this.actionQueue.length) { + this.state = this.reduce(this.state, this.actionQueue.shift(), this); + } + var newState = this.state; + this.isReducing = false; + if (!oldState.loadingLevel && newState.loadingLevel) { + this.publiclyTrigger('loading', [true]); + } + else if (oldState.loadingLevel && !newState.loadingLevel) { + this.publiclyTrigger('loading', [false]); + } + var view = this.component && this.component.view; + if (oldState.eventStore !== newState.eventStore) { + if (oldState.eventStore) { + this.isEventsUpdated = true; + } + } + if (oldState.dateProfile !== newState.dateProfile) { + if (oldState.dateProfile && view) { // why would view be null!? + this.publiclyTrigger('datesDestroy', [ + { + view: view, + el: view.el + } + ]); + } + this.isDatesUpdated = true; + } + if (oldState.viewType !== newState.viewType) { + if (oldState.viewType && view) { // why would view be null!? + this.publiclyTrigger('viewSkeletonDestroy', [ + { + view: view, + el: view.el + } + ]); + } + this.isViewUpdated = true; + } + this.requestRerender(); + } + }; + Calendar.prototype.reduce = function (state, action, calendar) { + return reduce(state, action, calendar); + }; + // Render Queue + // ----------------------------------------------------------------------------------------------------------------- + Calendar.prototype.requestRerender = function () { + this.needsRerender = true; + this.delayedRerender(); // will call a debounced-version of tryRerender + }; + Calendar.prototype.tryRerender = function () { + if (this.component && // must be accepting renders + this.needsRerender && // indicates that a rerender was requested + !this.renderingPauseDepth && // not paused + !this.isRendering // not currently in the render loop + ) { + this.executeRender(); + } + }; + Calendar.prototype.batchRendering = function (func) { + this.renderingPauseDepth++; + func(); + this.renderingPauseDepth--; + if (this.needsRerender) { + this.requestRerender(); + } + }; + // Rendering + // ----------------------------------------------------------------------------------------------------------------- + Calendar.prototype.executeRender = function () { + // clear these BEFORE the render so that new values will accumulate during render + this.needsRerender = false; + this.isRendering = true; + this.renderComponent(); + this.isRendering = false; + // received a rerender request while rendering + if (this.needsRerender) { + this.delayedRerender(); + } + }; + /* + don't call this directly. use executeRender instead + */ + Calendar.prototype.renderComponent = function () { + var _a = this, state = _a.state, component = _a.component; + var viewType = state.viewType; + var viewSpec = this.viewSpecs[viewType]; + if (!viewSpec) { + throw new Error("View type \"" + viewType + "\" is not valid"); + } + // if event sources are still loading and progressive rendering hasn't been enabled, + // keep rendering the last fully loaded set of events + var renderableEventStore = this.renderableEventStore = + (state.eventSourceLoadingLevel && !this.opt('progressiveEventRendering')) ? + this.renderableEventStore : + state.eventStore; + var eventUiSingleBase = this.buildEventUiSingleBase(viewSpec.options); + var eventUiBySource = this.buildEventUiBySource(state.eventSources); + var eventUiBases = this.eventUiBases = this.buildEventUiBases(renderableEventStore.defs, eventUiSingleBase, eventUiBySource); + component.receiveProps(__assign({}, state, { viewSpec: viewSpec, dateProfileGenerator: this.dateProfileGenerators[viewType], dateProfile: state.dateProfile, eventStore: renderableEventStore, eventUiBases: eventUiBases, dateSelection: state.dateSelection, eventSelection: state.eventSelection, eventDrag: state.eventDrag, eventResize: state.eventResize }), this.buildComponentContext(this.theme, this.dateEnv, this.optionsManager.computed)); + if (this.isViewUpdated) { + this.isViewUpdated = false; + this.publiclyTrigger('viewSkeletonRender', [ + { + view: component.view, + el: component.view.el + } + ]); + } + if (this.isDatesUpdated) { + this.isDatesUpdated = false; + this.publiclyTrigger('datesRender', [ + { + view: component.view, + el: component.view.el + } + ]); + } + if (this.isEventsUpdated) { + this.isEventsUpdated = false; + } + this.releaseAfterSizingTriggers(); + }; + // Options + // ----------------------------------------------------------------------------------------------------------------- + Calendar.prototype.setOption = function (name, val) { + var _a; + this.mutateOptions((_a = {}, _a[name] = val, _a), [], true); + }; + Calendar.prototype.getOption = function (name) { + return this.optionsManager.computed[name]; + }; + Calendar.prototype.opt = function (name) { + return this.optionsManager.computed[name]; + }; + Calendar.prototype.viewOpt = function (name) { + return this.viewOpts()[name]; + }; + Calendar.prototype.viewOpts = function () { + return this.viewSpecs[this.state.viewType].options; + }; + /* + handles option changes (like a diff) + */ + Calendar.prototype.mutateOptions = function (updates, removals, isDynamic, deepEqual) { + var _this = this; + var changeHandlers = this.pluginSystem.hooks.optionChangeHandlers; + var normalUpdates = {}; + var specialUpdates = {}; + var oldDateEnv = this.dateEnv; // do this before handleOptions + var isTimeZoneDirty = false; + var isSizeDirty = false; + var anyDifficultOptions = Boolean(removals.length); + for (var name_1 in updates) { + if (changeHandlers[name_1]) { + specialUpdates[name_1] = updates[name_1]; + } + else { + normalUpdates[name_1] = updates[name_1]; + } + } + for (var name_2 in normalUpdates) { + if (/^(height|contentHeight|aspectRatio)$/.test(name_2)) { + isSizeDirty = true; + } + else if (/^(defaultDate|defaultView)$/.test(name_2)) ; + else { + anyDifficultOptions = true; + if (name_2 === 'timeZone') { + isTimeZoneDirty = true; + } + } + } + this.optionsManager.mutate(normalUpdates, removals, isDynamic); + if (anyDifficultOptions) { + this.handleOptions(this.optionsManager.computed); + } + this.batchRendering(function () { + if (anyDifficultOptions) { + if (isTimeZoneDirty) { + _this.dispatch({ + type: 'CHANGE_TIMEZONE', + oldDateEnv: oldDateEnv + }); + } + /* HACK + has the same effect as calling this.requestRerender() + but recomputes the state's dateProfile + */ + _this.dispatch({ + type: 'SET_VIEW_TYPE', + viewType: _this.state.viewType + }); + } + else if (isSizeDirty) { + _this.updateSize(); + } + // special updates + if (deepEqual) { + for (var name_3 in specialUpdates) { + changeHandlers[name_3](specialUpdates[name_3], _this, deepEqual); + } + } + }); + }; + /* + rebuilds things based off of a complete set of refined options + */ + Calendar.prototype.handleOptions = function (options) { + var _this = this; + var pluginHooks = this.pluginSystem.hooks; + this.defaultAllDayEventDuration = createDuration(options.defaultAllDayEventDuration); + this.defaultTimedEventDuration = createDuration(options.defaultTimedEventDuration); + this.delayedRerender = this.buildDelayedRerender(options.rerenderDelay); + this.theme = this.buildTheme(options); + var available = this.parseRawLocales(options.locales); + this.availableRawLocales = available.map; + var locale = this.buildLocale(options.locale || available.defaultCode, available.map); + this.dateEnv = this.buildDateEnv(locale, options.timeZone, pluginHooks.namedTimeZonedImpl, options.firstDay, options.weekNumberCalculation, options.weekLabel, pluginHooks.cmdFormatter); + this.selectionConfig = this.buildSelectionConfig(options); // needs dateEnv. do after :( + // ineffecient to do every time? + this.viewSpecs = buildViewSpecs(pluginHooks.views, this.optionsManager); + // ineffecient to do every time? + this.dateProfileGenerators = mapHash(this.viewSpecs, function (viewSpec) { + return new viewSpec.class.prototype.dateProfileGeneratorClass(viewSpec, _this); + }); + }; + Calendar.prototype.getAvailableLocaleCodes = function () { + return Object.keys(this.availableRawLocales); + }; + Calendar.prototype._buildSelectionConfig = function (rawOpts) { + return processScopedUiProps('select', rawOpts, this); + }; + Calendar.prototype._buildEventUiSingleBase = function (rawOpts) { + if (rawOpts.editable) { // so 'editable' affected events + rawOpts = __assign({}, rawOpts, { eventEditable: true }); + } + return processScopedUiProps('event', rawOpts, this); + }; + // Trigger + // ----------------------------------------------------------------------------------------------------------------- + Calendar.prototype.hasPublicHandlers = function (name) { + return this.hasHandlers(name) || + this.opt(name); // handler specified in options + }; + Calendar.prototype.publiclyTrigger = function (name, args) { + var optHandler = this.opt(name); + this.triggerWith(name, this, args); + if (optHandler) { + return optHandler.apply(this, args); + } + }; + Calendar.prototype.publiclyTriggerAfterSizing = function (name, args) { + var afterSizingTriggers = this.afterSizingTriggers; + (afterSizingTriggers[name] || (afterSizingTriggers[name] = [])).push(args); + }; + Calendar.prototype.releaseAfterSizingTriggers = function () { + var afterSizingTriggers = this.afterSizingTriggers; + for (var name_4 in afterSizingTriggers) { + for (var _i = 0, _a = afterSizingTriggers[name_4]; _i < _a.length; _i++) { + var args = _a[_i]; + this.publiclyTrigger(name_4, args); + } + } + this.afterSizingTriggers = {}; + }; + // View + // ----------------------------------------------------------------------------------------------------------------- + // Returns a boolean about whether the view is okay to instantiate at some point + Calendar.prototype.isValidViewType = function (viewType) { + return Boolean(this.viewSpecs[viewType]); + }; + Calendar.prototype.changeView = function (viewType, dateOrRange) { + var dateMarker = null; + if (dateOrRange) { + if (dateOrRange.start && dateOrRange.end) { // a range + this.optionsManager.mutate({ visibleRange: dateOrRange }, []); // will not rerender + this.handleOptions(this.optionsManager.computed); // ...but yuck + } + else { // a date + dateMarker = this.dateEnv.createMarker(dateOrRange); // just like gotoDate + } + } + this.unselect(); + this.dispatch({ + type: 'SET_VIEW_TYPE', + viewType: viewType, + dateMarker: dateMarker + }); + }; + // Forces navigation to a view for the given date. + // `viewType` can be a specific view name or a generic one like "week" or "day". + // needs to change + Calendar.prototype.zoomTo = function (dateMarker, viewType) { + var spec; + viewType = viewType || 'day'; // day is default zoom + spec = this.viewSpecs[viewType] || + this.getUnitViewSpec(viewType); + this.unselect(); + if (spec) { + this.dispatch({ + type: 'SET_VIEW_TYPE', + viewType: spec.type, + dateMarker: dateMarker + }); + } + else { + this.dispatch({ + type: 'SET_DATE', + dateMarker: dateMarker + }); + } + }; + // Given a duration singular unit, like "week" or "day", finds a matching view spec. + // Preference is given to views that have corresponding buttons. + Calendar.prototype.getUnitViewSpec = function (unit) { + var component = this.component; + var viewTypes = []; + var i; + var spec; + // put views that have buttons first. there will be duplicates, but oh + if (component.header) { + viewTypes.push.apply(viewTypes, component.header.viewsWithButtons); + } + if (component.footer) { + viewTypes.push.apply(viewTypes, component.footer.viewsWithButtons); + } + for (var viewType in this.viewSpecs) { + viewTypes.push(viewType); + } + for (i = 0; i < viewTypes.length; i++) { + spec = this.viewSpecs[viewTypes[i]]; + if (spec) { + if (spec.singleUnit === unit) { + return spec; + } + } + } + }; + // Current Date + // ----------------------------------------------------------------------------------------------------------------- + Calendar.prototype.getInitialDate = function () { + var defaultDateInput = this.opt('defaultDate'); + // compute the initial ambig-timezone date + if (defaultDateInput != null) { + return this.dateEnv.createMarker(defaultDateInput); + } + else { + return this.getNow(); // getNow already returns unzoned + } + }; + Calendar.prototype.prev = function () { + this.unselect(); + this.dispatch({ type: 'PREV' }); + }; + Calendar.prototype.next = function () { + this.unselect(); + this.dispatch({ type: 'NEXT' }); + }; + Calendar.prototype.prevYear = function () { + this.unselect(); + this.dispatch({ + type: 'SET_DATE', + dateMarker: this.dateEnv.addYears(this.state.currentDate, -1) + }); + }; + Calendar.prototype.nextYear = function () { + this.unselect(); + this.dispatch({ + type: 'SET_DATE', + dateMarker: this.dateEnv.addYears(this.state.currentDate, 1) + }); + }; + Calendar.prototype.today = function () { + this.unselect(); + this.dispatch({ + type: 'SET_DATE', + dateMarker: this.getNow() + }); + }; + Calendar.prototype.gotoDate = function (zonedDateInput) { + this.unselect(); + this.dispatch({ + type: 'SET_DATE', + dateMarker: this.dateEnv.createMarker(zonedDateInput) + }); + }; + Calendar.prototype.incrementDate = function (deltaInput) { + var delta = createDuration(deltaInput); + if (delta) { // else, warn about invalid input? + this.unselect(); + this.dispatch({ + type: 'SET_DATE', + dateMarker: this.dateEnv.add(this.state.currentDate, delta) + }); + } + }; + // for external API + Calendar.prototype.getDate = function () { + return this.dateEnv.toDate(this.state.currentDate); + }; + // Date Formatting Utils + // ----------------------------------------------------------------------------------------------------------------- + Calendar.prototype.formatDate = function (d, formatter) { + var dateEnv = this.dateEnv; + return dateEnv.format(dateEnv.createMarker(d), createFormatter(formatter)); + }; + // `settings` is for formatter AND isEndExclusive + Calendar.prototype.formatRange = function (d0, d1, settings) { + var dateEnv = this.dateEnv; + return dateEnv.formatRange(dateEnv.createMarker(d0), dateEnv.createMarker(d1), createFormatter(settings, this.opt('defaultRangeSeparator')), settings); + }; + Calendar.prototype.formatIso = function (d, omitTime) { + var dateEnv = this.dateEnv; + return dateEnv.formatIso(dateEnv.createMarker(d), { omitTime: omitTime }); + }; + // Sizing + // ----------------------------------------------------------------------------------------------------------------- + Calendar.prototype.windowResize = function (ev) { + if (!this.isHandlingWindowResize && + this.component && // why? + ev.target === window // not a jqui resize event + ) { + this.isHandlingWindowResize = true; + this.updateSize(); + this.publiclyTrigger('windowResize', [this.view]); + this.isHandlingWindowResize = false; + } + }; + Calendar.prototype.updateSize = function () { + if (this.component) { // when? + this.component.updateSize(true); + } + }; + // Component Registration + // ----------------------------------------------------------------------------------------------------------------- + Calendar.prototype.registerInteractiveComponent = function (component, settingsInput) { + var settings = parseInteractionSettings(component, settingsInput); + var DEFAULT_INTERACTIONS = [ + EventClicking, + EventHovering + ]; + var interactionClasses = DEFAULT_INTERACTIONS.concat(this.pluginSystem.hooks.componentInteractions); + var interactions = interactionClasses.map(function (interactionClass) { + return new interactionClass(settings); + }); + this.interactionsStore[component.uid] = interactions; + interactionSettingsStore[component.uid] = settings; + }; + Calendar.prototype.unregisterInteractiveComponent = function (component) { + for (var _i = 0, _a = this.interactionsStore[component.uid]; _i < _a.length; _i++) { + var listener = _a[_i]; + listener.destroy(); + } + delete this.interactionsStore[component.uid]; + delete interactionSettingsStore[component.uid]; + }; + // Date Selection / Event Selection / DayClick + // ----------------------------------------------------------------------------------------------------------------- + // this public method receives start/end dates in any format, with any timezone + // NOTE: args were changed from v3 + Calendar.prototype.select = function (dateOrObj, endDate) { + var selectionInput; + if (endDate == null) { + if (dateOrObj.start != null) { + selectionInput = dateOrObj; + } + else { + selectionInput = { + start: dateOrObj, + end: null + }; + } + } + else { + selectionInput = { + start: dateOrObj, + end: endDate + }; + } + var selection = parseDateSpan(selectionInput, this.dateEnv, createDuration({ days: 1 }) // TODO: cache this? + ); + if (selection) { // throw parse error otherwise? + this.dispatch({ type: 'SELECT_DATES', selection: selection }); + this.triggerDateSelect(selection); + } + }; + // public method + Calendar.prototype.unselect = function (pev) { + if (this.state.dateSelection) { + this.dispatch({ type: 'UNSELECT_DATES' }); + this.triggerDateUnselect(pev); + } + }; + Calendar.prototype.triggerDateSelect = function (selection, pev) { + var arg = __assign({}, this.buildDateSpanApi(selection), { jsEvent: pev ? pev.origEvent : null, view: this.view }); + this.publiclyTrigger('select', [arg]); + }; + Calendar.prototype.triggerDateUnselect = function (pev) { + this.publiclyTrigger('unselect', [ + { + jsEvent: pev ? pev.origEvent : null, + view: this.view + } + ]); + }; + // TODO: receive pev? + Calendar.prototype.triggerDateClick = function (dateSpan, dayEl, view, ev) { + var arg = __assign({}, this.buildDatePointApi(dateSpan), { dayEl: dayEl, jsEvent: ev, // Is this always a mouse event? See #4655 + view: view }); + this.publiclyTrigger('dateClick', [arg]); + }; + Calendar.prototype.buildDatePointApi = function (dateSpan) { + var props = {}; + for (var _i = 0, _a = this.pluginSystem.hooks.datePointTransforms; _i < _a.length; _i++) { + var transform = _a[_i]; + __assign(props, transform(dateSpan, this)); + } + __assign(props, buildDatePointApi(dateSpan, this.dateEnv)); + return props; + }; + Calendar.prototype.buildDateSpanApi = function (dateSpan) { + var props = {}; + for (var _i = 0, _a = this.pluginSystem.hooks.dateSpanTransforms; _i < _a.length; _i++) { + var transform = _a[_i]; + __assign(props, transform(dateSpan, this)); + } + __assign(props, buildDateSpanApi(dateSpan, this.dateEnv)); + return props; + }; + // Date Utils + // ----------------------------------------------------------------------------------------------------------------- + // Returns a DateMarker for the current date, as defined by the client's computer or from the `now` option + Calendar.prototype.getNow = function () { + var now = this.opt('now'); + if (typeof now === 'function') { + now = now(); + } + if (now == null) { + return this.dateEnv.createNowMarker(); + } + return this.dateEnv.createMarker(now); + }; + // Event-Date Utilities + // ----------------------------------------------------------------------------------------------------------------- + // Given an event's allDay status and start date, return what its fallback end date should be. + // TODO: rename to computeDefaultEventEnd + Calendar.prototype.getDefaultEventEnd = function (allDay, marker) { + var end = marker; + if (allDay) { + end = startOfDay(end); + end = this.dateEnv.add(end, this.defaultAllDayEventDuration); + } + else { + end = this.dateEnv.add(end, this.defaultTimedEventDuration); + } + return end; + }; + // Public Events API + // ----------------------------------------------------------------------------------------------------------------- + Calendar.prototype.addEvent = function (eventInput, sourceInput) { + if (eventInput instanceof EventApi) { + var def = eventInput._def; + var instance = eventInput._instance; + // not already present? don't want to add an old snapshot + if (!this.state.eventStore.defs[def.defId]) { + this.dispatch({ + type: 'ADD_EVENTS', + eventStore: eventTupleToStore({ def: def, instance: instance }) // TODO: better util for two args? + }); + } + return eventInput; + } + var sourceId; + if (sourceInput instanceof EventSourceApi) { + sourceId = sourceInput.internalEventSource.sourceId; + } + else if (sourceInput != null) { + var sourceApi = this.getEventSourceById(sourceInput); // TODO: use an internal function + if (!sourceApi) { + console.warn('Could not find an event source with ID "' + sourceInput + '"'); // TODO: test + return null; + } + else { + sourceId = sourceApi.internalEventSource.sourceId; + } + } + var tuple = parseEvent(eventInput, sourceId, this); + if (tuple) { + this.dispatch({ + type: 'ADD_EVENTS', + eventStore: eventTupleToStore(tuple) + }); + return new EventApi(this, tuple.def, tuple.def.recurringDef ? null : tuple.instance); + } + return null; + }; + // TODO: optimize + Calendar.prototype.getEventById = function (id) { + var _a = this.state.eventStore, defs = _a.defs, instances = _a.instances; + id = String(id); + for (var defId in defs) { + var def = defs[defId]; + if (def.publicId === id) { + if (def.recurringDef) { + return new EventApi(this, def, null); + } + else { + for (var instanceId in instances) { + var instance = instances[instanceId]; + if (instance.defId === def.defId) { + return new EventApi(this, def, instance); + } + } + } + } + } + return null; + }; + Calendar.prototype.getEvents = function () { + var _a = this.state.eventStore, defs = _a.defs, instances = _a.instances; + var eventApis = []; + for (var id in instances) { + var instance = instances[id]; + var def = defs[instance.defId]; + eventApis.push(new EventApi(this, def, instance)); + } + return eventApis; + }; + Calendar.prototype.removeAllEvents = function () { + this.dispatch({ type: 'REMOVE_ALL_EVENTS' }); + }; + Calendar.prototype.rerenderEvents = function () { + this.dispatch({ type: 'RESET_EVENTS' }); + }; + // Public Event Sources API + // ----------------------------------------------------------------------------------------------------------------- + Calendar.prototype.getEventSources = function () { + var sourceHash = this.state.eventSources; + var sourceApis = []; + for (var internalId in sourceHash) { + sourceApis.push(new EventSourceApi(this, sourceHash[internalId])); + } + return sourceApis; + }; + Calendar.prototype.getEventSourceById = function (id) { + var sourceHash = this.state.eventSources; + id = String(id); + for (var sourceId in sourceHash) { + if (sourceHash[sourceId].publicId === id) { + return new EventSourceApi(this, sourceHash[sourceId]); + } + } + return null; + }; + Calendar.prototype.addEventSource = function (sourceInput) { + if (sourceInput instanceof EventSourceApi) { + // not already present? don't want to add an old snapshot + if (!this.state.eventSources[sourceInput.internalEventSource.sourceId]) { + this.dispatch({ + type: 'ADD_EVENT_SOURCES', + sources: [sourceInput.internalEventSource] + }); + } + return sourceInput; + } + var eventSource = parseEventSource(sourceInput, this); + if (eventSource) { // TODO: error otherwise? + this.dispatch({ type: 'ADD_EVENT_SOURCES', sources: [eventSource] }); + return new EventSourceApi(this, eventSource); + } + return null; + }; + Calendar.prototype.removeAllEventSources = function () { + this.dispatch({ type: 'REMOVE_ALL_EVENT_SOURCES' }); + }; + Calendar.prototype.refetchEvents = function () { + this.dispatch({ type: 'FETCH_EVENT_SOURCES' }); + }; + // Scroll + // ----------------------------------------------------------------------------------------------------------------- + Calendar.prototype.scrollToTime = function (timeInput) { + var duration = createDuration(timeInput); + if (duration) { + this.component.view.scrollToDuration(duration); + } + }; + return Calendar; + }()); + EmitterMixin.mixInto(Calendar); + // for memoizers + // ----------------------------------------------------------------------------------------------------------------- + function buildComponentContext$1(theme, dateEnv, options) { + return new ComponentContext(this, theme, dateEnv, options, null); + } + function buildDateEnv(locale, timeZone, namedTimeZoneImpl, firstDay, weekNumberCalculation, weekLabel, cmdFormatter) { + return new DateEnv({ + calendarSystem: 'gregory', + timeZone: timeZone, + namedTimeZoneImpl: namedTimeZoneImpl, + locale: locale, + weekNumberCalculation: weekNumberCalculation, + firstDay: firstDay, + weekLabel: weekLabel, + cmdFormatter: cmdFormatter + }); + } + function buildTheme(calendarOptions) { + var themeClass = this.pluginSystem.hooks.themeClasses[calendarOptions.themeSystem] || StandardTheme; + return new themeClass(calendarOptions); + } + function buildDelayedRerender(wait) { + var func = this.tryRerender.bind(this); + if (wait != null) { + func = debounce(func, wait); + } + return func; + } + function buildEventUiBySource(eventSources) { + return mapHash(eventSources, function (eventSource) { + return eventSource.ui; + }); + } + function buildEventUiBases(eventDefs, eventUiSingleBase, eventUiBySource) { + var eventUiBases = { '': eventUiSingleBase }; + for (var defId in eventDefs) { + var def = eventDefs[defId]; + if (def.sourceId && eventUiBySource[def.sourceId]) { + eventUiBases[defId] = eventUiBySource[def.sourceId]; + } + } + return eventUiBases; + } + + var View = /** @class */ (function (_super) { + __extends(View, _super); + function View(viewSpec, parentEl) { + var _this = _super.call(this, createElement('div', { className: 'fc-view fc-' + viewSpec.type + '-view' })) || this; + _this.renderDatesMem = memoizeRendering(_this.renderDatesWrap, _this.unrenderDatesWrap); + _this.renderBusinessHoursMem = memoizeRendering(_this.renderBusinessHours, _this.unrenderBusinessHours, [_this.renderDatesMem]); + _this.renderDateSelectionMem = memoizeRendering(_this.renderDateSelectionWrap, _this.unrenderDateSelectionWrap, [_this.renderDatesMem]); + _this.renderEventsMem = memoizeRendering(_this.renderEvents, _this.unrenderEvents, [_this.renderDatesMem]); + _this.renderEventSelectionMem = memoizeRendering(_this.renderEventSelectionWrap, _this.unrenderEventSelectionWrap, [_this.renderEventsMem]); + _this.renderEventDragMem = memoizeRendering(_this.renderEventDragWrap, _this.unrenderEventDragWrap, [_this.renderDatesMem]); + _this.renderEventResizeMem = memoizeRendering(_this.renderEventResizeWrap, _this.unrenderEventResizeWrap, [_this.renderDatesMem]); + _this.viewSpec = viewSpec; + _this.type = viewSpec.type; + parentEl.appendChild(_this.el); + _this.initialize(); + return _this; + } + View.prototype.initialize = function () { + }; + Object.defineProperty(View.prototype, "activeStart", { + // Date Setting/Unsetting + // ----------------------------------------------------------------------------------------------------------------- + get: function () { + return this.context.dateEnv.toDate(this.props.dateProfile.activeRange.start); + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(View.prototype, "activeEnd", { + get: function () { + return this.context.dateEnv.toDate(this.props.dateProfile.activeRange.end); + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(View.prototype, "currentStart", { + get: function () { + return this.context.dateEnv.toDate(this.props.dateProfile.currentRange.start); + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(View.prototype, "currentEnd", { + get: function () { + return this.context.dateEnv.toDate(this.props.dateProfile.currentRange.end); + }, + enumerable: true, + configurable: true + }); + // General Rendering + // ----------------------------------------------------------------------------------------------------------------- + View.prototype.render = function (props, context) { + this.renderDatesMem(props.dateProfile); + this.renderBusinessHoursMem(props.businessHours); + this.renderDateSelectionMem(props.dateSelection); + this.renderEventsMem(props.eventStore); + this.renderEventSelectionMem(props.eventSelection); + this.renderEventDragMem(props.eventDrag); + this.renderEventResizeMem(props.eventResize); + }; + View.prototype.beforeUpdate = function () { + this.addScroll(this.queryScroll()); + }; + View.prototype.destroy = function () { + _super.prototype.destroy.call(this); + this.renderDatesMem.unrender(); // should unrender everything else + }; + // Sizing + // ----------------------------------------------------------------------------------------------------------------- + View.prototype.updateSize = function (isResize, viewHeight, isAuto) { + var calendar = this.context.calendar; + if (isResize) { + this.addScroll(this.queryScroll()); // NOTE: same code as in beforeUpdate + } + if (isResize || // HACKS... + calendar.isViewUpdated || + calendar.isDatesUpdated || + calendar.isEventsUpdated) { + // sort of the catch-all sizing + // anything that might cause dimension changes + this.updateBaseSize(isResize, viewHeight, isAuto); + } + // NOTE: popScroll is called by CalendarComponent + }; + View.prototype.updateBaseSize = function (isResize, viewHeight, isAuto) { + }; + // Date Rendering + // ----------------------------------------------------------------------------------------------------------------- + View.prototype.renderDatesWrap = function (dateProfile) { + this.renderDates(dateProfile); + this.addScroll({ + duration: createDuration(this.context.options.scrollTime) + }); + }; + View.prototype.unrenderDatesWrap = function () { + this.stopNowIndicator(); + this.unrenderDates(); + }; + View.prototype.renderDates = function (dateProfile) { }; + View.prototype.unrenderDates = function () { }; + // Business Hours + // ----------------------------------------------------------------------------------------------------------------- + View.prototype.renderBusinessHours = function (businessHours) { }; + View.prototype.unrenderBusinessHours = function () { }; + // Date Selection + // ----------------------------------------------------------------------------------------------------------------- + View.prototype.renderDateSelectionWrap = function (selection) { + if (selection) { + this.renderDateSelection(selection); + } + }; + View.prototype.unrenderDateSelectionWrap = function (selection) { + if (selection) { + this.unrenderDateSelection(selection); + } + }; + View.prototype.renderDateSelection = function (selection) { }; + View.prototype.unrenderDateSelection = function (selection) { }; + // Event Rendering + // ----------------------------------------------------------------------------------------------------------------- + View.prototype.renderEvents = function (eventStore) { }; + View.prototype.unrenderEvents = function () { }; + // util for subclasses + View.prototype.sliceEvents = function (eventStore, allDay) { + var props = this.props; + return sliceEventStore(eventStore, props.eventUiBases, props.dateProfile.activeRange, allDay ? this.context.nextDayThreshold : null).fg; + }; + // Event Selection + // ----------------------------------------------------------------------------------------------------------------- + View.prototype.renderEventSelectionWrap = function (instanceId) { + if (instanceId) { + this.renderEventSelection(instanceId); + } + }; + View.prototype.unrenderEventSelectionWrap = function (instanceId) { + if (instanceId) { + this.unrenderEventSelection(instanceId); + } + }; + View.prototype.renderEventSelection = function (instanceId) { }; + View.prototype.unrenderEventSelection = function (instanceId) { }; + // Event Drag + // ----------------------------------------------------------------------------------------------------------------- + View.prototype.renderEventDragWrap = function (state) { + if (state) { + this.renderEventDrag(state); + } + }; + View.prototype.unrenderEventDragWrap = function (state) { + if (state) { + this.unrenderEventDrag(state); + } + }; + View.prototype.renderEventDrag = function (state) { }; + View.prototype.unrenderEventDrag = function (state) { }; + // Event Resize + // ----------------------------------------------------------------------------------------------------------------- + View.prototype.renderEventResizeWrap = function (state) { + if (state) { + this.renderEventResize(state); + } + }; + View.prototype.unrenderEventResizeWrap = function (state) { + if (state) { + this.unrenderEventResize(state); + } + }; + View.prototype.renderEventResize = function (state) { }; + View.prototype.unrenderEventResize = function (state) { }; + /* Now Indicator + ------------------------------------------------------------------------------------------------------------------*/ + // 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 + // USAGE: must be called manually from subclasses' render methods! don't need to call stopNowIndicator tho + View.prototype.startNowIndicator = function (dateProfile, dateProfileGenerator) { + var _this = this; + var _a = this.context, calendar = _a.calendar, dateEnv = _a.dateEnv, options = _a.options; + var unit; + var update; + var delay; // ms wait value + if (options.nowIndicator && !this.initialNowDate) { + unit = this.getNowIndicatorUnit(dateProfile, dateProfileGenerator); + if (unit) { + update = this.updateNowIndicator.bind(this); + this.initialNowDate = calendar.getNow(); + this.initialNowQueriedMs = new Date().valueOf(); + // wait until the beginning of the next interval + delay = dateEnv.add(dateEnv.startOf(this.initialNowDate, unit), createDuration(1, unit)).valueOf() - this.initialNowDate.valueOf(); + // TODO: maybe always use setTimeout, waiting until start of next unit + this.nowIndicatorTimeoutID = setTimeout(function () { + _this.nowIndicatorTimeoutID = null; + update(); + if (unit === 'second') { + delay = 1000; // every second + } + else { + delay = 1000 * 60; // otherwise, every minute + } + _this.nowIndicatorIntervalID = setInterval(update, delay); // update every interval + }, delay); + } + // rendering will be initiated in updateSize + } + }; + // rerenders the now indicator, computing the new current time from the amount of time that has passed + // since the initial getNow call. + View.prototype.updateNowIndicator = function () { + if (this.props.dateProfile && // a way to determine if dates were rendered yet + this.initialNowDate // activated before? + ) { + this.unrenderNowIndicator(); // won't unrender if unnecessary + this.renderNowIndicator(addMs(this.initialNowDate, new Date().valueOf() - this.initialNowQueriedMs)); + this.isNowIndicatorRendered = true; + } + }; + // Immediately unrenders the view's current time indicator and stops any re-rendering timers. + // Won't cause side effects if indicator isn't rendered. + View.prototype.stopNowIndicator = function () { + if (this.nowIndicatorTimeoutID) { + clearTimeout(this.nowIndicatorTimeoutID); + this.nowIndicatorTimeoutID = null; + } + if (this.nowIndicatorIntervalID) { + clearInterval(this.nowIndicatorIntervalID); + this.nowIndicatorIntervalID = null; + } + if (this.isNowIndicatorRendered) { + this.unrenderNowIndicator(); + this.isNowIndicatorRendered = false; + } + }; + View.prototype.getNowIndicatorUnit = function (dateProfile, dateProfileGenerator) { + // subclasses should implement + }; + // Renders a current time indicator at the given datetime + View.prototype.renderNowIndicator = function (date) { + // SUBCLASSES MUST PASS TO CHILDREN! + }; + // Undoes the rendering actions from renderNowIndicator + View.prototype.unrenderNowIndicator = function () { + // SUBCLASSES MUST PASS TO CHILDREN! + }; + /* Scroller + ------------------------------------------------------------------------------------------------------------------*/ + View.prototype.addScroll = function (scroll, isForced) { + if (isForced) { + scroll.isForced = isForced; + } + __assign(this.queuedScroll || (this.queuedScroll = {}), scroll); + }; + View.prototype.popScroll = function (isResize) { + this.applyQueuedScroll(isResize); + this.queuedScroll = null; + }; + View.prototype.applyQueuedScroll = function (isResize) { + if (this.queuedScroll) { + this.applyScroll(this.queuedScroll, isResize); + } + }; + View.prototype.queryScroll = function () { + var scroll = {}; + if (this.props.dateProfile) { // dates rendered yet? + __assign(scroll, this.queryDateScroll()); + } + return scroll; + }; + View.prototype.applyScroll = function (scroll, isResize) { + var duration = scroll.duration, isForced = scroll.isForced; + if (duration != null && !isForced) { + delete scroll.duration; + if (this.props.dateProfile) { // dates rendered yet? + __assign(scroll, this.computeDateScroll(duration)); + } + } + if (this.props.dateProfile) { // dates rendered yet? + this.applyDateScroll(scroll); + } + }; + View.prototype.computeDateScroll = function (duration) { + return {}; // subclasses must implement + }; + View.prototype.queryDateScroll = function () { + return {}; // subclasses must implement + }; + View.prototype.applyDateScroll = function (scroll) { + // subclasses must implement + }; + // for API + View.prototype.scrollToDuration = function (duration) { + this.applyScroll({ duration: duration }, false); + }; + return View; + }(DateComponent)); + EmitterMixin.mixInto(View); + View.prototype.usesMinMaxTime = false; + View.prototype.dateProfileGeneratorClass = DateProfileGenerator; + + var FgEventRenderer = /** @class */ (function () { + function FgEventRenderer() { + this.segs = []; + this.isSizeDirty = false; + } + FgEventRenderer.prototype.renderSegs = function (context, segs, mirrorInfo) { + this.context = context; + this.rangeUpdated(); // called too frequently :( + // render an `.el` on each seg + // returns a subset of the segs. segs that were actually rendered + segs = this.renderSegEls(segs, mirrorInfo); + this.segs = segs; + this.attachSegs(segs, mirrorInfo); + this.isSizeDirty = true; + triggerRenderedSegs(this.context, this.segs, Boolean(mirrorInfo)); + }; + FgEventRenderer.prototype.unrender = function (context, _segs, mirrorInfo) { + triggerWillRemoveSegs(this.context, this.segs, Boolean(mirrorInfo)); + this.detachSegs(this.segs); + this.segs = []; + }; + // Updates values that rely on options and also relate to range + FgEventRenderer.prototype.rangeUpdated = function () { + var options = this.context.options; + var displayEventTime; + var displayEventEnd; + this.eventTimeFormat = createFormatter(options.eventTimeFormat || this.computeEventTimeFormat(), options.defaultRangeSeparator); + displayEventTime = options.displayEventTime; + if (displayEventTime == null) { + displayEventTime = this.computeDisplayEventTime(); // might be based off of range + } + displayEventEnd = options.displayEventEnd; + if (displayEventEnd == null) { + displayEventEnd = this.computeDisplayEventEnd(); // might be based off of range + } + this.displayEventTime = displayEventTime; + this.displayEventEnd = displayEventEnd; + }; + // Renders and assigns an `el` property for each foreground event segment. + // Only returns segments that successfully rendered. + FgEventRenderer.prototype.renderSegEls = function (segs, mirrorInfo) { + var html = ''; + var i; + 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.renderSegHtml(segs[i], mirrorInfo); + } + // 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. + htmlToElements(html).forEach(function (el, i) { + var seg = segs[i]; + if (el) { + seg.el = el; + } + }); + segs = filterSegsViaEls(this.context, segs, Boolean(mirrorInfo)); + } + return segs; + }; + // Generic utility for generating the HTML classNames for an event segment's element + FgEventRenderer.prototype.getSegClasses = function (seg, isDraggable, isResizable, mirrorInfo) { + var classes = [ + 'fc-event', + seg.isStart ? 'fc-start' : 'fc-not-start', + seg.isEnd ? 'fc-end' : 'fc-not-end' + ].concat(seg.eventRange.ui.classNames); + if (isDraggable) { + classes.push('fc-draggable'); + } + if (isResizable) { + classes.push('fc-resizable'); + } + if (mirrorInfo) { + classes.push('fc-mirror'); + if (mirrorInfo.isDragging) { + classes.push('fc-dragging'); + } + if (mirrorInfo.isResizing) { + classes.push('fc-resizing'); + } + } + return classes; + }; + // 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, formatter will default to the eventTimeFormat setting, + // and displayEnd will default to the displayEventEnd setting. + FgEventRenderer.prototype.getTimeText = function (eventRange, formatter, displayEnd) { + var def = eventRange.def, instance = eventRange.instance; + return this._getTimeText(instance.range.start, def.hasEnd ? instance.range.end : null, def.allDay, formatter, displayEnd, instance.forcedStartTzo, instance.forcedEndTzo); + }; + FgEventRenderer.prototype._getTimeText = function (start, end, allDay, formatter, displayEnd, forcedStartTzo, forcedEndTzo) { + var dateEnv = this.context.dateEnv; + if (formatter == null) { + formatter = this.eventTimeFormat; + } + if (displayEnd == null) { + displayEnd = this.displayEventEnd; + } + if (this.displayEventTime && !allDay) { + if (displayEnd && end) { + return dateEnv.formatRange(start, end, formatter, { + forcedStartTzo: forcedStartTzo, + forcedEndTzo: forcedEndTzo + }); + } + else { + return dateEnv.format(start, formatter, { + forcedTzo: forcedStartTzo + }); + } + } + return ''; + }; + FgEventRenderer.prototype.computeEventTimeFormat = function () { + return { + hour: 'numeric', + minute: '2-digit', + omitZeroMinute: true + }; + }; + FgEventRenderer.prototype.computeDisplayEventTime = function () { + return true; + }; + FgEventRenderer.prototype.computeDisplayEventEnd = function () { + return true; + }; + // Utility for generating event skin-related CSS properties + FgEventRenderer.prototype.getSkinCss = function (ui) { + return { + 'background-color': ui.backgroundColor, + 'border-color': ui.borderColor, + color: ui.textColor + }; + }; + FgEventRenderer.prototype.sortEventSegs = function (segs) { + var specs = this.context.eventOrderSpecs; + var objs = segs.map(buildSegCompareObj); + objs.sort(function (obj0, obj1) { + return compareByFieldSpecs(obj0, obj1, specs); + }); + return objs.map(function (c) { + return c._seg; + }); + }; + FgEventRenderer.prototype.computeSizes = function (force) { + if (force || this.isSizeDirty) { + this.computeSegSizes(this.segs); + } + }; + FgEventRenderer.prototype.assignSizes = function (force) { + if (force || this.isSizeDirty) { + this.assignSegSizes(this.segs); + this.isSizeDirty = false; + } + }; + FgEventRenderer.prototype.computeSegSizes = function (segs) { + }; + FgEventRenderer.prototype.assignSegSizes = function (segs) { + }; + // Manipulation on rendered segs + FgEventRenderer.prototype.hideByHash = function (hash) { + if (hash) { + for (var _i = 0, _a = this.segs; _i < _a.length; _i++) { + var seg = _a[_i]; + if (hash[seg.eventRange.instance.instanceId]) { + seg.el.style.visibility = 'hidden'; + } + } + } + }; + FgEventRenderer.prototype.showByHash = function (hash) { + if (hash) { + for (var _i = 0, _a = this.segs; _i < _a.length; _i++) { + var seg = _a[_i]; + if (hash[seg.eventRange.instance.instanceId]) { + seg.el.style.visibility = ''; + } + } + } + }; + FgEventRenderer.prototype.selectByInstanceId = function (instanceId) { + if (instanceId) { + for (var _i = 0, _a = this.segs; _i < _a.length; _i++) { + var seg = _a[_i]; + var eventInstance = seg.eventRange.instance; + if (eventInstance && eventInstance.instanceId === instanceId && + seg.el // necessary? + ) { + seg.el.classList.add('fc-selected'); + } + } + } + }; + FgEventRenderer.prototype.unselectByInstanceId = function (instanceId) { + if (instanceId) { + for (var _i = 0, _a = this.segs; _i < _a.length; _i++) { + var seg = _a[_i]; + if (seg.el) { // necessary? + seg.el.classList.remove('fc-selected'); + } + } + } + }; + return FgEventRenderer; + }()); + // returns a object with all primitive props that can be compared + function buildSegCompareObj(seg) { + var eventDef = seg.eventRange.def; + var range = seg.eventRange.instance.range; + var start = range.start ? range.start.valueOf() : 0; // TODO: better support for open-range events + var end = range.end ? range.end.valueOf() : 0; // " + return __assign({}, eventDef.extendedProps, eventDef, { id: eventDef.publicId, start: start, + end: end, duration: end - start, allDay: Number(eventDef.allDay), _seg: seg // for later retrieval + }); + } + + /* + TODO: when refactoring this class, make a new FillRenderer instance for each `type` + */ + var FillRenderer = /** @class */ (function () { + function FillRenderer() { + this.fillSegTag = 'div'; + this.dirtySizeFlags = {}; + this.containerElsByType = {}; + this.segsByType = {}; + } + FillRenderer.prototype.getSegsByType = function (type) { + return this.segsByType[type] || []; + }; + FillRenderer.prototype.renderSegs = function (type, context, segs) { + var _a; + this.context = context; + var renderedSegs = this.renderSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs + var containerEls = this.attachSegs(type, renderedSegs); + if (containerEls) { + (_a = (this.containerElsByType[type] || (this.containerElsByType[type] = []))).push.apply(_a, containerEls); + } + this.segsByType[type] = renderedSegs; + if (type === 'bgEvent') { + triggerRenderedSegs(context, renderedSegs, false); // isMirror=false + } + this.dirtySizeFlags[type] = true; + }; + // Unrenders a specific type of fill that is currently rendered on the grid + FillRenderer.prototype.unrender = function (type, context) { + var segs = this.segsByType[type]; + if (segs) { + if (type === 'bgEvent') { + triggerWillRemoveSegs(context, segs, false); // isMirror=false + } + this.detachSegs(type, segs); + } + }; + // Renders and assigns an `el` property for each fill segment. Generic enough to work with different types. + // Only returns segments that successfully rendered. + FillRenderer.prototype.renderSegEls = function (type, segs) { + var _this = this; + var html = ''; + var i; + if (segs.length) { + // build a large concatenation of segment HTML + for (i = 0; i < segs.length; i++) { + html += this.renderSegHtml(type, segs[i]); + } + // Grab individual elements from the combined HTML string. Use each as the default rendering. + // Then, compute the 'el' for each segment. + htmlToElements(html).forEach(function (el, i) { + var seg = segs[i]; + if (el) { + seg.el = el; + } + }); + if (type === 'bgEvent') { + segs = filterSegsViaEls(this.context, segs, false // isMirror. background events can never be mirror elements + ); + } + // correct element type? (would be bad if a non-TD were inserted into a table for example) + segs = segs.filter(function (seg) { + return elementMatches(seg.el, _this.fillSegTag); + }); + } + return segs; + }; + // Builds the HTML needed for one fill segment. Generic enough to work with different types. + FillRenderer.prototype.renderSegHtml = function (type, seg) { + var css = null; + var classNames = []; + if (type !== 'highlight' && type !== 'businessHours') { + css = { + 'background-color': seg.eventRange.ui.backgroundColor + }; + } + if (type !== 'highlight') { + classNames = classNames.concat(seg.eventRange.ui.classNames); + } + if (type === 'businessHours') { + classNames.push('fc-bgevent'); + } + else { + classNames.push('fc-' + type.toLowerCase()); + } + return '<' + this.fillSegTag + + (classNames.length ? ' class="' + classNames.join(' ') + '"' : '') + + (css ? ' style="' + cssToStr(css) + '"' : '') + + '></' + this.fillSegTag + '>'; + }; + FillRenderer.prototype.detachSegs = function (type, segs) { + var containerEls = this.containerElsByType[type]; + if (containerEls) { + containerEls.forEach(removeElement); + delete this.containerElsByType[type]; + } + }; + FillRenderer.prototype.computeSizes = function (force) { + for (var type in this.segsByType) { + if (force || this.dirtySizeFlags[type]) { + this.computeSegSizes(this.segsByType[type]); + } + } + }; + FillRenderer.prototype.assignSizes = function (force) { + for (var type in this.segsByType) { + if (force || this.dirtySizeFlags[type]) { + this.assignSegSizes(this.segsByType[type]); + } + } + this.dirtySizeFlags = {}; + }; + FillRenderer.prototype.computeSegSizes = function (segs) { + }; + FillRenderer.prototype.assignSegSizes = function (segs) { + }; + return FillRenderer; + }()); + + var NamedTimeZoneImpl = /** @class */ (function () { + function NamedTimeZoneImpl(timeZoneName) { + this.timeZoneName = timeZoneName; + } + return NamedTimeZoneImpl; + }()); + + /* + An abstraction for a dragging interaction originating on an event. + Does higher-level things than PointerDragger, such as possibly: + - a "mirror" that moves with the pointer + - a minimum number of pixels or other criteria for a true drag to begin + + subclasses must emit: + - pointerdown + - dragstart + - dragmove + - pointerup + - dragend + */ + var ElementDragging = /** @class */ (function () { + function ElementDragging(el) { + this.emitter = new EmitterMixin(); + } + ElementDragging.prototype.destroy = function () { + }; + ElementDragging.prototype.setMirrorIsVisible = function (bool) { + // optional if subclass doesn't want to support a mirror + }; + ElementDragging.prototype.setMirrorNeedsRevert = function (bool) { + // optional if subclass doesn't want to support a mirror + }; + ElementDragging.prototype.setAutoScrollEnabled = function (bool) { + // optional + }; + return ElementDragging; + }()); + + function formatDate(dateInput, settings) { + if (settings === void 0) { settings = {}; } + var dateEnv = buildDateEnv$1(settings); + var formatter = createFormatter(settings); + var dateMeta = dateEnv.createMarkerMeta(dateInput); + if (!dateMeta) { // TODO: warning? + return ''; + } + return dateEnv.format(dateMeta.marker, formatter, { + forcedTzo: dateMeta.forcedTzo + }); + } + function formatRange(startInput, endInput, settings // mixture of env and formatter settings + ) { + var dateEnv = buildDateEnv$1(typeof settings === 'object' && settings ? settings : {}); // pass in if non-null object + var formatter = createFormatter(settings, globalDefaults.defaultRangeSeparator); + var startMeta = dateEnv.createMarkerMeta(startInput); + var endMeta = dateEnv.createMarkerMeta(endInput); + if (!startMeta || !endMeta) { // TODO: warning? + return ''; + } + return dateEnv.formatRange(startMeta.marker, endMeta.marker, formatter, { + forcedStartTzo: startMeta.forcedTzo, + forcedEndTzo: endMeta.forcedTzo, + isEndExclusive: settings.isEndExclusive + }); + } + // TODO: more DRY and optimized + function buildDateEnv$1(settings) { + var locale = buildLocale(settings.locale || 'en', parseRawLocales([]).map); // TODO: don't hardcode 'en' everywhere + // ensure required settings + settings = __assign({ timeZone: globalDefaults.timeZone, calendarSystem: 'gregory' }, settings, { locale: locale }); + return new DateEnv(settings); + } + + var DRAG_META_PROPS = { + startTime: createDuration, + duration: createDuration, + create: Boolean, + sourceId: String + }; + var DRAG_META_DEFAULTS = { + create: true + }; + function parseDragMeta(raw) { + var leftoverProps = {}; + var refined = refineProps(raw, DRAG_META_PROPS, DRAG_META_DEFAULTS, leftoverProps); + refined.leftoverProps = leftoverProps; + return refined; + } + + // Computes a default column header formatting string if `colFormat` is not explicitly defined + function computeFallbackHeaderFormat(datesRepDistinctDays, dayCnt) { + // 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 (!datesRepDistinctDays || dayCnt > 10) { + return { weekday: 'short' }; // "Sat" + } + else if (dayCnt > 1) { + return { weekday: 'short', month: 'numeric', day: 'numeric', omitCommas: true }; // "Sat 11/12" + } + else { + return { weekday: 'long' }; // "Saturday" + } + } + function renderDateCell(dateMarker, dateProfile, datesRepDistinctDays, colCnt, colHeadFormat, context, colspan, otherAttrs) { + var dateEnv = context.dateEnv, theme = context.theme, options = context.options; + var isDateValid = rangeContainsMarker(dateProfile.activeRange, dateMarker); // TODO: called too frequently. cache somehow. + var classNames = [ + 'fc-day-header', + theme.getClass('widgetHeader') + ]; + var innerHtml; + if (typeof options.columnHeaderHtml === 'function') { + innerHtml = options.columnHeaderHtml(dateEnv.toDate(dateMarker)); + } + else if (typeof options.columnHeaderText === 'function') { + innerHtml = htmlEscape(options.columnHeaderText(dateEnv.toDate(dateMarker))); + } + else { + innerHtml = htmlEscape(dateEnv.format(dateMarker, colHeadFormat)); + } + // if only one row of days, the classNames on the header can represent the specific days beneath + if (datesRepDistinctDays) { + classNames = classNames.concat( + // includes the day-of-week class + // noThemeHighlight=true (don't highlight the header) + getDayClasses(dateMarker, dateProfile, context, true)); + } + else { + classNames.push('fc-' + DAY_IDS[dateMarker.getUTCDay()]); // only add the day-of-week class + } + return '' + + '<th class="' + classNames.join(' ') + '"' + + ((isDateValid && datesRepDistinctDays) ? + ' data-date="' + dateEnv.formatIso(dateMarker, { omitTime: true }) + '"' : + '') + + (colspan > 1 ? + ' colspan="' + colspan + '"' : + '') + + (otherAttrs ? + ' ' + otherAttrs : + '') + + '>' + + (isDateValid ? + // don't make a link if the heading could represent multiple days, or if there's only one day (forceOff) + buildGotoAnchorHtml(options, dateEnv, { date: dateMarker, forceOff: !datesRepDistinctDays || colCnt === 1 }, innerHtml) : + // if not valid, display text, but no link + innerHtml) + + '</th>'; + } + + var DayHeader = /** @class */ (function (_super) { + __extends(DayHeader, _super); + function DayHeader(parentEl) { + var _this = _super.call(this) || this; + _this.renderSkeleton = memoizeRendering(_this._renderSkeleton, _this._unrenderSkeleton); + _this.parentEl = parentEl; + return _this; + } + DayHeader.prototype.render = function (props, context) { + var dates = props.dates, datesRepDistinctDays = props.datesRepDistinctDays; + var parts = []; + this.renderSkeleton(context); + if (props.renderIntroHtml) { + parts.push(props.renderIntroHtml()); + } + var colHeadFormat = createFormatter(context.options.columnHeaderFormat || + computeFallbackHeaderFormat(datesRepDistinctDays, dates.length)); + for (var _i = 0, dates_1 = dates; _i < dates_1.length; _i++) { + var date = dates_1[_i]; + parts.push(renderDateCell(date, props.dateProfile, datesRepDistinctDays, dates.length, colHeadFormat, context)); + } + if (context.isRtl) { + parts.reverse(); + } + this.thead.innerHTML = '<tr>' + parts.join('') + '</tr>'; + }; + DayHeader.prototype.destroy = function () { + _super.prototype.destroy.call(this); + this.renderSkeleton.unrender(); + }; + DayHeader.prototype._renderSkeleton = function (context) { + var theme = context.theme; + var parentEl = this.parentEl; + parentEl.innerHTML = ''; // because might be nbsp + parentEl.appendChild(this.el = htmlToElement('<div class="fc-row ' + theme.getClass('headerRow') + '">' + + '<table class="' + theme.getClass('tableGrid') + '">' + + '<thead></thead>' + + '</table>' + + '</div>')); + this.thead = this.el.querySelector('thead'); + }; + DayHeader.prototype._unrenderSkeleton = function () { + removeElement(this.el); + }; + return DayHeader; + }(Component)); + + var DaySeries = /** @class */ (function () { + function DaySeries(range, dateProfileGenerator) { + var date = range.start; + var end = range.end; + var indices = []; + var dates = []; + var dayIndex = -1; + while (date < end) { // loop each day from start to end + if (dateProfileGenerator.isHiddenDay(date)) { + indices.push(dayIndex + 0.5); // mark that it's between indices + } + else { + dayIndex++; + indices.push(dayIndex); + dates.push(date); + } + date = addDays(date, 1); + } + this.dates = dates; + this.indices = indices; + this.cnt = dates.length; + } + DaySeries.prototype.sliceRange = function (range) { + var firstIndex = this.getDateDayIndex(range.start); // inclusive first index + var lastIndex = this.getDateDayIndex(addDays(range.end, -1)); // inclusive last index + var clippedFirstIndex = Math.max(0, firstIndex); + var clippedLastIndex = Math.min(this.cnt - 1, lastIndex); + // deal with in-between indices + clippedFirstIndex = Math.ceil(clippedFirstIndex); // in-between starts round to next cell + clippedLastIndex = Math.floor(clippedLastIndex); // in-between ends round to prev cell + if (clippedFirstIndex <= clippedLastIndex) { + return { + firstIndex: clippedFirstIndex, + lastIndex: clippedLastIndex, + isStart: firstIndex === clippedFirstIndex, + isEnd: lastIndex === clippedLastIndex + }; + } + else { + return null; + } + }; + // 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. + DaySeries.prototype.getDateDayIndex = function (date) { + var indices = this.indices; + var dayOffset = Math.floor(diffDays(this.dates[0], date)); + if (dayOffset < 0) { + return indices[0] - 1; + } + else if (dayOffset >= indices.length) { + return indices[indices.length - 1] + 1; + } + else { + return indices[dayOffset]; + } + }; + return DaySeries; + }()); + + var DayTable = /** @class */ (function () { + function DayTable(daySeries, breakOnWeeks) { + var dates = daySeries.dates; + var daysPerRow; + var firstDay; + var rowCnt; + if (breakOnWeeks) { + // count columns until the day-of-week repeats + firstDay = dates[0].getUTCDay(); + for (daysPerRow = 1; daysPerRow < dates.length; daysPerRow++) { + if (dates[daysPerRow].getUTCDay() === firstDay) { + break; + } + } + rowCnt = Math.ceil(dates.length / daysPerRow); + } + else { + rowCnt = 1; + daysPerRow = dates.length; + } + this.rowCnt = rowCnt; + this.colCnt = daysPerRow; + this.daySeries = daySeries; + this.cells = this.buildCells(); + this.headerDates = this.buildHeaderDates(); + } + DayTable.prototype.buildCells = function () { + var rows = []; + for (var row = 0; row < this.rowCnt; row++) { + var cells = []; + for (var col = 0; col < this.colCnt; col++) { + cells.push(this.buildCell(row, col)); + } + rows.push(cells); + } + return rows; + }; + DayTable.prototype.buildCell = function (row, col) { + return { + date: this.daySeries.dates[row * this.colCnt + col] + }; + }; + DayTable.prototype.buildHeaderDates = function () { + var dates = []; + for (var col = 0; col < this.colCnt; col++) { + dates.push(this.cells[0][col].date); + } + return dates; + }; + DayTable.prototype.sliceRange = function (range) { + var colCnt = this.colCnt; + var seriesSeg = this.daySeries.sliceRange(range); + var segs = []; + if (seriesSeg) { + var firstIndex = seriesSeg.firstIndex, lastIndex = seriesSeg.lastIndex; + var index = firstIndex; + while (index <= lastIndex) { + var row = Math.floor(index / colCnt); + var nextIndex = Math.min((row + 1) * colCnt, lastIndex + 1); + segs.push({ + row: row, + firstCol: index % colCnt, + lastCol: (nextIndex - 1) % colCnt, + isStart: seriesSeg.isStart && index === firstIndex, + isEnd: seriesSeg.isEnd && (nextIndex - 1) === lastIndex + }); + index = nextIndex; + } + } + return segs; + }; + return DayTable; + }()); + + var Slicer = /** @class */ (function () { + function Slicer() { + this.sliceBusinessHours = memoize(this._sliceBusinessHours); + this.sliceDateSelection = memoize(this._sliceDateSpan); + this.sliceEventStore = memoize(this._sliceEventStore); + this.sliceEventDrag = memoize(this._sliceInteraction); + this.sliceEventResize = memoize(this._sliceInteraction); + } + Slicer.prototype.sliceProps = function (props, dateProfile, nextDayThreshold, calendar, component) { + var extraArgs = []; + for (var _i = 5; _i < arguments.length; _i++) { + extraArgs[_i - 5] = arguments[_i]; + } + var eventUiBases = props.eventUiBases; + var eventSegs = this.sliceEventStore.apply(this, [props.eventStore, eventUiBases, dateProfile, nextDayThreshold, component].concat(extraArgs)); + return { + dateSelectionSegs: this.sliceDateSelection.apply(this, [props.dateSelection, eventUiBases, component].concat(extraArgs)), + businessHourSegs: this.sliceBusinessHours.apply(this, [props.businessHours, dateProfile, nextDayThreshold, calendar, component].concat(extraArgs)), + fgEventSegs: eventSegs.fg, + bgEventSegs: eventSegs.bg, + eventDrag: this.sliceEventDrag.apply(this, [props.eventDrag, eventUiBases, dateProfile, nextDayThreshold, component].concat(extraArgs)), + eventResize: this.sliceEventResize.apply(this, [props.eventResize, eventUiBases, dateProfile, nextDayThreshold, component].concat(extraArgs)), + eventSelection: props.eventSelection + }; // TODO: give interactionSegs? + }; + Slicer.prototype.sliceNowDate = function (// does not memoize + date, component) { + var extraArgs = []; + for (var _i = 2; _i < arguments.length; _i++) { + extraArgs[_i - 2] = arguments[_i]; + } + return this._sliceDateSpan.apply(this, [{ range: { start: date, end: addMs(date, 1) }, allDay: false }, + {}, + component].concat(extraArgs)); + }; + Slicer.prototype._sliceBusinessHours = function (businessHours, dateProfile, nextDayThreshold, calendar, component) { + var extraArgs = []; + for (var _i = 5; _i < arguments.length; _i++) { + extraArgs[_i - 5] = arguments[_i]; + } + if (!businessHours) { + return []; + } + return this._sliceEventStore.apply(this, [expandRecurring(businessHours, computeActiveRange(dateProfile, Boolean(nextDayThreshold)), calendar), + {}, + dateProfile, + nextDayThreshold, + component].concat(extraArgs)).bg; + }; + Slicer.prototype._sliceEventStore = function (eventStore, eventUiBases, dateProfile, nextDayThreshold, component) { + var extraArgs = []; + for (var _i = 5; _i < arguments.length; _i++) { + extraArgs[_i - 5] = arguments[_i]; + } + if (eventStore) { + var rangeRes = sliceEventStore(eventStore, eventUiBases, computeActiveRange(dateProfile, Boolean(nextDayThreshold)), nextDayThreshold); + return { + bg: this.sliceEventRanges(rangeRes.bg, component, extraArgs), + fg: this.sliceEventRanges(rangeRes.fg, component, extraArgs) + }; + } + else { + return { bg: [], fg: [] }; + } + }; + Slicer.prototype._sliceInteraction = function (interaction, eventUiBases, dateProfile, nextDayThreshold, component) { + var extraArgs = []; + for (var _i = 5; _i < arguments.length; _i++) { + extraArgs[_i - 5] = arguments[_i]; + } + if (!interaction) { + return null; + } + var rangeRes = sliceEventStore(interaction.mutatedEvents, eventUiBases, computeActiveRange(dateProfile, Boolean(nextDayThreshold)), nextDayThreshold); + return { + segs: this.sliceEventRanges(rangeRes.fg, component, extraArgs), + affectedInstances: interaction.affectedEvents.instances, + isEvent: interaction.isEvent, + sourceSeg: interaction.origSeg + }; + }; + Slicer.prototype._sliceDateSpan = function (dateSpan, eventUiBases, component) { + var extraArgs = []; + for (var _i = 3; _i < arguments.length; _i++) { + extraArgs[_i - 3] = arguments[_i]; + } + if (!dateSpan) { + return []; + } + var eventRange = fabricateEventRange(dateSpan, eventUiBases, component.context.calendar); + var segs = this.sliceRange.apply(this, [dateSpan.range].concat(extraArgs)); + for (var _a = 0, segs_1 = segs; _a < segs_1.length; _a++) { + var seg = segs_1[_a]; + seg.component = component; + seg.eventRange = eventRange; + } + return segs; + }; + /* + "complete" seg means it has component and eventRange + */ + Slicer.prototype.sliceEventRanges = function (eventRanges, component, // TODO: kill + extraArgs) { + var segs = []; + for (var _i = 0, eventRanges_1 = eventRanges; _i < eventRanges_1.length; _i++) { + var eventRange = eventRanges_1[_i]; + segs.push.apply(segs, this.sliceEventRange(eventRange, component, extraArgs)); + } + return segs; + }; + /* + "complete" seg means it has component and eventRange + */ + Slicer.prototype.sliceEventRange = function (eventRange, component, // TODO: kill + extraArgs) { + var segs = this.sliceRange.apply(this, [eventRange.range].concat(extraArgs)); + for (var _i = 0, segs_2 = segs; _i < segs_2.length; _i++) { + var seg = segs_2[_i]; + seg.component = component; + seg.eventRange = eventRange; + seg.isStart = eventRange.isStart && seg.isStart; + seg.isEnd = eventRange.isEnd && seg.isEnd; + } + return segs; + }; + return Slicer; + }()); + /* + for incorporating minTime/maxTime if appropriate + TODO: should be part of DateProfile! + TimelineDateProfile already does this btw + */ + function computeActiveRange(dateProfile, isComponentAllDay) { + var range = dateProfile.activeRange; + if (isComponentAllDay) { + return range; + } + return { + start: addMs(range.start, dateProfile.minTime.milliseconds), + end: addMs(range.end, dateProfile.maxTime.milliseconds - 864e5) // 864e5 = ms in a day + }; + } + + // exports + // -------------------------------------------------------------------------------------------------- + var version = '4.4.0'; + + exports.Calendar = Calendar; + exports.Component = Component; + exports.ComponentContext = ComponentContext; + exports.DateComponent = DateComponent; + exports.DateEnv = DateEnv; + exports.DateProfileGenerator = DateProfileGenerator; + exports.DayHeader = DayHeader; + exports.DaySeries = DaySeries; + exports.DayTable = DayTable; + exports.ElementDragging = ElementDragging; + exports.ElementScrollController = ElementScrollController; + exports.EmitterMixin = EmitterMixin; + exports.EventApi = EventApi; + exports.FgEventRenderer = FgEventRenderer; + exports.FillRenderer = FillRenderer; + exports.Interaction = Interaction; + exports.Mixin = Mixin; + exports.NamedTimeZoneImpl = NamedTimeZoneImpl; + exports.PositionCache = PositionCache; + exports.ScrollComponent = ScrollComponent; + exports.ScrollController = ScrollController; + exports.Slicer = Slicer; + exports.Splitter = Splitter; + exports.Theme = Theme; + exports.View = View; + exports.WindowScrollController = WindowScrollController; + exports.addDays = addDays; + exports.addDurations = addDurations; + exports.addMs = addMs; + exports.addWeeks = addWeeks; + exports.allowContextMenu = allowContextMenu; + exports.allowSelection = allowSelection; + exports.appendToElement = appendToElement; + exports.applyAll = applyAll; + exports.applyMutationToEventStore = applyMutationToEventStore; + exports.applyStyle = applyStyle; + exports.applyStyleProp = applyStyleProp; + exports.asRoughMinutes = asRoughMinutes; + exports.asRoughMs = asRoughMs; + exports.asRoughSeconds = asRoughSeconds; + exports.buildGotoAnchorHtml = buildGotoAnchorHtml; + exports.buildSegCompareObj = buildSegCompareObj; + exports.capitaliseFirstLetter = capitaliseFirstLetter; + exports.combineEventUis = combineEventUis; + exports.compareByFieldSpec = compareByFieldSpec; + exports.compareByFieldSpecs = compareByFieldSpecs; + exports.compareNumbers = compareNumbers; + exports.compensateScroll = compensateScroll; + exports.computeClippingRect = computeClippingRect; + exports.computeEdges = computeEdges; + exports.computeEventDraggable = computeEventDraggable; + exports.computeEventEndResizable = computeEventEndResizable; + exports.computeEventStartResizable = computeEventStartResizable; + exports.computeFallbackHeaderFormat = computeFallbackHeaderFormat; + exports.computeHeightAndMargins = computeHeightAndMargins; + exports.computeInnerRect = computeInnerRect; + exports.computeRect = computeRect; + exports.computeVisibleDayRange = computeVisibleDayRange; + exports.config = config; + exports.constrainPoint = constrainPoint; + exports.createDuration = createDuration; + exports.createElement = createElement; + exports.createEmptyEventStore = createEmptyEventStore; + exports.createEventInstance = createEventInstance; + exports.createFormatter = createFormatter; + exports.createPlugin = createPlugin; + exports.cssToStr = cssToStr; + exports.debounce = debounce; + exports.diffDates = diffDates; + exports.diffDayAndTime = diffDayAndTime; + exports.diffDays = diffDays; + exports.diffPoints = diffPoints; + exports.diffWeeks = diffWeeks; + exports.diffWholeDays = diffWholeDays; + exports.diffWholeWeeks = diffWholeWeeks; + exports.disableCursor = disableCursor; + exports.distributeHeight = distributeHeight; + exports.elementClosest = elementClosest; + exports.elementMatches = elementMatches; + exports.enableCursor = enableCursor; + exports.eventTupleToStore = eventTupleToStore; + exports.filterEventStoreDefs = filterEventStoreDefs; + exports.filterHash = filterHash; + exports.findChildren = findChildren; + exports.findElements = findElements; + exports.flexibleCompare = flexibleCompare; + exports.forceClassName = forceClassName; + exports.formatDate = formatDate; + exports.formatIsoTimeString = formatIsoTimeString; + exports.formatRange = formatRange; + exports.getAllDayHtml = getAllDayHtml; + exports.getClippingParents = getClippingParents; + exports.getDayClasses = getDayClasses; + exports.getElSeg = getElSeg; + exports.getRectCenter = getRectCenter; + exports.getRelevantEvents = getRelevantEvents; + exports.globalDefaults = globalDefaults; + exports.greatestDurationDenominator = greatestDurationDenominator; + exports.hasBgRendering = hasBgRendering; + exports.htmlEscape = htmlEscape; + exports.htmlToElement = htmlToElement; + exports.insertAfterElement = insertAfterElement; + exports.interactionSettingsStore = interactionSettingsStore; + exports.interactionSettingsToStore = interactionSettingsToStore; + exports.intersectRanges = intersectRanges; + exports.intersectRects = intersectRects; + exports.isArraysEqual = isArraysEqual; + exports.isDateSpansEqual = isDateSpansEqual; + exports.isInt = isInt; + exports.isInteractionValid = isInteractionValid; + exports.isMultiDayRange = isMultiDayRange; + exports.isPropsEqual = isPropsEqual; + exports.isPropsValid = isPropsValid; + exports.isSingleDay = isSingleDay; + exports.isValidDate = isValidDate; + exports.listenBySelector = listenBySelector; + exports.mapHash = mapHash; + exports.matchCellWidths = matchCellWidths; + exports.memoize = memoize; + exports.memoizeOutput = memoizeOutput; + exports.memoizeRendering = memoizeRendering; + exports.mergeEventStores = mergeEventStores; + exports.multiplyDuration = multiplyDuration; + exports.padStart = padStart; + exports.parseBusinessHours = parseBusinessHours; + exports.parseDragMeta = parseDragMeta; + exports.parseEventDef = parseEventDef; + exports.parseFieldSpecs = parseFieldSpecs; + exports.parseMarker = parse; + exports.pointInsideRect = pointInsideRect; + exports.prependToElement = prependToElement; + exports.preventContextMenu = preventContextMenu; + exports.preventDefault = preventDefault; + exports.preventSelection = preventSelection; + exports.processScopedUiProps = processScopedUiProps; + exports.rangeContainsMarker = rangeContainsMarker; + exports.rangeContainsRange = rangeContainsRange; + exports.rangesEqual = rangesEqual; + exports.rangesIntersect = rangesIntersect; + exports.refineProps = refineProps; + exports.removeElement = removeElement; + exports.removeExact = removeExact; + exports.renderDateCell = renderDateCell; + exports.requestJson = requestJson; + exports.sliceEventStore = sliceEventStore; + exports.startOfDay = startOfDay; + exports.subtractInnerElHeight = subtractInnerElHeight; + exports.translateRect = translateRect; + exports.uncompensateScroll = uncompensateScroll; + exports.undistributeHeight = undistributeHeight; + exports.unpromisify = unpromisify; + exports.version = version; + exports.whenTransitionDone = whenTransitionDone; + exports.wholeDivideDurations = wholeDivideDurations; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); diff --git a/addons/web/static/lib/fullcalendar/css/fullcalendar.css b/addons/web/static/lib/fullcalendar/css/fullcalendar.css deleted file mode 100644 index 0fa5d9ea419..00000000000 --- a/addons/web/static/lib/fullcalendar/css/fullcalendar.css +++ /dev/null @@ -1,1402 +0,0 @@ -/*! - * FullCalendar v3.0.1 Stylesheet - * Docs & License: http://fullcalendar.io/ - * (c) 2016 Adam Shaw - */ - - -.fc { - direction: ltr; - text-align: left; -} - -.fc-rtl { - text-align: right; -} - -body .fc { /* extra precedence to overcome jqui */ - font-size: 1em; -} - - -/* Colors ---------------------------------------------------------------------------------------------------*/ - -.fc-unthemed th, -.fc-unthemed td, -.fc-unthemed thead, -.fc-unthemed tbody, -.fc-unthemed .fc-divider, -.fc-unthemed .fc-row, -.fc-unthemed .fc-content, /* for gutter border */ -.fc-unthemed .fc-popover, -.fc-unthemed .fc-list-view, -.fc-unthemed .fc-list-heading td { - border-color: #ddd; -} - -.fc-unthemed .fc-popover { - background-color: #fff; -} - -.fc-unthemed .fc-divider, -.fc-unthemed .fc-popover .fc-header, -.fc-unthemed .fc-list-heading td { - background: #eee; -} - -.fc-unthemed .fc-popover .fc-header .fc-close { - color: #666; -} - -.fc-unthemed .fc-today { - background: #fcf8e3; -} - -.fc-highlight { /* when user is selecting cells */ - background: #bce8f1; - opacity: .3; -} - -.fc-bgevent { /* default look for background events */ - background: rgb(143, 223, 130); - opacity: .3; -} - -.fc-nonbusiness { /* default look for non-business-hours areas */ - /* will inherit .fc-bgevent's styles */ - background: #d7d7d7; -} - - -/* Icons (inline elements with styled text that mock arrow icons) ---------------------------------------------------------------------------------------------------*/ - -.fc-icon { - display: inline-block; - height: 1em; - line-height: 1em; - font-size: 1em; - text-align: center; - overflow: hidden; - font-family: "Courier New", Courier, monospace; - - /* don't allow browser text-selection */ - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - } - -/* -Acceptable font-family overrides for individual icons: - "Arial", sans-serif - "Times New Roman", serif - -NOTE: use percentage font sizes or else old IE chokes -*/ - -.fc-icon:after { - position: relative; -} - -.fc-icon-left-single-arrow:after { - content: "\02039"; - font-weight: bold; - font-size: 200%; - top: -7%; -} - -.fc-icon-right-single-arrow:after { - content: "\0203A"; - font-weight: bold; - font-size: 200%; - top: -7%; -} - -.fc-icon-left-double-arrow:after { - content: "\000AB"; - font-size: 160%; - top: -7%; -} - -.fc-icon-right-double-arrow:after { - content: "\000BB"; - font-size: 160%; - top: -7%; -} - -.fc-icon-left-triangle:after { - content: "\25C4"; - font-size: 125%; - top: 3%; -} - -.fc-icon-right-triangle:after { - content: "\25BA"; - font-size: 125%; - top: 3%; -} - -.fc-icon-down-triangle:after { - content: "\25BC"; - font-size: 125%; - top: 2%; -} - -.fc-icon-x:after { - content: "\000D7"; - font-size: 200%; - top: 6%; -} - - -/* Buttons (styled <button> tags, normalized to work cross-browser) ---------------------------------------------------------------------------------------------------*/ - -.fc button { - /* force height to include the border and padding */ - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; - - /* dimensions */ - margin: 0; - height: 2.1em; - padding: 0 .6em; - - /* text & cursor */ - font-size: 1em; /* normalize */ - white-space: nowrap; - cursor: pointer; -} - -/* Firefox has an annoying inner border */ -.fc button::-moz-focus-inner { margin: 0; padding: 0; } - -.fc-state-default { /* non-theme */ - border: 1px solid; -} - -.fc-state-default.fc-corner-left { /* non-theme */ - border-top-left-radius: 4px; - border-bottom-left-radius: 4px; -} - -.fc-state-default.fc-corner-right { /* non-theme */ - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; -} - -/* icons in buttons */ - -.fc button .fc-icon { /* non-theme */ - position: relative; - top: -0.05em; /* seems to be a good adjustment across browsers */ - margin: 0 .2em; - vertical-align: middle; -} - -/* - button states - borrowed from twitter bootstrap (http://twitter.github.com/bootstrap/) -*/ - -.fc-state-default { - background-color: #f5f5f5; - background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); - background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); - background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); - background-image: linear-gradient(to bottom, #ffffff, #e6e6e6); - background-repeat: repeat-x; - border-color: #e6e6e6 #e6e6e6 #bfbfbf; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - color: #333; - text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.fc-state-hover, -.fc-state-down, -.fc-state-active, -.fc-state-disabled { - color: #333333; - background-color: #e6e6e6; -} - -.fc-state-hover { - color: #333333; - text-decoration: none; - background-position: 0 -15px; - -webkit-transition: background-position 0.1s linear; - -moz-transition: background-position 0.1s linear; - -o-transition: background-position 0.1s linear; - transition: background-position 0.1s linear; -} - -.fc-state-down, -.fc-state-active { - background-color: #cccccc; - background-image: none; - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.fc-state-disabled { - cursor: default; - background-image: none; - opacity: 0.65; - box-shadow: none; -} - - -/* Buttons Groups ---------------------------------------------------------------------------------------------------*/ - -.fc-button-group { - display: inline-block; -} - -/* -every button that is not first in a button group should scootch over one pixel and cover the -previous button's border... -*/ - -.fc .fc-button-group > * { /* extra precedence b/c buttons have margin set to zero */ - float: left; - margin: 0 0 0 -1px; -} - -.fc .fc-button-group > :first-child { /* same */ - margin-left: 0; -} - - -/* Popover ---------------------------------------------------------------------------------------------------*/ - -.fc-popover { - position: absolute; - box-shadow: 0 2px 6px rgba(0,0,0,.15); -} - -.fc-popover .fc-header { /* TODO: be more consistent with fc-head/fc-body */ - padding: 2px 4px; -} - -.fc-popover .fc-header .fc-title { - margin: 0 2px; -} - -.fc-popover .fc-header .fc-close { - cursor: pointer; -} - -.fc-ltr .fc-popover .fc-header .fc-title, -.fc-rtl .fc-popover .fc-header .fc-close { - float: left; -} - -.fc-rtl .fc-popover .fc-header .fc-title, -.fc-ltr .fc-popover .fc-header .fc-close { - float: right; -} - -/* unthemed */ - -.fc-unthemed .fc-popover { - border-width: 1px; - border-style: solid; -} - -.fc-unthemed .fc-popover .fc-header .fc-close { - font-size: .9em; - margin-top: 2px; -} - -/* jqui themed */ - -.fc-popover > .ui-widget-header + .ui-widget-content { - border-top: 0; /* where they meet, let the header have the border */ -} - - -/* Misc Reusable Components ---------------------------------------------------------------------------------------------------*/ - -.fc-divider { - border-style: solid; - border-width: 1px; -} - -hr.fc-divider { - height: 0; - margin: 0; - padding: 0 0 2px; /* height is unreliable across browsers, so use padding */ - border-width: 1px 0; -} - -.fc-clear { - clear: both; -} - -.fc-bg, -.fc-bgevent-skeleton, -.fc-highlight-skeleton, -.fc-helper-skeleton { - /* these element should always cling to top-left/right corners */ - position: absolute; - top: 0; - left: 0; - right: 0; -} - -.fc-bg { - bottom: 0; /* strech bg to bottom edge */ -} - -.fc-bg table { - height: 100%; /* strech bg to bottom edge */ -} - - -/* Tables ---------------------------------------------------------------------------------------------------*/ - -.fc table { - width: 100%; - box-sizing: border-box; /* fix scrollbar issue in firefox */ - table-layout: fixed; - border-collapse: collapse; - border-spacing: 0; - font-size: 1em; /* normalize cross-browser */ -} - -.fc th { - text-align: center; -} - -.fc th, -.fc td { - border-style: solid; - border-width: 1px; - padding: 0; - vertical-align: top; -} - -.fc td.fc-today { - border-style: double; /* overcome neighboring borders */ -} - - -/* Internal Nav Links ---------------------------------------------------------------------------------------------------*/ - -a[data-goto] { - cursor: pointer; -} - -a[data-goto]:hover { - text-decoration: underline; -} - - -/* Fake Table Rows ---------------------------------------------------------------------------------------------------*/ - -.fc .fc-row { /* extra precedence to overcome themes w/ .ui-widget-content forcing a 1px border */ - /* no visible border by default. but make available if need be (scrollbar width compensation) */ - border-style: solid; - border-width: 0; -} - -.fc-row table { - /* don't put left/right border on anything within a fake row. - the outer tbody will worry about this */ - border-left: 0 hidden transparent; - border-right: 0 hidden transparent; - - /* no bottom borders on rows */ - border-bottom: 0 hidden transparent; -} - -.fc-row:first-child table { - border-top: 0 hidden transparent; /* no top border on first row */ -} - - -/* Day Row (used within the header and the DayGrid) ---------------------------------------------------------------------------------------------------*/ - -.fc-row { - position: relative; -} - -.fc-row .fc-bg { - z-index: 1; -} - -/* highlighting cells & background event skeleton */ - -.fc-row .fc-bgevent-skeleton, -.fc-row .fc-highlight-skeleton { - bottom: 0; /* stretch skeleton to bottom of row */ -} - -.fc-row .fc-bgevent-skeleton table, -.fc-row .fc-highlight-skeleton table { - height: 100%; /* stretch skeleton to bottom of row */ -} - -.fc-row .fc-highlight-skeleton td, -.fc-row .fc-bgevent-skeleton td { - border-color: transparent; -} - -.fc-row .fc-bgevent-skeleton { - z-index: 2; - -} - -.fc-row .fc-highlight-skeleton { - z-index: 3; -} - -/* -row content (which contains day/week numbers and events) as well as "helper" (which contains -temporary rendered events). -*/ - -.fc-row .fc-content-skeleton { - position: relative; - z-index: 4; - padding-bottom: 2px; /* matches the space above the events */ -} - -.fc-row .fc-helper-skeleton { - z-index: 5; -} - -.fc-row .fc-content-skeleton td, -.fc-row .fc-helper-skeleton td { - /* see-through to the background below */ - background: none; /* in case <td>s are globally styled */ - border-color: transparent; - - /* don't put a border between events and/or the day number */ - border-bottom: 0; -} - -.fc-row .fc-content-skeleton tbody td, /* cells with events inside (so NOT the day number cell) */ -.fc-row .fc-helper-skeleton tbody td { - /* don't put a border between event cells */ - border-top: 0; -} - - -/* Scrolling Container ---------------------------------------------------------------------------------------------------*/ - -.fc-scroller { - -webkit-overflow-scrolling: touch; -} - -/* TODO: move to agenda/basic */ -.fc-scroller > .fc-day-grid, -.fc-scroller > .fc-time-grid { - position: relative; /* re-scope all positions */ - width: 100%; /* hack to force re-sizing this inner element when scrollbars appear/disappear */ -} - - -/* Global Event Styles ---------------------------------------------------------------------------------------------------*/ - -.fc-event { - position: relative; /* for resize handle and other inner positioning */ - display: block; /* make the <a> tag block */ - font-size: .85em; - line-height: 1.3; - border-radius: 3px; - border: 1px solid #3a87ad; /* default BORDER color */ - font-weight: normal; /* undo jqui's ui-widget-header bold */ -} - -.fc-event, -.fc-event-dot { - background-color: #3a87ad; /* default BACKGROUND color */ -} - -/* overpower some of bootstrap's and jqui's styles on <a> tags */ -.fc-event, -.fc-event:hover, -.ui-widget .fc-event { - color: #fff; /* default TEXT color */ - text-decoration: none; /* if <a> has an href */ -} - -.fc-event[href], -.fc-event.fc-draggable { - cursor: pointer; /* give events with links and draggable events a hand mouse pointer */ -} - -.fc-not-allowed, /* causes a "warning" cursor. applied on body */ -.fc-not-allowed .fc-event { /* to override an event's custom cursor */ - cursor: not-allowed; -} - -.fc-event .fc-bg { /* the generic .fc-bg already does position */ - z-index: 1; - background: #fff; - opacity: .25; -} - -.fc-event .fc-content { - position: relative; - z-index: 2; -} - -/* resizer (cursor AND touch devices) */ - -.fc-event .fc-resizer { - position: absolute; - z-index: 4; -} - -/* resizer (touch devices) */ - -.fc-event .fc-resizer { - display: none; -} - -.fc-event.fc-allow-mouse-resize .fc-resizer, -.fc-event.fc-selected .fc-resizer { - /* only show when hovering or selected (with touch) */ - display: block; -} - -/* hit area */ - -.fc-event.fc-selected .fc-resizer:before { - /* 40x40 touch area */ - content: ""; - position: absolute; - z-index: 9999; /* user of this util can scope within a lower z-index */ - top: 50%; - left: 50%; - width: 40px; - height: 40px; - margin-left: -20px; - margin-top: -20px; -} - - -/* Event Selection (only for touch devices) ---------------------------------------------------------------------------------------------------*/ - -.fc-event.fc-selected { - z-index: 9999 !important; /* overcomes inline z-index */ - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); -} - -.fc-event.fc-selected.fc-dragging { - box-shadow: 0 2px 7px rgba(0, 0, 0, 0.3); -} - - -/* Horizontal Events ---------------------------------------------------------------------------------------------------*/ - -/* bigger touch area when selected */ -.fc-h-event.fc-selected:before { - content: ""; - position: absolute; - z-index: 3; /* below resizers */ - top: -10px; - bottom: -10px; - left: 0; - right: 0; -} - -/* events that are continuing to/from another week. kill rounded corners and butt up against edge */ - -.fc-ltr .fc-h-event.fc-not-start, -.fc-rtl .fc-h-event.fc-not-end { - margin-left: 0; - border-left-width: 0; - padding-left: 1px; /* replace the border with padding */ - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} - -.fc-ltr .fc-h-event.fc-not-end, -.fc-rtl .fc-h-event.fc-not-start { - margin-right: 0; - border-right-width: 0; - padding-right: 1px; /* replace the border with padding */ - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} - -/* resizer (cursor AND touch devices) */ - -/* left resizer */ -.fc-ltr .fc-h-event .fc-start-resizer, -.fc-rtl .fc-h-event .fc-end-resizer { - cursor: w-resize; - left: -1px; /* overcome border */ -} - -/* right resizer */ -.fc-ltr .fc-h-event .fc-end-resizer, -.fc-rtl .fc-h-event .fc-start-resizer { - cursor: e-resize; - right: -1px; /* overcome border */ -} - -/* resizer (mouse devices) */ - -.fc-h-event.fc-allow-mouse-resize .fc-resizer { - width: 7px; - top: -1px; /* overcome top border */ - bottom: -1px; /* overcome bottom border */ -} - -/* resizer (touch devices) */ - -.fc-h-event.fc-selected .fc-resizer { - /* 8x8 little dot */ - border-radius: 4px; - border-width: 1px; - width: 6px; - height: 6px; - border-style: solid; - border-color: inherit; - background: #fff; - /* vertically center */ - top: 50%; - margin-top: -4px; -} - -/* left resizer */ -.fc-ltr .fc-h-event.fc-selected .fc-start-resizer, -.fc-rtl .fc-h-event.fc-selected .fc-end-resizer { - margin-left: -4px; /* centers the 8x8 dot on the left edge */ -} - -/* right resizer */ -.fc-ltr .fc-h-event.fc-selected .fc-end-resizer, -.fc-rtl .fc-h-event.fc-selected .fc-start-resizer { - margin-right: -4px; /* centers the 8x8 dot on the right edge */ -} - - -/* DayGrid events ----------------------------------------------------------------------------------------------------- -We use the full "fc-day-grid-event" class instead of using descendants because the event won't -be a descendant of the grid when it is being dragged. -*/ - -.fc-day-grid-event { - margin: 1px 2px 0; /* spacing between events and edges */ - padding: 0 1px; -} - -tr:first-child > td > .fc-day-grid-event { - margin-top: 2px; /* a little bit more space before the first event */ -} - -.fc-day-grid-event.fc-selected:after { - content: ""; - position: absolute; - z-index: 1; /* same z-index as fc-bg, behind text */ - /* overcome the borders */ - top: -1px; - right: -1px; - bottom: -1px; - left: -1px; - /* darkening effect */ - background: #000; - opacity: .25; -} - -.fc-day-grid-event .fc-content { /* force events to be one-line tall */ - white-space: nowrap; - overflow: hidden; -} - -.fc-day-grid-event .fc-time { - font-weight: bold; -} - -/* resizer (cursor devices) */ - -/* left resizer */ -.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer, -.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer { - margin-left: -2px; /* to the day cell's edge */ -} - -/* right resizer */ -.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer, -.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer { - margin-right: -2px; /* to the day cell's edge */ -} - - -/* Event Limiting ---------------------------------------------------------------------------------------------------*/ - -/* "more" link that represents hidden events */ - -a.fc-more { - margin: 1px 3px; - font-size: .85em; - cursor: pointer; - text-decoration: none; -} - -a.fc-more:hover { - text-decoration: underline; -} - -.fc-limited { /* rows and cells that are hidden because of a "more" link */ - display: none; -} - -/* popover that appears when "more" link is clicked */ - -.fc-day-grid .fc-row { - z-index: 1; /* make the "more" popover one higher than this */ -} - -.fc-more-popover { - z-index: 2; - width: 220px; -} - -.fc-more-popover .fc-event-container { - padding: 10px; -} - - -/* Now Indicator ---------------------------------------------------------------------------------------------------*/ - -.fc-now-indicator { - position: absolute; - border: 0 solid red; -} - - -/* Utilities ---------------------------------------------------------------------------------------------------*/ - -.fc-unselectable { - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - -webkit-touch-callout: none; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); -} - - - -/* Toolbar ---------------------------------------------------------------------------------------------------*/ - -.fc-toolbar { - text-align: center; - margin-bottom: 1em; -} - -.fc-toolbar .fc-left { - float: left; -} - -.fc-toolbar .fc-right { - float: right; -} - -.fc-toolbar .fc-center { - display: inline-block; -} - -/* the things within each left/right/center section */ -.fc .fc-toolbar > * > * { /* extra precedence to override button border margins */ - float: left; - margin-left: .75em; -} - -/* the first thing within each left/center/right section */ -.fc .fc-toolbar > * > :first-child { /* extra precedence to override button border margins */ - margin-left: 0; -} - -/* title text */ - -.fc-toolbar h2 { - margin: 0; -} - -/* button layering (for border precedence) */ - -.fc-toolbar button { - position: relative; -} - -.fc-toolbar .fc-state-hover, -.fc-toolbar .ui-state-hover { - z-index: 2; -} - -.fc-toolbar .fc-state-down { - z-index: 3; -} - -.fc-toolbar .fc-state-active, -.fc-toolbar .ui-state-active { - z-index: 4; -} - -.fc-toolbar button:focus { - z-index: 5; -} - - -/* View Structure ---------------------------------------------------------------------------------------------------*/ - -/* undo twitter bootstrap's box-sizing rules. normalizes positioning techniques */ -/* don't do this for the toolbar because we'll want bootstrap to style those buttons as some pt */ -.fc-view-container *, -.fc-view-container *:before, -.fc-view-container *:after { - -webkit-box-sizing: content-box; - -moz-box-sizing: content-box; - box-sizing: content-box; -} - -.fc-view, /* scope positioning and z-index's for everything within the view */ -.fc-view > table { /* so dragged elements can be above the view's main element */ - position: relative; - z-index: 1; -} - - - -/* BasicView ---------------------------------------------------------------------------------------------------*/ - -/* day row structure */ - -.fc-basicWeek-view .fc-content-skeleton, -.fc-basicDay-view .fc-content-skeleton { - /* there may be week numbers in these views, so no padding-top */ - padding-bottom: 1em; /* ensure a space at bottom of cell for user selecting/clicking */ -} - -.fc-basic-view .fc-body .fc-row { - min-height: 4em; /* ensure that all rows are at least this tall */ -} - -/* a "rigid" row will take up a constant amount of height because content-skeleton is absolute */ - -.fc-row.fc-rigid { - overflow: hidden; -} - -.fc-row.fc-rigid .fc-content-skeleton { - position: absolute; - top: 0; - left: 0; - right: 0; -} - -/* week and day number styling */ - -.fc-day-top.fc-other-month { - opacity: 0.3; -} - -.fc-basic-view .fc-week-number, -.fc-basic-view .fc-day-number { - padding: 2px; -} - -.fc-basic-view th.fc-week-number, -.fc-basic-view th.fc-day-number { - padding: 0 2px; /* column headers can't have as much v space */ -} - -.fc-ltr .fc-basic-view .fc-day-top .fc-day-number { float: right; } -.fc-rtl .fc-basic-view .fc-day-top .fc-day-number { float: left; } - -.fc-ltr .fc-basic-view .fc-day-top .fc-week-number { float: left; border-radius: 0 0 3px 0; } -.fc-rtl .fc-basic-view .fc-day-top .fc-week-number { float: right; border-radius: 0 0 0 3px; } - -.fc-basic-view .fc-day-top .fc-week-number { - min-width: 1.5em; - text-align: center; - background-color: #f2f2f2; - color: #808080; -} - -/* when week/day number have own column */ - -.fc-basic-view td.fc-week-number { - text-align: center; -} - -.fc-basic-view td.fc-week-number > * { - /* work around the way we do column resizing and ensure a minimum width */ - display: inline-block; - min-width: 1.25em; -} - - -/* AgendaView all-day area ---------------------------------------------------------------------------------------------------*/ - -.fc-agenda-view .fc-day-grid { - position: relative; - z-index: 2; /* so the "more.." popover will be over the time grid */ -} - -.fc-agenda-view .fc-day-grid .fc-row { - min-height: 3em; /* all-day section will never get shorter than this */ -} - -.fc-agenda-view .fc-day-grid .fc-row .fc-content-skeleton { - padding-bottom: 1em; /* give space underneath events for clicking/selecting days */ -} - - -/* TimeGrid axis running down the side (for both the all-day area and the slot area) ---------------------------------------------------------------------------------------------------*/ - -.fc .fc-axis { /* .fc to overcome default cell styles */ - vertical-align: middle; - padding: 0 4px; - white-space: nowrap; -} - -.fc-ltr .fc-axis { - text-align: right; -} - -.fc-rtl .fc-axis { - text-align: left; -} - -.ui-widget td.fc-axis { - font-weight: normal; /* overcome jqui theme making it bold */ -} - - -/* TimeGrid Structure ---------------------------------------------------------------------------------------------------*/ - -.fc-time-grid-container, /* so scroll container's z-index is below all-day */ -.fc-time-grid { /* so slats/bg/content/etc positions get scoped within here */ - position: relative; - z-index: 1; -} - -.fc-time-grid { - min-height: 100%; /* so if height setting is 'auto', .fc-bg stretches to fill height */ -} - -.fc-time-grid table { /* don't put outer borders on slats/bg/content/etc */ - border: 0 hidden transparent; -} - -.fc-time-grid > .fc-bg { - z-index: 1; -} - -.fc-time-grid .fc-slats, -.fc-time-grid > hr { /* the <hr> AgendaView injects when grid is shorter than scroller */ - position: relative; - z-index: 2; -} - -.fc-time-grid .fc-content-col { - position: relative; /* because now-indicator lives directly inside */ -} - -.fc-time-grid .fc-content-skeleton { - position: absolute; - z-index: 3; - top: 0; - left: 0; - right: 0; -} - -/* divs within a cell within the fc-content-skeleton */ - -.fc-time-grid .fc-business-container { - position: relative; - z-index: 1; -} - -.fc-time-grid .fc-bgevent-container { - position: relative; - z-index: 2; -} - -.fc-time-grid .fc-highlight-container { - position: relative; - z-index: 3; -} - -.fc-time-grid .fc-event-container { - position: relative; - z-index: 4; -} - -.fc-time-grid .fc-now-indicator-line { - z-index: 5; -} - -.fc-time-grid .fc-helper-container { /* also is fc-event-container */ - position: relative; - z-index: 6; -} - - -/* TimeGrid Slats (lines that run horizontally) ---------------------------------------------------------------------------------------------------*/ - -.fc-time-grid .fc-slats td { - height: 1.5em; - border-bottom: 0; /* each cell is responsible for its top border */ -} - -.fc-time-grid .fc-slats .fc-minor td { - border-top-style: dotted; -} - -.fc-time-grid .fc-slats .ui-widget-content { /* for jqui theme */ - background: none; /* see through to fc-bg */ -} - - -/* TimeGrid Highlighting Slots ---------------------------------------------------------------------------------------------------*/ - -.fc-time-grid .fc-highlight-container { /* a div within a cell within the fc-highlight-skeleton */ - position: relative; /* scopes the left/right of the fc-highlight to be in the column */ -} - -.fc-time-grid .fc-highlight { - position: absolute; - left: 0; - right: 0; - /* top and bottom will be in by JS */ -} - - -/* TimeGrid Event Containment ---------------------------------------------------------------------------------------------------*/ - -.fc-ltr .fc-time-grid .fc-event-container { /* space on the sides of events for LTR (default) */ - margin: 0 2.5% 0 2px; -} - -.fc-rtl .fc-time-grid .fc-event-container { /* space on the sides of events for RTL */ - margin: 0 2px 0 2.5%; -} - -.fc-time-grid .fc-event, -.fc-time-grid .fc-bgevent { - position: absolute; - z-index: 1; /* scope inner z-index's */ -} - -.fc-time-grid .fc-bgevent { - /* background events always span full width */ - left: 0; - right: 0; -} - - -/* Generic Vertical Event ---------------------------------------------------------------------------------------------------*/ - -.fc-v-event.fc-not-start { /* events that are continuing from another day */ - /* replace space made by the top border with padding */ - border-top-width: 0; - padding-top: 1px; - - /* remove top rounded corners */ - border-top-left-radius: 0; - border-top-right-radius: 0; -} - -.fc-v-event.fc-not-end { - /* replace space made by the top border with padding */ - border-bottom-width: 0; - padding-bottom: 1px; - - /* remove bottom rounded corners */ - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} - - -/* TimeGrid Event Styling ----------------------------------------------------------------------------------------------------- -We use the full "fc-time-grid-event" class instead of using descendants because the event won't -be a descendant of the grid when it is being dragged. -*/ - -.fc-time-grid-event { - overflow: hidden; /* don't let the bg flow over rounded corners */ -} - -.fc-time-grid-event.fc-selected { - /* need to allow touch resizers to extend outside event's bounding box */ - /* common fc-selected styles hide the fc-bg, so don't need this anyway */ - overflow: visible; -} - -.fc-time-grid-event.fc-selected .fc-bg { - display: none; /* hide semi-white background, to appear darker */ -} - -.fc-time-grid-event .fc-content { - overflow: hidden; /* for when .fc-selected */ -} - -.fc-time-grid-event .fc-time, -.fc-time-grid-event .fc-title { - padding: 0 1px; -} - -.fc-time-grid-event .fc-time { - font-size: .85em; - white-space: nowrap; -} - -/* short mode, where time and title are on the same line */ - -.fc-time-grid-event.fc-short .fc-content { - /* don't wrap to second line (now that contents will be inline) */ - white-space: nowrap; -} - -.fc-time-grid-event.fc-short .fc-time, -.fc-time-grid-event.fc-short .fc-title { - /* put the time and title on the same line */ - display: inline-block; - vertical-align: top; -} - -.fc-time-grid-event.fc-short .fc-time span { - display: none; /* don't display the full time text... */ -} - -.fc-time-grid-event.fc-short .fc-time:before { - content: attr(data-start); /* ...instead, display only the start time */ -} - -.fc-time-grid-event.fc-short .fc-time:after { - content: "\000A0-\000A0"; /* seperate with a dash, wrapped in nbsp's */ -} - -.fc-time-grid-event.fc-short .fc-title { - font-size: .85em; /* make the title text the same size as the time */ - padding: 0; /* undo padding from above */ -} - -/* resizer (cursor device) */ - -.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer { - left: 0; - right: 0; - bottom: 0; - height: 8px; - overflow: hidden; - line-height: 8px; - font-size: 11px; - font-family: monospace; - text-align: center; - cursor: s-resize; -} -.fc-time-grid-event.fc-allow-mouse-resize .fc-start-resizer { - top: 0; - bottom: auto; - cursor: n-resize; -} - -.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer:after { - content: "="; -} - -/* resizer (touch device) */ - -.fc-time-grid-event.fc-selected .fc-resizer { - /* 10x10 dot */ - border-radius: 5px; - border-width: 1px; - width: 8px; - height: 8px; - border-style: solid; - border-color: inherit; - background: #fff; - /* horizontally center */ - left: 50%; - margin-left: -5px; - /* center on the bottom edge */ - bottom: -5px; -} - - -/* Now Indicator ---------------------------------------------------------------------------------------------------*/ - -.fc-time-grid .fc-now-indicator-line { - border-top-width: 1px; - left: 0; - right: 0; -} - -/* arrow on axis */ - -.fc-time-grid .fc-now-indicator-arrow { - margin-top: -5px; /* vertically center on top coordinate */ -} - -.fc-ltr .fc-time-grid .fc-now-indicator-arrow { - left: 0; - /* triangle pointing right... */ - border-width: 5px 0 5px 6px; - border-top-color: transparent; - border-bottom-color: transparent; -} - -.fc-rtl .fc-time-grid .fc-now-indicator-arrow { - right: 0; - /* triangle pointing left... */ - border-width: 5px 6px 5px 0; - border-top-color: transparent; - border-bottom-color: transparent; -} - - - -/* List View ---------------------------------------------------------------------------------------------------*/ - -/* possibly reusable */ - -.fc-event-dot { - display: inline-block; - width: 10px; - height: 10px; - border-radius: 5px; -} - -/* view wrapper */ - -.fc-rtl .fc-list-view { - direction: rtl; /* unlike core views, leverage browser RTL */ -} - -.fc-list-view { - border-width: 1px; - border-style: solid; -} - -/* table resets */ - -.fc .fc-list-table { - table-layout: auto; /* for shrinkwrapping cell content */ -} - -.fc-list-table td { - border-width: 1px 0 0; - padding: 8px 14px; -} - -.fc-list-table tr:first-child td { - border-top-width: 0; -} - -/* day headings with the list */ - -.fc-list-heading { - border-bottom-width: 1px; -} - -.fc-list-heading td { - font-weight: bold; -} - -.fc-ltr .fc-list-heading-main { float: left; } -.fc-ltr .fc-list-heading-alt { float: right; } - -.fc-rtl .fc-list-heading-main { float: right; } -.fc-rtl .fc-list-heading-alt { float: left; } - -/* event list items */ - -.fc-list-item.fc-has-url { - cursor: pointer; /* whole row will be clickable */ -} - -.fc-list-item:hover td { - background-color: #f5f5f5; -} - -.fc-list-item-marker, -.fc-list-item-time { - white-space: nowrap; - width: 1px; -} - -/* make the dot closer to the event title */ -.fc-ltr .fc-list-item-marker { padding-right: 0; } -.fc-rtl .fc-list-item-marker { padding-left: 0; } - -.fc-list-item-title a { - /* every event title cell has an <a> tag */ - text-decoration: none; - color: inherit; -} - -.fc-list-item-title a[href]:hover { - /* hover effect only on titles with hrefs */ - text-decoration: underline; -} - -/* message when no events */ - -.fc-list-empty-wrap2 { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; -} - -.fc-list-empty-wrap1 { - width: 100%; - height: 100%; - display: table; -} - -.fc-list-empty { - display: table-cell; - vertical-align: middle; - text-align: center; -} - -.fc-unthemed .fc-list-empty { /* theme will provide own background */ - background-color: #eee; -} diff --git a/addons/web/static/lib/fullcalendar/daygrid/main.css b/addons/web/static/lib/fullcalendar/daygrid/main.css new file mode 100644 index 00000000000..37bc5538b5f --- /dev/null +++ b/addons/web/static/lib/fullcalendar/daygrid/main.css @@ -0,0 +1,78 @@ +/* DayGridView +--------------------------------------------------------------------------------------------------*/ +/* day row structure */ +.fc-dayGridWeek-view .fc-content-skeleton, +.fc-dayGridDay-view .fc-content-skeleton { + /* there may be week numbers in these views, so no padding-top */ + padding-bottom: 1em; + /* ensure a space at bottom of cell for user selecting/clicking */ +} + +.fc-dayGrid-view .fc-body .fc-row { + min-height: 4em; + /* ensure that all rows are at least this tall */ +} + +/* a "rigid" row will take up a constant amount of height because content-skeleton is absolute */ +.fc-row.fc-rigid { + overflow: hidden; +} + +.fc-row.fc-rigid .fc-content-skeleton { + position: absolute; + top: 0; + left: 0; + right: 0; +} + +/* week and day number styling */ +.fc-day-top.fc-other-month { + opacity: 0.3; +} + +.fc-dayGrid-view .fc-week-number, +.fc-dayGrid-view .fc-day-number { + padding: 2px; +} + +.fc-dayGrid-view th.fc-week-number, +.fc-dayGrid-view th.fc-day-number { + padding: 0 2px; + /* column headers can't have as much v space */ +} + +.fc-ltr .fc-dayGrid-view .fc-day-top .fc-day-number { + float: right; +} + +.fc-rtl .fc-dayGrid-view .fc-day-top .fc-day-number { + float: left; +} + +.fc-ltr .fc-dayGrid-view .fc-day-top .fc-week-number { + float: left; + border-radius: 0 0 3px 0; +} + +.fc-rtl .fc-dayGrid-view .fc-day-top .fc-week-number { + float: right; + border-radius: 0 0 0 3px; +} + +.fc-dayGrid-view .fc-day-top .fc-week-number { + min-width: 1.5em; + text-align: center; + background-color: #f2f2f2; + color: #808080; +} + +/* when week/day number have own column */ +.fc-dayGrid-view td.fc-week-number { + text-align: center; +} + +.fc-dayGrid-view td.fc-week-number > * { + /* work around the way we do column resizing and ensure a minimum width */ + display: inline-block; + min-width: 1.25em; +}
\ No newline at end of file diff --git a/addons/web/static/lib/fullcalendar/daygrid/main.js b/addons/web/static/lib/fullcalendar/daygrid/main.js new file mode 100644 index 00000000000..b49fa3176bd --- /dev/null +++ b/addons/web/static/lib/fullcalendar/daygrid/main.js @@ -0,0 +1,1668 @@ +/*! +FullCalendar Day Grid Plugin v4.4.0 +Docs & License: https://fullcalendar.io/ +(c) 2019 Adam Shaw +*/ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@fullcalendar/core')) : + typeof define === 'function' && define.amd ? define(['exports', '@fullcalendar/core'], factory) : + (global = global || self, factory(global.FullCalendarDayGrid = {}, global.FullCalendar)); +}(this, function (exports, core) { 'use strict'; + + /*! *****************************************************************************
+ Copyright (c) Microsoft Corporation. All rights reserved.
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+ this file except in compliance with the License. You may obtain a copy of the
+ License at http://www.apache.org/licenses/LICENSE-2.0
+
+ THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
+ WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+ MERCHANTABLITY OR NON-INFRINGEMENT.
+
+ See the Apache Version 2.0 License for specific language governing permissions
+ and limitations under the License.
+ ***************************************************************************** */
+ /* global Reflect, Promise */
+
+ var extendStatics = function(d, b) {
+ extendStatics = Object.setPrototypeOf ||
+ ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+ function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+ return extendStatics(d, b);
+ };
+
+ function __extends(d, b) {
+ extendStatics(d, b);
+ function __() { this.constructor = d; }
+ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+ }
+
+ var __assign = function() {
+ __assign = Object.assign || function __assign(t) {
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
+ s = arguments[i];
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
+ }
+ return t;
+ };
+ return __assign.apply(this, arguments);
+ }; + + var DayGridDateProfileGenerator = /** @class */ (function (_super) { + __extends(DayGridDateProfileGenerator, _super); + function DayGridDateProfileGenerator() { + return _super !== null && _super.apply(this, arguments) || this; + } + // Computes the date range that will be rendered. + DayGridDateProfileGenerator.prototype.buildRenderRange = function (currentRange, currentRangeUnit, isRangeAllDay) { + var dateEnv = this.dateEnv; + var renderRange = _super.prototype.buildRenderRange.call(this, currentRange, currentRangeUnit, isRangeAllDay); + var start = renderRange.start; + var end = renderRange.end; + var endOfWeek; + // year and month views should be aligned with weeks. this is already done for week + if (/^(year|month)$/.test(currentRangeUnit)) { + start = dateEnv.startOfWeek(start); + // make end-of-week if not already + endOfWeek = dateEnv.startOfWeek(end); + if (endOfWeek.valueOf() !== end.valueOf()) { + end = core.addWeeks(endOfWeek, 1); + } + } + // ensure 6 weeks + if (this.options.monthMode && + this.options.fixedWeekCount) { + var rowCnt = Math.ceil(// could be partial weeks due to hiddenDays + core.diffWeeks(start, end)); + end = core.addWeeks(end, 6 - rowCnt); + } + return { start: start, end: end }; + }; + return DayGridDateProfileGenerator; + }(core.DateProfileGenerator)); + + /* A rectangular panel that is absolutely positioned over other content + ------------------------------------------------------------------------------------------------------------------------ + Options: + - className (string) + - content (HTML string, element, or element array) + - 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 */ (function () { + function Popover(options) { + var _this = this; + this.isHidden = true; + this.margin = 10; // the space required between the popover and the edges of the scroll container + // Triggered when the user clicks *anywhere* in the document, for the autoHide feature + this.documentMousedown = function (ev) { + // only hide the popover if the click happened outside the popover + if (_this.el && !_this.el.contains(ev.target)) { + _this.hide(); + } + }; + this.options = options; + } + // Shows the popover on the specified position. Renders it if not already + Popover.prototype.show = function () { + if (this.isHidden) { + if (!this.el) { + this.render(); + } + this.el.style.display = ''; + this.position(); + this.isHidden = false; + this.trigger('show'); + } + }; + // Hides the popover, through CSS, but does not remove it from the DOM + Popover.prototype.hide = function () { + if (!this.isHidden) { + this.el.style.display = 'none'; + this.isHidden = true; + this.trigger('hide'); + } + }; + // Creates `this.el` and renders content inside of it + Popover.prototype.render = function () { + var _this = this; + var options = this.options; + var el = this.el = core.createElement('div', { + className: 'fc-popover ' + (options.className || ''), + style: { + top: '0', + left: '0' + } + }); + if (typeof options.content === 'function') { + options.content(el); + } + options.parentEl.appendChild(el); + // when a click happens on anything inside with a 'fc-close' className, hide the popover + core.listenBySelector(el, 'click', '.fc-close', function (ev) { + _this.hide(); + }); + if (options.autoHide) { + document.addEventListener('mousedown', this.documentMousedown); + } + }; + // Hides and unregisters any handlers + Popover.prototype.destroy = function () { + this.hide(); + if (this.el) { + core.removeElement(this.el); + this.el = null; + } + document.removeEventListener('mousedown', this.documentMousedown); + }; + // Positions the popover optimally, using the top/left/right options + Popover.prototype.position = function () { + var options = this.options; + var el = this.el; + var elDims = el.getBoundingClientRect(); // only used for width,height + var origin = core.computeRect(el.offsetParent); + var clippingRect = core.computeClippingRect(options.parentEl); + 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 - elDims.width; // derive the left value from the right value + } + else { + left = 0; + } + // constrain to the view port. if constrained by two edges, give precedence to top/left + top = Math.min(top, clippingRect.bottom - elDims.height - this.margin); + top = Math.max(top, clippingRect.top + this.margin); + left = Math.min(left, clippingRect.right - elDims.width - this.margin); + left = Math.max(left, clippingRect.left + this.margin); + core.applyStyle(el, { + 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 + // can kill this??? + Popover.prototype.trigger = function (name) { + if (this.options[name]) { + this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)); + } + }; + return Popover; + }()); + + /* Event-rendering methods for the DayGrid class + ----------------------------------------------------------------------------------------------------------------------*/ + // "Simple" is bad a name. has nothing to do with SimpleDayGrid + var SimpleDayGridEventRenderer = /** @class */ (function (_super) { + __extends(SimpleDayGridEventRenderer, _super); + function SimpleDayGridEventRenderer() { + return _super !== null && _super.apply(this, arguments) || this; + } + // Builds the HTML to be used for the default element for an individual segment + SimpleDayGridEventRenderer.prototype.renderSegHtml = function (seg, mirrorInfo) { + var context = this.context; + var eventRange = seg.eventRange; + var eventDef = eventRange.def; + var eventUi = eventRange.ui; + var allDay = eventDef.allDay; + var isDraggable = core.computeEventDraggable(context, eventDef, eventUi); + var isResizableFromStart = allDay && seg.isStart && core.computeEventStartResizable(context, eventDef, eventUi); + var isResizableFromEnd = allDay && seg.isEnd && core.computeEventEndResizable(context, eventDef, eventUi); + var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd, mirrorInfo); + var skinCss = core.cssToStr(this.getSkinCss(eventUi)); + 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.getTimeText(eventRange); + if (timeText) { + timeHtml = '<span class="fc-time">' + core.htmlEscape(timeText) + '</span>'; + } + } + titleHtml = + '<span class="fc-title">' + + (core.htmlEscape(eventDef.title || '') || ' ') + // we always want one line of height + '</span>'; + return '<a class="' + classes.join(' ') + '"' + + (eventDef.url ? + ' href="' + core.htmlEscape(eventDef.url) + '"' : + '') + + (skinCss ? + ' style="' + skinCss + '"' : + '') + + '>' + + '<div class="fc-content">' + + (context.options.dir === 'rtl' ? + titleHtml + ' ' + timeHtml : // put a natural space in between + timeHtml + ' ' + titleHtml // + ) + + '</div>' + + (isResizableFromStart ? + '<div class="fc-resizer fc-start-resizer"></div>' : + '') + + (isResizableFromEnd ? + '<div class="fc-resizer fc-end-resizer"></div>' : + '') + + '</a>'; + }; + // Computes a default event time formatting string if `eventTimeFormat` is not explicitly defined + SimpleDayGridEventRenderer.prototype.computeEventTimeFormat = function () { + return { + hour: 'numeric', + minute: '2-digit', + omitZeroMinute: true, + meridiem: 'narrow' + }; + }; + SimpleDayGridEventRenderer.prototype.computeDisplayEventEnd = function () { + return false; // TODO: somehow consider the originating DayGrid's column count + }; + return SimpleDayGridEventRenderer; + }(core.FgEventRenderer)); + + /* Event-rendering methods for the DayGrid class + ----------------------------------------------------------------------------------------------------------------------*/ + var DayGridEventRenderer = /** @class */ (function (_super) { + __extends(DayGridEventRenderer, _super); + function DayGridEventRenderer(dayGrid) { + var _this = _super.call(this) || this; + _this.dayGrid = dayGrid; + return _this; + } + // Renders the given foreground event segments onto the grid + DayGridEventRenderer.prototype.attachSegs = function (segs, mirrorInfo) { + var rowStructs = this.rowStructs = this.renderSegRows(segs); + // append to each row's content skeleton + this.dayGrid.rowEls.forEach(function (rowNode, i) { + rowNode.querySelector('.fc-content-skeleton > table').appendChild(rowStructs[i].tbodyEl); + }); + // removes the "more.." events popover + if (!mirrorInfo) { + this.dayGrid.removeSegPopover(); + } + }; + // Unrenders all currently rendered foreground event segments + DayGridEventRenderer.prototype.detachSegs = function () { + var rowStructs = this.rowStructs || []; + var rowStruct; + while ((rowStruct = rowStructs.pop())) { + core.removeElement(rowStruct.tbodyEl); + } + this.rowStructs = null; + }; + // Uses the given events array to generate <tbody> 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` + DayGridEventRenderer.prototype.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; + }; + // Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains + // the segments. Returns object with a bunch of internal data about how the render was calculated. + // NOTE: modifies rowSegs + DayGridEventRenderer.prototype.renderSegRow = function (row, rowSegs) { + var isRtl = this.context.isRtl; + var dayGrid = this.dayGrid; + var colCnt = dayGrid.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 = document.createElement('tbody'); + var segMatrix = []; // lookup for which segments are rendered into which level+col cells + var cellMatrix = []; // lookup for all <td> elements of the level+col matrix + var loneCellMatrix = []; // lookup for <td> elements that only take up a single column + var i; + var levelSegs; + var col; + var tr; + var j; + var 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.rowSpan = (td.rowSpan || 1) + 1; + } + else { + td = document.createElement('td'); + tr.appendChild(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 = document.createElement('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]; + var leftCol = isRtl ? (colCnt - 1 - seg.lastCol) : seg.firstCol; + var rightCol = isRtl ? (colCnt - 1 - seg.firstCol) : seg.lastCol; + emptyCellsUntil(leftCol); + // create a container that occupies or more columns. append the event element. + td = core.createElement('td', { className: 'fc-event-container' }, seg.el); + if (leftCol !== rightCol) { + td.colSpan = rightCol - leftCol + 1; + } + else { // a single-column segment + loneCellMatrix[i][col] = td; + } + while (col <= rightCol) { + cellMatrix[i][col] = td; + segMatrix[i][col] = seg; + col++; + } + tr.appendChild(td); + } + } + emptyCellsUntil(colCnt); // finish off the row + var introHtml = dayGrid.renderProps.renderIntroHtml(); + if (introHtml) { + if (isRtl) { + core.appendToElement(tr, introHtml); + } + else { + core.prependToElement(tr, introHtml); + } + } + tbody.appendChild(tr); + } + return { + row: row, + 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 + DayGridEventRenderer.prototype.buildSegLevels = function (segs) { + var isRtl = this.context.isRtl; + var colCnt = this.dayGrid.colCnt; + var levels = []; + var i; + var seg; + var j; + // Give preference to elements with certain criteria, so they have + // a chance to be closer to the top. + segs = 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; + seg.leftCol = isRtl ? (colCnt - 1 - seg.lastCol) : seg.firstCol; // for sorting only + seg.rightCol = isRtl ? (colCnt - 1 - seg.firstCol) : seg.lastCol // for sorting only + ; + (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 + DayGridEventRenderer.prototype.groupSegRows = function (segs) { + var segRows = []; + var i; + for (i = 0; i < this.dayGrid.rowCnt; i++) { + segRows.push([]); + } + for (i = 0; i < segs.length; i++) { + segRows[segs[i].row].push(segs[i]); + } + return segRows; + }; + // Computes a default `displayEventEnd` value if one is not expliclty defined + DayGridEventRenderer.prototype.computeDisplayEventEnd = function () { + return this.dayGrid.colCnt === 1; // we'll likely have space if there's only one day + }; + return DayGridEventRenderer; + }(SimpleDayGridEventRenderer)); + // Computes whether two segments' columns collide. They are assumed to be in the same row. + function isDaySegCollision(seg, otherSegs) { + var i; + var otherSeg; + for (i = 0; i < otherSegs.length; i++) { + otherSeg = otherSegs[i]; + if (otherSeg.firstCol <= seg.lastCol && + otherSeg.lastCol >= seg.firstCol) { + return true; + } + } + return false; + } + // A cmp function for determining the leftmost event + function compareDaySegCols(a, b) { + return a.leftCol - b.leftCol; + } + + var DayGridMirrorRenderer = /** @class */ (function (_super) { + __extends(DayGridMirrorRenderer, _super); + function DayGridMirrorRenderer() { + return _super !== null && _super.apply(this, arguments) || this; + } + DayGridMirrorRenderer.prototype.attachSegs = function (segs, mirrorInfo) { + var sourceSeg = mirrorInfo.sourceSeg; + var rowStructs = this.rowStructs = this.renderSegRows(segs); + // inject each new event skeleton into each associated row + this.dayGrid.rowEls.forEach(function (rowNode, row) { + var skeletonEl = core.htmlToElement('<div class="fc-mirror-skeleton"><table></table></div>'); // will be absolutely positioned + var skeletonTopEl; + 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) { + skeletonTopEl = sourceSeg.el; + } + else { + skeletonTopEl = rowNode.querySelector('.fc-content-skeleton tbody'); + if (!skeletonTopEl) { // when no events + skeletonTopEl = rowNode.querySelector('.fc-content-skeleton table'); + } + } + skeletonTop = skeletonTopEl.getBoundingClientRect().top - + rowNode.getBoundingClientRect().top; // the offsetParent origin + skeletonEl.style.top = skeletonTop + 'px'; + skeletonEl.querySelector('table').appendChild(rowStructs[row].tbodyEl); + rowNode.appendChild(skeletonEl); + }); + }; + return DayGridMirrorRenderer; + }(DayGridEventRenderer)); + + var EMPTY_CELL_HTML = '<td style="pointer-events:none"></td>'; + var DayGridFillRenderer = /** @class */ (function (_super) { + __extends(DayGridFillRenderer, _super); + function DayGridFillRenderer(dayGrid) { + var _this = _super.call(this) || this; + _this.fillSegTag = 'td'; // override the default tag name + _this.dayGrid = dayGrid; + return _this; + } + DayGridFillRenderer.prototype.renderSegs = function (type, context, segs) { + // don't render timed background events + if (type === 'bgEvent') { + segs = segs.filter(function (seg) { + return seg.eventRange.def.allDay; + }); + } + _super.prototype.renderSegs.call(this, type, context, segs); + }; + DayGridFillRenderer.prototype.attachSegs = function (type, segs) { + var els = []; + var i; + var seg; + var skeletonEl; + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + skeletonEl = this.renderFillRow(type, seg); + this.dayGrid.rowEls[seg.row].appendChild(skeletonEl); + els.push(skeletonEl); + } + return els; + }; + // Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered. + DayGridFillRenderer.prototype.renderFillRow = function (type, seg) { + var dayGrid = this.dayGrid; + var isRtl = this.context.isRtl; + var colCnt = dayGrid.colCnt; + var leftCol = isRtl ? (colCnt - 1 - seg.lastCol) : seg.firstCol; + var rightCol = isRtl ? (colCnt - 1 - seg.firstCol) : seg.lastCol; + var startCol = leftCol; + var endCol = rightCol + 1; + var className; + var skeletonEl; + var trEl; + if (type === 'businessHours') { + className = 'bgevent'; + } + else { + className = type.toLowerCase(); + } + skeletonEl = core.htmlToElement('<div class="fc-' + className + '-skeleton">' + + '<table><tr></tr></table>' + + '</div>'); + trEl = skeletonEl.getElementsByTagName('tr')[0]; + if (startCol > 0) { + core.appendToElement(trEl, + // will create (startCol + 1) td's + new Array(startCol + 1).join(EMPTY_CELL_HTML)); + } + seg.el.colSpan = endCol - startCol; + trEl.appendChild(seg.el); + if (endCol < colCnt) { + core.appendToElement(trEl, + // will create (colCnt - endCol) td's + new Array(colCnt - endCol + 1).join(EMPTY_CELL_HTML)); + } + var introHtml = dayGrid.renderProps.renderIntroHtml(); + if (introHtml) { + if (isRtl) { + core.appendToElement(trEl, introHtml); + } + else { + core.prependToElement(trEl, introHtml); + } + } + return skeletonEl; + }; + return DayGridFillRenderer; + }(core.FillRenderer)); + + var DayTile = /** @class */ (function (_super) { + __extends(DayTile, _super); + function DayTile(el) { + var _this = _super.call(this, el) || this; + var eventRenderer = _this.eventRenderer = new DayTileEventRenderer(_this); + var renderFrame = _this.renderFrame = core.memoizeRendering(_this._renderFrame); + _this.renderFgEvents = core.memoizeRendering(eventRenderer.renderSegs.bind(eventRenderer), eventRenderer.unrender.bind(eventRenderer), [renderFrame]); + _this.renderEventSelection = core.memoizeRendering(eventRenderer.selectByInstanceId.bind(eventRenderer), eventRenderer.unselectByInstanceId.bind(eventRenderer), [_this.renderFgEvents]); + _this.renderEventDrag = core.memoizeRendering(eventRenderer.hideByHash.bind(eventRenderer), eventRenderer.showByHash.bind(eventRenderer), [renderFrame]); + _this.renderEventResize = core.memoizeRendering(eventRenderer.hideByHash.bind(eventRenderer), eventRenderer.showByHash.bind(eventRenderer), [renderFrame]); + return _this; + } + DayTile.prototype.firstContext = function (context) { + context.calendar.registerInteractiveComponent(this, { + el: this.el, + useEventCenter: false + }); + }; + DayTile.prototype.render = function (props, context) { + this.renderFrame(props.date); + this.renderFgEvents(context, props.fgSegs); + this.renderEventSelection(props.eventSelection); + this.renderEventDrag(props.eventDragInstances); + this.renderEventResize(props.eventResizeInstances); + }; + DayTile.prototype.destroy = function () { + _super.prototype.destroy.call(this); + this.renderFrame.unrender(); // should unrender everything else + this.context.calendar.unregisterInteractiveComponent(this); + }; + DayTile.prototype._renderFrame = function (date) { + var _a = this.context, theme = _a.theme, dateEnv = _a.dateEnv, options = _a.options; + var title = dateEnv.format(date, core.createFormatter(options.dayPopoverFormat) // TODO: cache + ); + this.el.innerHTML = + '<div class="fc-header ' + theme.getClass('popoverHeader') + '">' + + '<span class="fc-title">' + + core.htmlEscape(title) + + '</span>' + + '<span class="fc-close ' + theme.getIconClass('close') + '"></span>' + + '</div>' + + '<div class="fc-body ' + theme.getClass('popoverContent') + '">' + + '<div class="fc-event-container"></div>' + + '</div>'; + this.segContainerEl = this.el.querySelector('.fc-event-container'); + }; + DayTile.prototype.queryHit = function (positionLeft, positionTop, elWidth, elHeight) { + var date = this.props.date; // HACK + if (positionLeft < elWidth && positionTop < elHeight) { + return { + component: this, + dateSpan: { + allDay: true, + range: { start: date, end: core.addDays(date, 1) } + }, + dayEl: this.el, + rect: { + left: 0, + top: 0, + right: elWidth, + bottom: elHeight + }, + layer: 1 + }; + } + }; + return DayTile; + }(core.DateComponent)); + var DayTileEventRenderer = /** @class */ (function (_super) { + __extends(DayTileEventRenderer, _super); + function DayTileEventRenderer(dayTile) { + var _this = _super.call(this) || this; + _this.dayTile = dayTile; + return _this; + } + DayTileEventRenderer.prototype.attachSegs = function (segs) { + for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) { + var seg = segs_1[_i]; + this.dayTile.segContainerEl.appendChild(seg.el); + } + }; + DayTileEventRenderer.prototype.detachSegs = function (segs) { + for (var _i = 0, segs_2 = segs; _i < segs_2.length; _i++) { + var seg = segs_2[_i]; + core.removeElement(seg.el); + } + }; + return DayTileEventRenderer; + }(SimpleDayGridEventRenderer)); + + var DayBgRow = /** @class */ (function () { + function DayBgRow(context) { + this.context = context; + } + DayBgRow.prototype.renderHtml = function (props) { + var parts = []; + if (props.renderIntroHtml) { + parts.push(props.renderIntroHtml()); + } + for (var _i = 0, _a = props.cells; _i < _a.length; _i++) { + var cell = _a[_i]; + parts.push(renderCellHtml(cell.date, props.dateProfile, this.context, cell.htmlAttrs)); + } + if (!props.cells.length) { + parts.push('<td class="fc-day ' + this.context.theme.getClass('widgetContent') + '"></td>'); + } + if (this.context.options.dir === 'rtl') { + parts.reverse(); + } + return '<tr>' + parts.join('') + '</tr>'; + }; + return DayBgRow; + }()); + function renderCellHtml(date, dateProfile, context, otherAttrs) { + var dateEnv = context.dateEnv, theme = context.theme; + var isDateValid = core.rangeContainsMarker(dateProfile.activeRange, date); // TODO: called too frequently. cache somehow. + var classes = core.getDayClasses(date, dateProfile, context); + classes.unshift('fc-day', theme.getClass('widgetContent')); + return '<td class="' + classes.join(' ') + '"' + + (isDateValid ? + ' data-date="' + dateEnv.formatIso(date, { omitTime: true }) + '"' : + '') + + (otherAttrs ? + ' ' + otherAttrs : + '') + + '></td>'; + } + + var DAY_NUM_FORMAT = core.createFormatter({ day: 'numeric' }); + var WEEK_NUM_FORMAT = core.createFormatter({ week: 'numeric' }); + var DayGrid = /** @class */ (function (_super) { + __extends(DayGrid, _super); + function DayGrid(el, renderProps) { + var _this = _super.call(this, el) || this; + _this.bottomCoordPadding = 0; // hack for extending the hit area for the last row of the coordinate grid + _this.isCellSizesDirty = false; + _this.renderProps = renderProps; + var eventRenderer = _this.eventRenderer = new DayGridEventRenderer(_this); + var fillRenderer = _this.fillRenderer = new DayGridFillRenderer(_this); + _this.mirrorRenderer = new DayGridMirrorRenderer(_this); + var renderCells = _this.renderCells = core.memoizeRendering(_this._renderCells, _this._unrenderCells); + _this.renderBusinessHours = core.memoizeRendering(fillRenderer.renderSegs.bind(fillRenderer, 'businessHours'), fillRenderer.unrender.bind(fillRenderer, 'businessHours'), [renderCells]); + _this.renderDateSelection = core.memoizeRendering(fillRenderer.renderSegs.bind(fillRenderer, 'highlight'), fillRenderer.unrender.bind(fillRenderer, 'highlight'), [renderCells]); + _this.renderBgEvents = core.memoizeRendering(fillRenderer.renderSegs.bind(fillRenderer, 'bgEvent'), fillRenderer.unrender.bind(fillRenderer, 'bgEvent'), [renderCells]); + _this.renderFgEvents = core.memoizeRendering(eventRenderer.renderSegs.bind(eventRenderer), eventRenderer.unrender.bind(eventRenderer), [renderCells]); + _this.renderEventSelection = core.memoizeRendering(eventRenderer.selectByInstanceId.bind(eventRenderer), eventRenderer.unselectByInstanceId.bind(eventRenderer), [_this.renderFgEvents]); + _this.renderEventDrag = core.memoizeRendering(_this._renderEventDrag, _this._unrenderEventDrag, [renderCells]); + _this.renderEventResize = core.memoizeRendering(_this._renderEventResize, _this._unrenderEventResize, [renderCells]); + return _this; + } + DayGrid.prototype.render = function (props, context) { + var cells = props.cells; + this.rowCnt = cells.length; + this.colCnt = cells[0].length; + this.renderCells(cells, props.isRigid); + this.renderBusinessHours(context, props.businessHourSegs); + this.renderDateSelection(context, props.dateSelectionSegs); + this.renderBgEvents(context, props.bgEventSegs); + this.renderFgEvents(context, props.fgEventSegs); + this.renderEventSelection(props.eventSelection); + this.renderEventDrag(props.eventDrag); + this.renderEventResize(props.eventResize); + if (this.segPopoverTile) { + this.updateSegPopoverTile(); + } + }; + DayGrid.prototype.destroy = function () { + _super.prototype.destroy.call(this); + this.renderCells.unrender(); // will unrender everything else + }; + DayGrid.prototype.getCellRange = function (row, col) { + var start = this.props.cells[row][col].date; + var end = core.addDays(start, 1); + return { start: start, end: end }; + }; + DayGrid.prototype.updateSegPopoverTile = function (date, segs) { + var ownProps = this.props; + this.segPopoverTile.receiveProps({ + date: date || this.segPopoverTile.props.date, + fgSegs: segs || this.segPopoverTile.props.fgSegs, + eventSelection: ownProps.eventSelection, + eventDragInstances: ownProps.eventDrag ? ownProps.eventDrag.affectedInstances : null, + eventResizeInstances: ownProps.eventResize ? ownProps.eventResize.affectedInstances : null + }, this.context); + }; + /* Date Rendering + ------------------------------------------------------------------------------------------------------------------*/ + DayGrid.prototype._renderCells = function (cells, isRigid) { + var _a = this.context, calendar = _a.calendar, view = _a.view, isRtl = _a.isRtl, dateEnv = _a.dateEnv; + var _b = this, rowCnt = _b.rowCnt, colCnt = _b.colCnt; + var html = ''; + var row; + var col; + for (row = 0; row < rowCnt; row++) { + html += this.renderDayRowHtml(row, isRigid); + } + this.el.innerHTML = html; + this.rowEls = core.findElements(this.el, '.fc-row'); + this.cellEls = core.findElements(this.el, '.fc-day, .fc-disabled-day'); + if (isRtl) { + this.cellEls.reverse(); + } + this.rowPositions = new core.PositionCache(this.el, this.rowEls, false, true // vertical + ); + this.colPositions = new core.PositionCache(this.el, this.cellEls.slice(0, colCnt), // only the first row + true, false // horizontal + ); + // trigger dayRender with each cell's element + for (row = 0; row < rowCnt; row++) { + for (col = 0; col < colCnt; col++) { + calendar.publiclyTrigger('dayRender', [ + { + date: dateEnv.toDate(cells[row][col].date), + el: this.getCellEl(row, col), + view: view + } + ]); + } + } + this.isCellSizesDirty = true; + }; + DayGrid.prototype._unrenderCells = function () { + this.removeSegPopover(); + }; + // Generates the HTML for a single row, which is a div that wraps a table. + // `row` is the row number. + DayGrid.prototype.renderDayRowHtml = function (row, isRigid) { + var theme = this.context.theme; + var classes = ['fc-row', 'fc-week', theme.getClass('dayRow')]; + if (isRigid) { + classes.push('fc-rigid'); + } + var bgRow = new DayBgRow(this.context); + return '' + + '<div class="' + classes.join(' ') + '">' + + '<div class="fc-bg">' + + '<table class="' + theme.getClass('tableGrid') + '">' + + bgRow.renderHtml({ + cells: this.props.cells[row], + dateProfile: this.props.dateProfile, + renderIntroHtml: this.renderProps.renderBgIntroHtml + }) + + '</table>' + + '</div>' + + '<div class="fc-content-skeleton">' + + '<table>' + + (this.getIsNumbersVisible() ? + '<thead>' + + this.renderNumberTrHtml(row) + + '</thead>' : + '') + + '</table>' + + '</div>' + + '</div>'; + }; + DayGrid.prototype.getIsNumbersVisible = function () { + return this.getIsDayNumbersVisible() || + this.renderProps.cellWeekNumbersVisible || + this.renderProps.colWeekNumbersVisible; + }; + DayGrid.prototype.getIsDayNumbersVisible = function () { + return this.rowCnt > 1; + }; + /* Grid Number Rendering + ------------------------------------------------------------------------------------------------------------------*/ + DayGrid.prototype.renderNumberTrHtml = function (row) { + var isRtl = this.context.isRtl; + var intro = this.renderProps.renderNumberIntroHtml(row, this); + return '' + + '<tr>' + + (isRtl ? '' : intro) + + this.renderNumberCellsHtml(row) + + (isRtl ? intro : '') + + '</tr>'; + }; + DayGrid.prototype.renderNumberCellsHtml = function (row) { + var htmls = []; + var col; + var date; + for (col = 0; col < this.colCnt; col++) { + date = this.props.cells[row][col].date; + htmls.push(this.renderNumberCellHtml(date)); + } + if (this.context.isRtl) { + htmls.reverse(); + } + return htmls.join(''); + }; + // Generates the HTML for the <td>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. + DayGrid.prototype.renderNumberCellHtml = function (date) { + var _a = this.context, dateEnv = _a.dateEnv, options = _a.options; + var html = ''; + var isDateValid = core.rangeContainsMarker(this.props.dateProfile.activeRange, date); // TODO: called too frequently. cache somehow. + var isDayNumberVisible = this.getIsDayNumbersVisible() && isDateValid; + var classes; + var weekCalcFirstDow; + if (!isDayNumberVisible && !this.renderProps.cellWeekNumbersVisible) { + // no numbers in day cell (week number must be along the side) + return '<td></td>'; // will create an empty space above events :( + } + classes = core.getDayClasses(date, this.props.dateProfile, this.context); + classes.unshift('fc-day-top'); + if (this.renderProps.cellWeekNumbersVisible) { + weekCalcFirstDow = dateEnv.weekDow; + } + html += '<td class="' + classes.join(' ') + '"' + + (isDateValid ? + ' data-date="' + dateEnv.formatIso(date, { omitTime: true }) + '"' : + '') + + '>'; + if (this.renderProps.cellWeekNumbersVisible && (date.getUTCDay() === weekCalcFirstDow)) { + html += core.buildGotoAnchorHtml(options, dateEnv, { date: date, type: 'week' }, { 'class': 'fc-week-number' }, dateEnv.format(date, WEEK_NUM_FORMAT) // inner HTML + ); + } + if (isDayNumberVisible) { + html += core.buildGotoAnchorHtml(options, dateEnv, date, { 'class': 'fc-day-number' }, dateEnv.format(date, DAY_NUM_FORMAT) // inner HTML + ); + } + html += '</td>'; + return html; + }; + /* Sizing + ------------------------------------------------------------------------------------------------------------------*/ + DayGrid.prototype.updateSize = function (isResize) { + var calendar = this.context.calendar; + var _a = this, fillRenderer = _a.fillRenderer, eventRenderer = _a.eventRenderer, mirrorRenderer = _a.mirrorRenderer; + if (isResize || + this.isCellSizesDirty || + calendar.isEventsUpdated // hack + ) { + this.buildPositionCaches(); + this.isCellSizesDirty = false; + } + fillRenderer.computeSizes(isResize); + eventRenderer.computeSizes(isResize); + mirrorRenderer.computeSizes(isResize); + fillRenderer.assignSizes(isResize); + eventRenderer.assignSizes(isResize); + mirrorRenderer.assignSizes(isResize); + }; + DayGrid.prototype.buildPositionCaches = function () { + this.buildColPositions(); + this.buildRowPositions(); + }; + DayGrid.prototype.buildColPositions = function () { + this.colPositions.build(); + }; + DayGrid.prototype.buildRowPositions = function () { + this.rowPositions.build(); + this.rowPositions.bottoms[this.rowCnt - 1] += this.bottomCoordPadding; // hack + }; + /* Hit System + ------------------------------------------------------------------------------------------------------------------*/ + DayGrid.prototype.positionToHit = function (leftPosition, topPosition) { + var _a = this, colPositions = _a.colPositions, rowPositions = _a.rowPositions; + var col = colPositions.leftToIndex(leftPosition); + var row = rowPositions.topToIndex(topPosition); + if (row != null && col != null) { + return { + row: row, + col: col, + dateSpan: { + range: this.getCellRange(row, col), + allDay: true + }, + dayEl: this.getCellEl(row, col), + relativeRect: { + left: colPositions.lefts[col], + right: colPositions.rights[col], + top: rowPositions.tops[row], + bottom: rowPositions.bottoms[row] + } + }; + } + }; + /* Cell System + ------------------------------------------------------------------------------------------------------------------*/ + // FYI: the first column is the leftmost column, regardless of date + DayGrid.prototype.getCellEl = function (row, col) { + return this.cellEls[row * this.colCnt + col]; + }; + /* Event Drag Visualization + ------------------------------------------------------------------------------------------------------------------*/ + DayGrid.prototype._renderEventDrag = function (state) { + if (state) { + this.eventRenderer.hideByHash(state.affectedInstances); + this.fillRenderer.renderSegs('highlight', this.context, state.segs); + } + }; + DayGrid.prototype._unrenderEventDrag = function (state) { + if (state) { + this.eventRenderer.showByHash(state.affectedInstances); + this.fillRenderer.unrender('highlight', this.context); + } + }; + /* Event Resize Visualization + ------------------------------------------------------------------------------------------------------------------*/ + DayGrid.prototype._renderEventResize = function (state) { + if (state) { + this.eventRenderer.hideByHash(state.affectedInstances); + this.fillRenderer.renderSegs('highlight', this.context, state.segs); + this.mirrorRenderer.renderSegs(this.context, state.segs, { isResizing: true, sourceSeg: state.sourceSeg }); + } + }; + DayGrid.prototype._unrenderEventResize = function (state) { + if (state) { + this.eventRenderer.showByHash(state.affectedInstances); + this.fillRenderer.unrender('highlight', this.context); + this.mirrorRenderer.unrender(this.context, state.segs, { isResizing: true, sourceSeg: state.sourceSeg }); + } + }; + /* More+ Link Popover + ------------------------------------------------------------------------------------------------------------------*/ + DayGrid.prototype.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). + DayGrid.prototype.limitRows = function (levelLimit) { + var rowStructs = this.eventRenderer.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. + DayGrid.prototype.computeRowLevelLimit = function (row) { + var rowEl = this.rowEls[row]; // the containing "fake" row div + var rowBottom = rowEl.getBoundingClientRect().bottom; // relative to viewport! + var trEls = core.findChildren(this.eventRenderer.rowStructs[row].tbodyEl); + var i; + var trEl; + // Reveal one level <tr> at a time and stop when we find one out of bounds + for (i = 0; i < trEls.length; i++) { + trEl = trEls[i]; + trEl.classList.remove('fc-limited'); // reset to original state (reveal) + if (trEl.getBoundingClientRect().bottom > rowBottom) { + 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. + DayGrid.prototype.limitRow = function (row, levelLimit) { + var _this = this; + var colCnt = this.colCnt; + var isRtl = this.context.isRtl; + var rowStruct = this.eventRenderer.rowStructs[row]; + var moreNodes = []; // array of "more" <a> links and <td> 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 <td> elements in the row + var limitedNodes; // array of temporarily hidden level <tr> and segment <td> DOM nodes + var i; + var 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; + var rowSpan; + var segMoreNodes; // array of "more" <td> cells that will stand-in for the current seg's cell + var j; + var moreTd; + var moreWrap; + var moreLink; + // Iterates through empty level cells and places "more" links inside if need be + var emptyCellsUntil = function (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 = core.createElement('div', null, moreLink); + td.appendChild(moreWrap); + moreNodes.push(moreWrap); + } + col++; + } + }; + if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit? + levelSegs = rowStruct.segLevels[levelLimit - 1]; + cellMatrix = rowStruct.cellMatrix; + limitedNodes = core.findChildren(rowStruct.tbodyEl).slice(levelLimit); // get level <tr> elements past the limit + limitedNodes.forEach(function (node) { + node.classList.add('fc-limited'); // 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]; + var leftCol = isRtl ? (colCnt - 1 - seg.lastCol) : seg.firstCol; + var rightCol = isRtl ? (colCnt - 1 - seg.firstCol) : seg.lastCol; + emptyCellsUntil(leftCol); // process empty cells before the segment + // determine *all* segments below `seg` that occupy the same columns + colSegsBelow = []; + totalSegsBelow = 0; + while (col <= 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][leftCol]; // the segment's parent cell + rowSpan = td.rowSpan || 1; + segMoreNodes = []; + // make a replacement <td> for each column the segment occupies. will be one for each colspan + for (j = 0; j < colSegsBelow.length; j++) { + moreTd = core.createElement('td', { className: 'fc-more-cell', rowSpan: rowSpan }); + segsBelow = colSegsBelow[j]; + moreLink = this.renderMoreLink(row, leftCol + j, [seg].concat(segsBelow) // count seg as hidden too + ); + moreWrap = core.createElement('div', null, moreLink); + moreTd.appendChild(moreWrap); + segMoreNodes.push(moreTd); + moreNodes.push(moreTd); + } + td.classList.add('fc-limited'); + core.insertAfterElement(td, segMoreNodes); + limitedNodes.push(td); + } + } + 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. + DayGrid.prototype.unlimitRow = function (row) { + var rowStruct = this.eventRenderer.rowStructs[row]; + if (rowStruct.moreEls) { + rowStruct.moreEls.forEach(core.removeElement); + rowStruct.moreEls = null; + } + if (rowStruct.limitedEls) { + rowStruct.limitedEls.forEach(function (limitedEl) { + limitedEl.classList.remove('fc-limited'); + }); + rowStruct.limitedEls = null; + } + }; + // Renders an <a> element that represents hidden event element for a cell. + // Responsible for attaching click handler as well. + DayGrid.prototype.renderMoreLink = function (row, col, hiddenSegs) { + var _this = this; + var _a = this.context, calendar = _a.calendar, view = _a.view, dateEnv = _a.dateEnv, options = _a.options, isRtl = _a.isRtl; + var a = core.createElement('a', { className: 'fc-more' }); + a.innerText = this.getMoreLinkText(hiddenSegs.length); + a.addEventListener('click', function (ev) { + var clickOption = options.eventLimitClick; + var _col = isRtl ? _this.colCnt - col - 1 : col; // HACK: props.cells has different dir system? + var date = _this.props.cells[row][_col].date; + var moreEl = ev.currentTarget; + 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 = calendar.publiclyTrigger('eventLimitClick', [ + { + date: dateEnv.toDate(date), + allDay: true, + dayEl: dayEl, + moreEl: moreEl, + segs: reslicedAllSegs, + hiddenSegs: reslicedHiddenSegs, + jsEvent: ev, + view: view + } + ]); + } + if (clickOption === 'popover') { + _this.showSegPopover(row, col, moreEl, reslicedAllSegs); + } + else if (typeof clickOption === 'string') { // a view name + calendar.zoomTo(date, clickOption); + } + }); + return a; + }; + // Reveals the popover that displays all events within a cell + DayGrid.prototype.showSegPopover = function (row, col, moreLink, segs) { + var _this = this; + var _a = this.context, calendar = _a.calendar, view = _a.view, theme = _a.theme, isRtl = _a.isRtl; + var _col = isRtl ? this.colCnt - col - 1 : col; // HACK: props.cells has different dir system? + var moreWrap = moreLink.parentNode; // the <div> wrapper around the <a> + 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[row]; // will align with top of row + } + options = { + className: 'fc-more-popover ' + theme.getClass('popover'), + parentEl: view.el, + top: core.computeRect(topEl).top, + autoHide: true, + content: function (el) { + _this.segPopoverTile = new DayTile(el); + _this.updateSegPopoverTile(_this.props.cells[row][_col].date, segs); + }, + hide: function () { + _this.segPopoverTile.destroy(); + _this.segPopoverTile = null; + _this.segPopover.destroy(); + _this.segPopover = null; + } + }; + // Determine horizontal coordinate. + // We use the moreWrap instead of the <td> to avoid border confusion. + if (isRtl) { + options.right = core.computeRect(moreWrap).right + 1; // +1 to be over cell border + } + else { + options.left = core.computeRect(moreWrap).left - 1; // -1 to be over cell border + } + this.segPopover = new Popover(options); + this.segPopover.show(); + calendar.releaseAfterSizingTriggers(); // hack for eventPositioned + }; + // Given the events within an array of segment objects, reslice them to be in a single day + DayGrid.prototype.resliceDaySegs = function (segs, dayDate) { + var dayStart = dayDate; + var dayEnd = core.addDays(dayStart, 1); + var dayRange = { start: dayStart, end: dayEnd }; + var newSegs = []; + for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) { + var seg = segs_1[_i]; + var eventRange = seg.eventRange; + var origRange = eventRange.range; + var slicedRange = core.intersectRanges(origRange, dayRange); + if (slicedRange) { + newSegs.push(__assign({}, seg, { eventRange: { + def: eventRange.def, + ui: __assign({}, eventRange.ui, { durationEditable: false }), + instance: eventRange.instance, + range: slicedRange + }, isStart: seg.isStart && slicedRange.start.valueOf() === origRange.start.valueOf(), isEnd: seg.isEnd && slicedRange.end.valueOf() === origRange.end.valueOf() })); + } + } + return newSegs; + }; + // Generates the text that should be inside a "more" link, given the number of events it represents + DayGrid.prototype.getMoreLinkText = function (num) { + var opt = this.context.options.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. + DayGrid.prototype.getCellSegs = function (row, col, startLevel) { + var segMatrix = this.eventRenderer.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 DayGrid; + }(core.DateComponent)); + + var WEEK_NUM_FORMAT$1 = core.createFormatter({ week: 'numeric' }); + /* An abstract class for the daygrid 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. + var AbstractDayGridView = /** @class */ (function (_super) { + __extends(AbstractDayGridView, _super); + function AbstractDayGridView() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.processOptions = core.memoize(_this._processOptions); + _this.renderSkeleton = core.memoizeRendering(_this._renderSkeleton, _this._unrenderSkeleton); + /* Header Rendering + ------------------------------------------------------------------------------------------------------------------*/ + // Generates the HTML that will go before the day-of week header cells + _this.renderHeadIntroHtml = function () { + var _a = _this.context, theme = _a.theme, options = _a.options; + if (_this.colWeekNumbersVisible) { + return '' + + '<th class="fc-week-number ' + theme.getClass('widgetHeader') + '" ' + _this.weekNumberStyleAttr() + '>' + + '<span>' + // needed for matchCellWidths + core.htmlEscape(options.weekLabel) + + '</span>' + + '</th>'; + } + return ''; + }; + /* Day Grid Rendering + ------------------------------------------------------------------------------------------------------------------*/ + // Generates the HTML that will go before content-skeleton cells that display the day/week numbers + _this.renderDayGridNumberIntroHtml = function (row, dayGrid) { + var _a = _this.context, options = _a.options, dateEnv = _a.dateEnv; + var weekStart = dayGrid.props.cells[row][0].date; + if (_this.colWeekNumbersVisible) { + return '' + + '<td class="fc-week-number" ' + _this.weekNumberStyleAttr() + '>' + + core.buildGotoAnchorHtml(// aside from link, important for matchCellWidths + options, dateEnv, { date: weekStart, type: 'week', forceOff: dayGrid.colCnt === 1 }, dateEnv.format(weekStart, WEEK_NUM_FORMAT$1) // inner HTML + ) + + '</td>'; + } + return ''; + }; + // Generates the HTML that goes before the day bg cells for each day-row + _this.renderDayGridBgIntroHtml = function () { + var theme = _this.context.theme; + if (_this.colWeekNumbersVisible) { + return '<td class="fc-week-number ' + theme.getClass('widgetContent') + '" ' + _this.weekNumberStyleAttr() + '></td>'; + } + return ''; + }; + // Generates the HTML that goes before every other type of row generated by DayGrid. + // Affects mirror-skeleton and highlight-skeleton rows. + _this.renderDayGridIntroHtml = function () { + if (_this.colWeekNumbersVisible) { + return '<td class="fc-week-number" ' + _this.weekNumberStyleAttr() + '></td>'; + } + return ''; + }; + return _this; + } + AbstractDayGridView.prototype._processOptions = function (options) { + if (options.weekNumbers) { + if (options.weekNumbersWithinDays) { + this.cellWeekNumbersVisible = true; + this.colWeekNumbersVisible = false; + } + else { + this.cellWeekNumbersVisible = false; + this.colWeekNumbersVisible = true; + } + } + else { + this.colWeekNumbersVisible = false; + this.cellWeekNumbersVisible = false; + } + }; + AbstractDayGridView.prototype.render = function (props, context) { + _super.prototype.render.call(this, props, context); + this.processOptions(context.options); + this.renderSkeleton(context); + }; + AbstractDayGridView.prototype.destroy = function () { + _super.prototype.destroy.call(this); + this.renderSkeleton.unrender(); + }; + AbstractDayGridView.prototype._renderSkeleton = function (context) { + this.el.classList.add('fc-dayGrid-view'); + this.el.innerHTML = this.renderSkeletonHtml(); + this.scroller = new core.ScrollComponent('hidden', // overflow x + 'auto' // overflow y + ); + var dayGridContainerEl = this.scroller.el; + this.el.querySelector('.fc-body > tr > td').appendChild(dayGridContainerEl); + dayGridContainerEl.classList.add('fc-day-grid-container'); + var dayGridEl = core.createElement('div', { className: 'fc-day-grid' }); + dayGridContainerEl.appendChild(dayGridEl); + this.dayGrid = new DayGrid(dayGridEl, { + renderNumberIntroHtml: this.renderDayGridNumberIntroHtml, + renderBgIntroHtml: this.renderDayGridBgIntroHtml, + renderIntroHtml: this.renderDayGridIntroHtml, + colWeekNumbersVisible: this.colWeekNumbersVisible, + cellWeekNumbersVisible: this.cellWeekNumbersVisible + }); + }; + AbstractDayGridView.prototype._unrenderSkeleton = function () { + this.el.classList.remove('fc-dayGrid-view'); + this.dayGrid.destroy(); + this.scroller.destroy(); + }; + // Builds the HTML skeleton for the view. + // The day-grid component will render inside of a container defined by this HTML. + AbstractDayGridView.prototype.renderSkeletonHtml = function () { + var _a = this.context, theme = _a.theme, options = _a.options; + return '' + + '<table class="' + theme.getClass('tableGrid') + '">' + + (options.columnHeader ? + '<thead class="fc-head">' + + '<tr>' + + '<td class="fc-head-container ' + theme.getClass('widgetHeader') + '"> </td>' + + '</tr>' + + '</thead>' : + '') + + '<tbody class="fc-body">' + + '<tr>' + + '<td class="' + theme.getClass('widgetContent') + '"></td>' + + '</tr>' + + '</tbody>' + + '</table>'; + }; + // Generates an HTML attribute string for setting the width of the week number column, if it is known + AbstractDayGridView.prototype.weekNumberStyleAttr = function () { + if (this.weekNumberWidth != null) { + return 'style="width:' + this.weekNumberWidth + 'px"'; + } + return ''; + }; + // Determines whether each row should have a constant height + AbstractDayGridView.prototype.hasRigidRows = function () { + var eventLimit = this.context.options.eventLimit; + return eventLimit && typeof eventLimit !== 'number'; + }; + /* Dimensions + ------------------------------------------------------------------------------------------------------------------*/ + AbstractDayGridView.prototype.updateSize = function (isResize, viewHeight, isAuto) { + _super.prototype.updateSize.call(this, isResize, viewHeight, isAuto); // will call updateBaseSize. important that executes first + this.dayGrid.updateSize(isResize); + }; + // Refreshes the horizontal dimensions of the view + AbstractDayGridView.prototype.updateBaseSize = function (isResize, viewHeight, isAuto) { + var dayGrid = this.dayGrid; + var eventLimit = this.context.options.eventLimit; + var headRowEl = this.header ? this.header.el : null; // HACK + var scrollerHeight; + var scrollbarWidths; + // hack to give the view some height prior to dayGrid's columns being rendered + // TODO: separate setting height from scroller VS dayGrid. + if (!dayGrid.rowEls) { + if (!isAuto) { + scrollerHeight = this.computeScrollerHeight(viewHeight); + this.scroller.setHeight(scrollerHeight); + } + return; + } + if (this.colWeekNumbersVisible) { + // Make sure all week number cells running down the side have the same width. + this.weekNumberWidth = core.matchCellWidths(core.findElements(this.el, '.fc-week-number')); + } + // reset all heights to be natural + this.scroller.clear(); + if (headRowEl) { + core.uncompensateScroll(headRowEl); + } + dayGrid.removeSegPopover(); // kill the "more" popover if displayed + // is the event limit a constant level number? + if (eventLimit && typeof eventLimit === 'number') { + dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after + } + // distribute the height to the rows + // (viewHeight is a "recommended" value if isAuto) + scrollerHeight = this.computeScrollerHeight(viewHeight); + this.setGridHeight(scrollerHeight, isAuto); + // is the event limit dynamically calculated? + if (eventLimit && typeof eventLimit !== 'number') { + dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set + } + if (!isAuto) { // should we force dimensions of the scroll container? + this.scroller.setHeight(scrollerHeight); + scrollbarWidths = this.scroller.getScrollbarWidths(); + if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars? + if (headRowEl) { + core.compensateScroll(headRowEl, scrollbarWidths); + } + // doing the scrollbar compensation might have created text overflow which created more height. redo + scrollerHeight = this.computeScrollerHeight(viewHeight); + 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 + AbstractDayGridView.prototype.computeScrollerHeight = function (viewHeight) { + return viewHeight - + core.subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller + }; + // Sets the height of just the DayGrid component in this view + AbstractDayGridView.prototype.setGridHeight = function (height, isAuto) { + if (this.context.options.monthMode) { + // if auto, make the height of each row the height that it would be if there were 6 weeks + if (isAuto) { + height *= this.dayGrid.rowCnt / 6; + } + core.distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows + } + else { + if (isAuto) { + core.undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding + } + else { + core.distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows + } + } + }; + /* Scroll + ------------------------------------------------------------------------------------------------------------------*/ + AbstractDayGridView.prototype.computeDateScroll = function (duration) { + return { top: 0 }; + }; + AbstractDayGridView.prototype.queryDateScroll = function () { + return { top: this.scroller.getScrollTop() }; + }; + AbstractDayGridView.prototype.applyDateScroll = function (scroll) { + if (scroll.top !== undefined) { + this.scroller.setScrollTop(scroll.top); + } + }; + return AbstractDayGridView; + }(core.View)); + AbstractDayGridView.prototype.dateProfileGeneratorClass = DayGridDateProfileGenerator; + + var SimpleDayGrid = /** @class */ (function (_super) { + __extends(SimpleDayGrid, _super); + function SimpleDayGrid(dayGrid) { + var _this = _super.call(this, dayGrid.el) || this; + _this.slicer = new DayGridSlicer(); + _this.dayGrid = dayGrid; + return _this; + } + SimpleDayGrid.prototype.firstContext = function (context) { + context.calendar.registerInteractiveComponent(this, { el: this.dayGrid.el }); + }; + SimpleDayGrid.prototype.destroy = function () { + _super.prototype.destroy.call(this); + this.context.calendar.unregisterInteractiveComponent(this); + }; + SimpleDayGrid.prototype.render = function (props, context) { + var dayGrid = this.dayGrid; + var dateProfile = props.dateProfile, dayTable = props.dayTable; + dayGrid.receiveProps(__assign({}, this.slicer.sliceProps(props, dateProfile, props.nextDayThreshold, context.calendar, dayGrid, dayTable), { dateProfile: dateProfile, cells: dayTable.cells, isRigid: props.isRigid }), context); + }; + SimpleDayGrid.prototype.buildPositionCaches = function () { + this.dayGrid.buildPositionCaches(); + }; + SimpleDayGrid.prototype.queryHit = function (positionLeft, positionTop) { + var rawHit = this.dayGrid.positionToHit(positionLeft, positionTop); + if (rawHit) { + return { + component: this.dayGrid, + dateSpan: rawHit.dateSpan, + dayEl: rawHit.dayEl, + rect: { + left: rawHit.relativeRect.left, + right: rawHit.relativeRect.right, + top: rawHit.relativeRect.top, + bottom: rawHit.relativeRect.bottom + }, + layer: 0 + }; + } + }; + return SimpleDayGrid; + }(core.DateComponent)); + var DayGridSlicer = /** @class */ (function (_super) { + __extends(DayGridSlicer, _super); + function DayGridSlicer() { + return _super !== null && _super.apply(this, arguments) || this; + } + DayGridSlicer.prototype.sliceRange = function (dateRange, dayTable) { + return dayTable.sliceRange(dateRange); + }; + return DayGridSlicer; + }(core.Slicer)); + + var DayGridView = /** @class */ (function (_super) { + __extends(DayGridView, _super); + function DayGridView() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.buildDayTable = core.memoize(buildDayTable); + return _this; + } + DayGridView.prototype.render = function (props, context) { + _super.prototype.render.call(this, props, context); // will call _renderSkeleton/_unrenderSkeleton + var dateProfile = this.props.dateProfile; + var dayTable = this.dayTable = + this.buildDayTable(dateProfile, props.dateProfileGenerator); + if (this.header) { + this.header.receiveProps({ + dateProfile: dateProfile, + dates: dayTable.headerDates, + datesRepDistinctDays: dayTable.rowCnt === 1, + renderIntroHtml: this.renderHeadIntroHtml + }, context); + } + this.simpleDayGrid.receiveProps({ + dateProfile: dateProfile, + dayTable: dayTable, + businessHours: props.businessHours, + dateSelection: props.dateSelection, + eventStore: props.eventStore, + eventUiBases: props.eventUiBases, + eventSelection: props.eventSelection, + eventDrag: props.eventDrag, + eventResize: props.eventResize, + isRigid: this.hasRigidRows(), + nextDayThreshold: this.context.nextDayThreshold + }, context); + }; + DayGridView.prototype._renderSkeleton = function (context) { + _super.prototype._renderSkeleton.call(this, context); + if (context.options.columnHeader) { + this.header = new core.DayHeader(this.el.querySelector('.fc-head-container')); + } + this.simpleDayGrid = new SimpleDayGrid(this.dayGrid); + }; + DayGridView.prototype._unrenderSkeleton = function () { + _super.prototype._unrenderSkeleton.call(this); + if (this.header) { + this.header.destroy(); + } + this.simpleDayGrid.destroy(); + }; + return DayGridView; + }(AbstractDayGridView)); + function buildDayTable(dateProfile, dateProfileGenerator) { + var daySeries = new core.DaySeries(dateProfile.renderRange, dateProfileGenerator); + return new core.DayTable(daySeries, /year|month|week/.test(dateProfile.currentRangeUnit)); + } + + var main = core.createPlugin({ + defaultView: 'dayGridMonth', + views: { + dayGrid: DayGridView, + dayGridDay: { + type: 'dayGrid', + duration: { days: 1 } + }, + dayGridWeek: { + type: 'dayGrid', + duration: { weeks: 1 } + }, + dayGridMonth: { + type: 'dayGrid', + duration: { months: 1 }, + monthMode: true, + fixedWeekCount: true + } + } + }); + + exports.AbstractDayGridView = AbstractDayGridView; + exports.DayBgRow = DayBgRow; + exports.DayGrid = DayGrid; + exports.DayGridSlicer = DayGridSlicer; + exports.DayGridView = DayGridView; + exports.SimpleDayGrid = SimpleDayGrid; + exports.buildBasicDayTable = buildDayTable; + exports.default = main; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); diff --git a/addons/web/static/lib/fullcalendar/google-calendar/main.js b/addons/web/static/lib/fullcalendar/google-calendar/main.js new file mode 100644 index 00000000000..2dc7e77cce9 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/google-calendar/main.js @@ -0,0 +1,175 @@ +/*! +FullCalendar Google Calendar Plugin v4.4.0 +Docs & License: https://fullcalendar.io/ +(c) 2019 Adam Shaw +*/ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@fullcalendar/core')) : + typeof define === 'function' && define.amd ? define(['exports', '@fullcalendar/core'], factory) : + (global = global || self, factory(global.FullCalendarGoogleCalendar = {}, global.FullCalendar)); +}(this, function (exports, core) { 'use strict'; + + /*! *****************************************************************************
+ Copyright (c) Microsoft Corporation. All rights reserved.
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+ this file except in compliance with the License. You may obtain a copy of the
+ License at http://www.apache.org/licenses/LICENSE-2.0
+
+ THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
+ WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+ MERCHANTABLITY OR NON-INFRINGEMENT.
+
+ See the Apache Version 2.0 License for specific language governing permissions
+ and limitations under the License.
+ ***************************************************************************** */
+
+ var __assign = function() {
+ __assign = Object.assign || function __assign(t) {
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
+ s = arguments[i];
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
+ }
+ return t;
+ };
+ return __assign.apply(this, arguments);
+ }; + + // TODO: expose somehow + var API_BASE = 'https://www.googleapis.com/calendar/v3/calendars'; + var STANDARD_PROPS = { + url: String, + googleCalendarApiKey: String, + googleCalendarId: String, + googleCalendarApiBase: String, + data: null + }; + var eventSourceDef = { + parseMeta: function (raw) { + if (typeof raw === 'string') { + raw = { url: raw }; + } + if (typeof raw === 'object') { + var standardProps = core.refineProps(raw, STANDARD_PROPS); + if (!standardProps.googleCalendarId && standardProps.url) { + standardProps.googleCalendarId = parseGoogleCalendarId(standardProps.url); + } + delete standardProps.url; + if (standardProps.googleCalendarId) { + return standardProps; + } + } + return null; + }, + fetch: function (arg, onSuccess, onFailure) { + var calendar = arg.calendar; + var meta = arg.eventSource.meta; + var apiKey = meta.googleCalendarApiKey || calendar.opt('googleCalendarApiKey'); + if (!apiKey) { + onFailure({ + message: 'Specify a googleCalendarApiKey. See http://fullcalendar.io/docs/google_calendar/' + }); + } + else { + var url = buildUrl(meta); + var requestParams_1 = buildRequestParams(arg.range, apiKey, meta.data, calendar.dateEnv); + core.requestJson('GET', url, requestParams_1, function (body, xhr) { + if (body.error) { + onFailure({ + message: 'Google Calendar API: ' + body.error.message, + errors: body.error.errors, + xhr: xhr + }); + } + else { + onSuccess({ + rawEvents: gcalItemsToRawEventDefs(body.items, requestParams_1.timeZone), + xhr: xhr + }); + } + }, function (message, xhr) { + onFailure({ message: message, xhr: xhr }); + }); + } + } + }; + function parseGoogleCalendarId(url) { + var match; + // detect if the ID was specified as a single string. + // will match calendars like "asdf1234@calendar.google.com" in addition to person email calendars. + if (/^[^\/]+@([^\/\.]+\.)*(google|googlemail|gmail)\.com$/.test(url)) { + return url; + } + else if ((match = /^https:\/\/www.googleapis.com\/calendar\/v3\/calendars\/([^\/]*)/.exec(url)) || + (match = /^https?:\/\/www.google.com\/calendar\/feeds\/([^\/]*)/.exec(url))) { + return decodeURIComponent(match[1]); + } + } + function buildUrl(meta) { + var apiBase = meta.googleCalendarApiBase; + if (!apiBase) { + apiBase = API_BASE; + } + return apiBase + '/' + encodeURIComponent(meta.googleCalendarId) + '/events'; + } + function buildRequestParams(range, apiKey, extraParams, dateEnv) { + var params; + var startStr; + var endStr; + if (dateEnv.canComputeOffset) { + // strings will naturally have offsets, which GCal needs + startStr = dateEnv.formatIso(range.start); + endStr = dateEnv.formatIso(range.end); + } + else { + // when timezone isn't known, we don't know what the UTC offset should be, so ask for +/- 1 day + // from the UTC day-start to guarantee we're getting all the events + // (start/end will be UTC-coerced dates, so toISOString is okay) + startStr = core.addDays(range.start, -1).toISOString(); + endStr = core.addDays(range.end, 1).toISOString(); + } + params = __assign({}, (extraParams || {}), { key: apiKey, timeMin: startStr, timeMax: endStr, singleEvents: true, maxResults: 9999 }); + if (dateEnv.timeZone !== 'local') { + params.timeZone = dateEnv.timeZone; + } + return params; + } + function gcalItemsToRawEventDefs(items, gcalTimezone) { + return items.map(function (item) { + return gcalItemToRawEventDef(item, gcalTimezone); + }); + } + function gcalItemToRawEventDef(item, gcalTimezone) { + var url = item.htmlLink || null; + // make the URLs for each event show times in the correct timezone + if (url && gcalTimezone) { + url = injectQsComponent(url, 'ctz=' + gcalTimezone); + } + return { + id: item.id, + title: item.summary, + start: item.start.dateTime || item.start.date, + end: item.end.dateTime || item.end.date, + url: url, + location: item.location, + description: item.description + }; + } + // Injects a string like "arg=value" into the querystring of a URL + // TODO: move to a general util file? + function injectQsComponent(url, component) { + // inject it after the querystring but before the fragment + return url.replace(/(\?.*?)?(#|$)/, function (whole, qs, hash) { + return (qs ? qs + '&' : '?') + component + hash; + }); + } + var main = core.createPlugin({ + eventSourceDefs: [eventSourceDef] + }); + + exports.default = main; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); diff --git a/addons/web/static/lib/fullcalendar/interaction/main.js b/addons/web/static/lib/fullcalendar/interaction/main.js new file mode 100644 index 00000000000..41958ebf81d --- /dev/null +++ b/addons/web/static/lib/fullcalendar/interaction/main.js @@ -0,0 +1,2152 @@ +/*! +FullCalendar Interaction Plugin v4.4.0 +Docs & License: https://fullcalendar.io/ +(c) 2019 Adam Shaw +*/ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@fullcalendar/core')) : + typeof define === 'function' && define.amd ? define(['exports', '@fullcalendar/core'], factory) : + (global = global || self, factory(global.FullCalendarInteraction = {}, global.FullCalendar)); +}(this, function (exports, core) { 'use strict'; + + /*! *****************************************************************************
+ Copyright (c) Microsoft Corporation. All rights reserved.
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+ this file except in compliance with the License. You may obtain a copy of the
+ License at http://www.apache.org/licenses/LICENSE-2.0
+
+ THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
+ WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+ MERCHANTABLITY OR NON-INFRINGEMENT.
+
+ See the Apache Version 2.0 License for specific language governing permissions
+ and limitations under the License.
+ ***************************************************************************** */
+ /* global Reflect, Promise */
+
+ var extendStatics = function(d, b) {
+ extendStatics = Object.setPrototypeOf ||
+ ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+ function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+ return extendStatics(d, b);
+ };
+
+ function __extends(d, b) {
+ extendStatics(d, b);
+ function __() { this.constructor = d; }
+ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+ }
+
+ var __assign = function() {
+ __assign = Object.assign || function __assign(t) {
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
+ s = arguments[i];
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
+ }
+ return t;
+ };
+ return __assign.apply(this, arguments);
+ }; + + core.config.touchMouseIgnoreWait = 500; + var ignoreMouseDepth = 0; + var listenerCnt = 0; + var isWindowTouchMoveCancelled = false; + /* + Uses a "pointer" abstraction, which monitors UI events for both mouse and touch. + Tracks when the pointer "drags" on a certain element, meaning down+move+up. + + Also, tracks if there was touch-scrolling. + Also, can prevent touch-scrolling from happening. + Also, can fire pointermove events when scrolling happens underneath, even when no real pointer movement. + + emits: + - pointerdown + - pointermove + - pointerup + */ + var PointerDragging = /** @class */ (function () { + function PointerDragging(containerEl) { + var _this = this; + this.subjectEl = null; + this.downEl = null; + // options that can be directly assigned by caller + this.selector = ''; // will cause subjectEl in all emitted events to be this element + this.handleSelector = ''; + this.shouldIgnoreMove = false; + this.shouldWatchScroll = true; // for simulating pointermove on scroll + // internal states + this.isDragging = false; + this.isTouchDragging = false; + this.wasTouchScroll = false; + // Mouse + // ---------------------------------------------------------------------------------------------------- + this.handleMouseDown = function (ev) { + if (!_this.shouldIgnoreMouse() && + isPrimaryMouseButton(ev) && + _this.tryStart(ev)) { + var pev = _this.createEventFromMouse(ev, true); + _this.emitter.trigger('pointerdown', pev); + _this.initScrollWatch(pev); + if (!_this.shouldIgnoreMove) { + document.addEventListener('mousemove', _this.handleMouseMove); + } + document.addEventListener('mouseup', _this.handleMouseUp); + } + }; + this.handleMouseMove = function (ev) { + var pev = _this.createEventFromMouse(ev); + _this.recordCoords(pev); + _this.emitter.trigger('pointermove', pev); + }; + this.handleMouseUp = function (ev) { + document.removeEventListener('mousemove', _this.handleMouseMove); + document.removeEventListener('mouseup', _this.handleMouseUp); + _this.emitter.trigger('pointerup', _this.createEventFromMouse(ev)); + _this.cleanup(); // call last so that pointerup has access to props + }; + // Touch + // ---------------------------------------------------------------------------------------------------- + this.handleTouchStart = function (ev) { + if (_this.tryStart(ev)) { + _this.isTouchDragging = true; + var pev = _this.createEventFromTouch(ev, true); + _this.emitter.trigger('pointerdown', pev); + _this.initScrollWatch(pev); + // unlike mouse, need to attach to target, not document + // https://stackoverflow.com/a/45760014 + var target = ev.target; + if (!_this.shouldIgnoreMove) { + target.addEventListener('touchmove', _this.handleTouchMove); + } + target.addEventListener('touchend', _this.handleTouchEnd); + target.addEventListener('touchcancel', _this.handleTouchEnd); // treat it as a touch end + // attach a handler to get called when ANY scroll action happens on the page. + // this was impossible to do with normal on/off because 'scroll' doesn't bubble. + // http://stackoverflow.com/a/32954565/96342 + window.addEventListener('scroll', _this.handleTouchScroll, true // useCapture + ); + } + }; + this.handleTouchMove = function (ev) { + var pev = _this.createEventFromTouch(ev); + _this.recordCoords(pev); + _this.emitter.trigger('pointermove', pev); + }; + this.handleTouchEnd = function (ev) { + if (_this.isDragging) { // done to guard against touchend followed by touchcancel + var target = ev.target; + target.removeEventListener('touchmove', _this.handleTouchMove); + target.removeEventListener('touchend', _this.handleTouchEnd); + target.removeEventListener('touchcancel', _this.handleTouchEnd); + window.removeEventListener('scroll', _this.handleTouchScroll, true); // useCaptured=true + _this.emitter.trigger('pointerup', _this.createEventFromTouch(ev)); + _this.cleanup(); // call last so that pointerup has access to props + _this.isTouchDragging = false; + startIgnoringMouse(); + } + }; + this.handleTouchScroll = function () { + _this.wasTouchScroll = true; + }; + this.handleScroll = function (ev) { + if (!_this.shouldIgnoreMove) { + var pageX = (window.pageXOffset - _this.prevScrollX) + _this.prevPageX; + var pageY = (window.pageYOffset - _this.prevScrollY) + _this.prevPageY; + _this.emitter.trigger('pointermove', { + origEvent: ev, + isTouch: _this.isTouchDragging, + subjectEl: _this.subjectEl, + pageX: pageX, + pageY: pageY, + deltaX: pageX - _this.origPageX, + deltaY: pageY - _this.origPageY + }); + } + }; + this.containerEl = containerEl; + this.emitter = new core.EmitterMixin(); + containerEl.addEventListener('mousedown', this.handleMouseDown); + containerEl.addEventListener('touchstart', this.handleTouchStart, { passive: true }); + listenerCreated(); + } + PointerDragging.prototype.destroy = function () { + this.containerEl.removeEventListener('mousedown', this.handleMouseDown); + this.containerEl.removeEventListener('touchstart', this.handleTouchStart, { passive: true }); + listenerDestroyed(); + }; + PointerDragging.prototype.tryStart = function (ev) { + var subjectEl = this.querySubjectEl(ev); + var downEl = ev.target; + if (subjectEl && + (!this.handleSelector || core.elementClosest(downEl, this.handleSelector))) { + this.subjectEl = subjectEl; + this.downEl = downEl; + this.isDragging = true; // do this first so cancelTouchScroll will work + this.wasTouchScroll = false; + return true; + } + return false; + }; + PointerDragging.prototype.cleanup = function () { + isWindowTouchMoveCancelled = false; + this.isDragging = false; + this.subjectEl = null; + this.downEl = null; + // keep wasTouchScroll around for later access + this.destroyScrollWatch(); + }; + PointerDragging.prototype.querySubjectEl = function (ev) { + if (this.selector) { + return core.elementClosest(ev.target, this.selector); + } + else { + return this.containerEl; + } + }; + PointerDragging.prototype.shouldIgnoreMouse = function () { + return ignoreMouseDepth || this.isTouchDragging; + }; + // can be called by user of this class, to cancel touch-based scrolling for the current drag + PointerDragging.prototype.cancelTouchScroll = function () { + if (this.isDragging) { + isWindowTouchMoveCancelled = true; + } + }; + // Scrolling that simulates pointermoves + // ---------------------------------------------------------------------------------------------------- + PointerDragging.prototype.initScrollWatch = function (ev) { + if (this.shouldWatchScroll) { + this.recordCoords(ev); + window.addEventListener('scroll', this.handleScroll, true); // useCapture=true + } + }; + PointerDragging.prototype.recordCoords = function (ev) { + if (this.shouldWatchScroll) { + this.prevPageX = ev.pageX; + this.prevPageY = ev.pageY; + this.prevScrollX = window.pageXOffset; + this.prevScrollY = window.pageYOffset; + } + }; + PointerDragging.prototype.destroyScrollWatch = function () { + if (this.shouldWatchScroll) { + window.removeEventListener('scroll', this.handleScroll, true); // useCaptured=true + } + }; + // Event Normalization + // ---------------------------------------------------------------------------------------------------- + PointerDragging.prototype.createEventFromMouse = function (ev, isFirst) { + var deltaX = 0; + var deltaY = 0; + // TODO: repeat code + if (isFirst) { + this.origPageX = ev.pageX; + this.origPageY = ev.pageY; + } + else { + deltaX = ev.pageX - this.origPageX; + deltaY = ev.pageY - this.origPageY; + } + return { + origEvent: ev, + isTouch: false, + subjectEl: this.subjectEl, + pageX: ev.pageX, + pageY: ev.pageY, + deltaX: deltaX, + deltaY: deltaY + }; + }; + PointerDragging.prototype.createEventFromTouch = function (ev, isFirst) { + var touches = ev.touches; + var pageX; + var pageY; + var deltaX = 0; + var deltaY = 0; + // if touch coords available, prefer, + // because FF would give bad ev.pageX ev.pageY + if (touches && touches.length) { + pageX = touches[0].pageX; + pageY = touches[0].pageY; + } + else { + pageX = ev.pageX; + pageY = ev.pageY; + } + // TODO: repeat code + if (isFirst) { + this.origPageX = pageX; + this.origPageY = pageY; + } + else { + deltaX = pageX - this.origPageX; + deltaY = pageY - this.origPageY; + } + return { + origEvent: ev, + isTouch: true, + subjectEl: this.subjectEl, + pageX: pageX, + pageY: pageY, + deltaX: deltaX, + deltaY: deltaY + }; + }; + return PointerDragging; + }()); + // 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.button === 0 && !ev.ctrlKey; + } + // Ignoring fake mouse events generated by touch + // ---------------------------------------------------------------------------------------------------- + function startIgnoringMouse() { + ignoreMouseDepth++; + setTimeout(function () { + ignoreMouseDepth--; + }, core.config.touchMouseIgnoreWait); + } + // We want to attach touchmove as early as possible for Safari + // ---------------------------------------------------------------------------------------------------- + function listenerCreated() { + if (!(listenerCnt++)) { + window.addEventListener('touchmove', onWindowTouchMove, { passive: false }); + } + } + function listenerDestroyed() { + if (!(--listenerCnt)) { + window.removeEventListener('touchmove', onWindowTouchMove, { passive: false }); + } + } + function onWindowTouchMove(ev) { + if (isWindowTouchMoveCancelled) { + ev.preventDefault(); + } + } + + /* + An effect in which an element follows the movement of a pointer across the screen. + The moving element is a clone of some other element. + Must call start + handleMove + stop. + */ + var ElementMirror = /** @class */ (function () { + function ElementMirror() { + this.isVisible = false; // must be explicitly enabled + this.sourceEl = null; + this.mirrorEl = null; + this.sourceElRect = null; // screen coords relative to viewport + // options that can be set directly by caller + this.parentNode = document.body; + this.zIndex = 9999; + this.revertDuration = 0; + } + ElementMirror.prototype.start = function (sourceEl, pageX, pageY) { + this.sourceEl = sourceEl; + this.sourceElRect = this.sourceEl.getBoundingClientRect(); + this.origScreenX = pageX - window.pageXOffset; + this.origScreenY = pageY - window.pageYOffset; + this.deltaX = 0; + this.deltaY = 0; + this.updateElPosition(); + }; + ElementMirror.prototype.handleMove = function (pageX, pageY) { + this.deltaX = (pageX - window.pageXOffset) - this.origScreenX; + this.deltaY = (pageY - window.pageYOffset) - this.origScreenY; + this.updateElPosition(); + }; + // can be called before start + ElementMirror.prototype.setIsVisible = function (bool) { + if (bool) { + if (!this.isVisible) { + if (this.mirrorEl) { + this.mirrorEl.style.display = ''; + } + this.isVisible = bool; // needs to happen before updateElPosition + this.updateElPosition(); // because was not updating the position while invisible + } + } + else { + if (this.isVisible) { + if (this.mirrorEl) { + this.mirrorEl.style.display = 'none'; + } + this.isVisible = bool; + } + } + }; + // always async + ElementMirror.prototype.stop = function (needsRevertAnimation, callback) { + var _this = this; + var done = function () { + _this.cleanup(); + callback(); + }; + if (needsRevertAnimation && + this.mirrorEl && + this.isVisible && + this.revertDuration && // if 0, transition won't work + (this.deltaX || this.deltaY) // if same coords, transition won't work + ) { + this.doRevertAnimation(done, this.revertDuration); + } + else { + setTimeout(done, 0); + } + }; + ElementMirror.prototype.doRevertAnimation = function (callback, revertDuration) { + var mirrorEl = this.mirrorEl; + var finalSourceElRect = this.sourceEl.getBoundingClientRect(); // because autoscrolling might have happened + mirrorEl.style.transition = + 'top ' + revertDuration + 'ms,' + + 'left ' + revertDuration + 'ms'; + core.applyStyle(mirrorEl, { + left: finalSourceElRect.left, + top: finalSourceElRect.top + }); + core.whenTransitionDone(mirrorEl, function () { + mirrorEl.style.transition = ''; + callback(); + }); + }; + ElementMirror.prototype.cleanup = function () { + if (this.mirrorEl) { + core.removeElement(this.mirrorEl); + this.mirrorEl = null; + } + this.sourceEl = null; + }; + ElementMirror.prototype.updateElPosition = function () { + if (this.sourceEl && this.isVisible) { + core.applyStyle(this.getMirrorEl(), { + left: this.sourceElRect.left + this.deltaX, + top: this.sourceElRect.top + this.deltaY + }); + } + }; + ElementMirror.prototype.getMirrorEl = function () { + var sourceElRect = this.sourceElRect; + var mirrorEl = this.mirrorEl; + if (!mirrorEl) { + mirrorEl = this.mirrorEl = this.sourceEl.cloneNode(true); // cloneChildren=true + // we don't want long taps or any mouse interaction causing selection/menus. + // would use preventSelection(), but that prevents selectstart, causing problems. + mirrorEl.classList.add('fc-unselectable'); + mirrorEl.classList.add('fc-dragging'); + core.applyStyle(mirrorEl, { + position: 'fixed', + zIndex: this.zIndex, + visibility: '', + boxSizing: 'border-box', + width: sourceElRect.right - sourceElRect.left, + height: sourceElRect.bottom - sourceElRect.top, + right: 'auto', + bottom: 'auto', + margin: 0 + }); + this.parentNode.appendChild(mirrorEl); + } + return mirrorEl; + }; + return ElementMirror; + }()); + + /* + Is a cache for a given element's scroll information (all the info that ScrollController stores) + in addition the "client rectangle" of the element.. the area within the scrollbars. + + The cache can be in one of two modes: + - doesListening:false - ignores when the container is scrolled by someone else + - doesListening:true - watch for scrolling and update the cache + */ + var ScrollGeomCache = /** @class */ (function (_super) { + __extends(ScrollGeomCache, _super); + function ScrollGeomCache(scrollController, doesListening) { + var _this = _super.call(this) || this; + _this.handleScroll = function () { + _this.scrollTop = _this.scrollController.getScrollTop(); + _this.scrollLeft = _this.scrollController.getScrollLeft(); + _this.handleScrollChange(); + }; + _this.scrollController = scrollController; + _this.doesListening = doesListening; + _this.scrollTop = _this.origScrollTop = scrollController.getScrollTop(); + _this.scrollLeft = _this.origScrollLeft = scrollController.getScrollLeft(); + _this.scrollWidth = scrollController.getScrollWidth(); + _this.scrollHeight = scrollController.getScrollHeight(); + _this.clientWidth = scrollController.getClientWidth(); + _this.clientHeight = scrollController.getClientHeight(); + _this.clientRect = _this.computeClientRect(); // do last in case it needs cached values + if (_this.doesListening) { + _this.getEventTarget().addEventListener('scroll', _this.handleScroll); + } + return _this; + } + ScrollGeomCache.prototype.destroy = function () { + if (this.doesListening) { + this.getEventTarget().removeEventListener('scroll', this.handleScroll); + } + }; + ScrollGeomCache.prototype.getScrollTop = function () { + return this.scrollTop; + }; + ScrollGeomCache.prototype.getScrollLeft = function () { + return this.scrollLeft; + }; + ScrollGeomCache.prototype.setScrollTop = function (top) { + this.scrollController.setScrollTop(top); + if (!this.doesListening) { + // we are not relying on the element to normalize out-of-bounds scroll values + // so we need to sanitize ourselves + this.scrollTop = Math.max(Math.min(top, this.getMaxScrollTop()), 0); + this.handleScrollChange(); + } + }; + ScrollGeomCache.prototype.setScrollLeft = function (top) { + this.scrollController.setScrollLeft(top); + if (!this.doesListening) { + // we are not relying on the element to normalize out-of-bounds scroll values + // so we need to sanitize ourselves + this.scrollLeft = Math.max(Math.min(top, this.getMaxScrollLeft()), 0); + this.handleScrollChange(); + } + }; + ScrollGeomCache.prototype.getClientWidth = function () { + return this.clientWidth; + }; + ScrollGeomCache.prototype.getClientHeight = function () { + return this.clientHeight; + }; + ScrollGeomCache.prototype.getScrollWidth = function () { + return this.scrollWidth; + }; + ScrollGeomCache.prototype.getScrollHeight = function () { + return this.scrollHeight; + }; + ScrollGeomCache.prototype.handleScrollChange = function () { + }; + return ScrollGeomCache; + }(core.ScrollController)); + var ElementScrollGeomCache = /** @class */ (function (_super) { + __extends(ElementScrollGeomCache, _super); + function ElementScrollGeomCache(el, doesListening) { + return _super.call(this, new core.ElementScrollController(el), doesListening) || this; + } + ElementScrollGeomCache.prototype.getEventTarget = function () { + return this.scrollController.el; + }; + ElementScrollGeomCache.prototype.computeClientRect = function () { + return core.computeInnerRect(this.scrollController.el); + }; + return ElementScrollGeomCache; + }(ScrollGeomCache)); + var WindowScrollGeomCache = /** @class */ (function (_super) { + __extends(WindowScrollGeomCache, _super); + function WindowScrollGeomCache(doesListening) { + return _super.call(this, new core.WindowScrollController(), doesListening) || this; + } + WindowScrollGeomCache.prototype.getEventTarget = function () { + return window; + }; + WindowScrollGeomCache.prototype.computeClientRect = function () { + return { + left: this.scrollLeft, + right: this.scrollLeft + this.clientWidth, + top: this.scrollTop, + bottom: this.scrollTop + this.clientHeight + }; + }; + // the window is the only scroll object that changes it's rectangle relative + // to the document's topleft as it scrolls + WindowScrollGeomCache.prototype.handleScrollChange = function () { + this.clientRect = this.computeClientRect(); + }; + return WindowScrollGeomCache; + }(ScrollGeomCache)); + + // If available we are using native "performance" API instead of "Date" + // Read more about it on MDN: + // https://developer.mozilla.org/en-US/docs/Web/API/Performance + var getTime = typeof performance === 'function' ? performance.now : Date.now; + /* + For a pointer interaction, automatically scrolls certain scroll containers when the pointer + approaches the edge. + + The caller must call start + handleMove + stop. + */ + var AutoScroller = /** @class */ (function () { + function AutoScroller() { + var _this = this; + // options that can be set by caller + this.isEnabled = true; + this.scrollQuery = [window, '.fc-scroller']; + this.edgeThreshold = 50; // pixels + this.maxVelocity = 300; // pixels per second + // internal state + this.pointerScreenX = null; + this.pointerScreenY = null; + this.isAnimating = false; + this.scrollCaches = null; + // protect against the initial pointerdown being too close to an edge and starting the scroll + this.everMovedUp = false; + this.everMovedDown = false; + this.everMovedLeft = false; + this.everMovedRight = false; + this.animate = function () { + if (_this.isAnimating) { // wasn't cancelled between animation calls + var edge = _this.computeBestEdge(_this.pointerScreenX + window.pageXOffset, _this.pointerScreenY + window.pageYOffset); + if (edge) { + var now = getTime(); + _this.handleSide(edge, (now - _this.msSinceRequest) / 1000); + _this.requestAnimation(now); + } + else { + _this.isAnimating = false; // will stop animation + } + } + }; + } + AutoScroller.prototype.start = function (pageX, pageY) { + if (this.isEnabled) { + this.scrollCaches = this.buildCaches(); + this.pointerScreenX = null; + this.pointerScreenY = null; + this.everMovedUp = false; + this.everMovedDown = false; + this.everMovedLeft = false; + this.everMovedRight = false; + this.handleMove(pageX, pageY); + } + }; + AutoScroller.prototype.handleMove = function (pageX, pageY) { + if (this.isEnabled) { + var pointerScreenX = pageX - window.pageXOffset; + var pointerScreenY = pageY - window.pageYOffset; + var yDelta = this.pointerScreenY === null ? 0 : pointerScreenY - this.pointerScreenY; + var xDelta = this.pointerScreenX === null ? 0 : pointerScreenX - this.pointerScreenX; + if (yDelta < 0) { + this.everMovedUp = true; + } + else if (yDelta > 0) { + this.everMovedDown = true; + } + if (xDelta < 0) { + this.everMovedLeft = true; + } + else if (xDelta > 0) { + this.everMovedRight = true; + } + this.pointerScreenX = pointerScreenX; + this.pointerScreenY = pointerScreenY; + if (!this.isAnimating) { + this.isAnimating = true; + this.requestAnimation(getTime()); + } + } + }; + AutoScroller.prototype.stop = function () { + if (this.isEnabled) { + this.isAnimating = false; // will stop animation + for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) { + var scrollCache = _a[_i]; + scrollCache.destroy(); + } + this.scrollCaches = null; + } + }; + AutoScroller.prototype.requestAnimation = function (now) { + this.msSinceRequest = now; + requestAnimationFrame(this.animate); + }; + AutoScroller.prototype.handleSide = function (edge, seconds) { + var scrollCache = edge.scrollCache; + var edgeThreshold = this.edgeThreshold; + var invDistance = edgeThreshold - edge.distance; + var velocity = // the closer to the edge, the faster we scroll + (invDistance * invDistance) / (edgeThreshold * edgeThreshold) * // quadratic + this.maxVelocity * seconds; + var sign = 1; + switch (edge.name) { + case 'left': + sign = -1; + // falls through + case 'right': + scrollCache.setScrollLeft(scrollCache.getScrollLeft() + velocity * sign); + break; + case 'top': + sign = -1; + // falls through + case 'bottom': + scrollCache.setScrollTop(scrollCache.getScrollTop() + velocity * sign); + break; + } + }; + // left/top are relative to document topleft + AutoScroller.prototype.computeBestEdge = function (left, top) { + var edgeThreshold = this.edgeThreshold; + var bestSide = null; + for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) { + var scrollCache = _a[_i]; + var rect = scrollCache.clientRect; + var leftDist = left - rect.left; + var rightDist = rect.right - left; + var topDist = top - rect.top; + var bottomDist = rect.bottom - top; + // completely within the rect? + if (leftDist >= 0 && rightDist >= 0 && topDist >= 0 && bottomDist >= 0) { + if (topDist <= edgeThreshold && this.everMovedUp && scrollCache.canScrollUp() && + (!bestSide || bestSide.distance > topDist)) { + bestSide = { scrollCache: scrollCache, name: 'top', distance: topDist }; + } + if (bottomDist <= edgeThreshold && this.everMovedDown && scrollCache.canScrollDown() && + (!bestSide || bestSide.distance > bottomDist)) { + bestSide = { scrollCache: scrollCache, name: 'bottom', distance: bottomDist }; + } + if (leftDist <= edgeThreshold && this.everMovedLeft && scrollCache.canScrollLeft() && + (!bestSide || bestSide.distance > leftDist)) { + bestSide = { scrollCache: scrollCache, name: 'left', distance: leftDist }; + } + if (rightDist <= edgeThreshold && this.everMovedRight && scrollCache.canScrollRight() && + (!bestSide || bestSide.distance > rightDist)) { + bestSide = { scrollCache: scrollCache, name: 'right', distance: rightDist }; + } + } + } + return bestSide; + }; + AutoScroller.prototype.buildCaches = function () { + return this.queryScrollEls().map(function (el) { + if (el === window) { + return new WindowScrollGeomCache(false); // false = don't listen to user-generated scrolls + } + else { + return new ElementScrollGeomCache(el, false); // false = don't listen to user-generated scrolls + } + }); + }; + AutoScroller.prototype.queryScrollEls = function () { + var els = []; + for (var _i = 0, _a = this.scrollQuery; _i < _a.length; _i++) { + var query = _a[_i]; + if (typeof query === 'object') { + els.push(query); + } + else { + els.push.apply(els, Array.prototype.slice.call(document.querySelectorAll(query))); + } + } + return els; + }; + return AutoScroller; + }()); + + /* + Monitors dragging on an element. Has a number of high-level features: + - minimum distance required before dragging + - minimum wait time ("delay") before dragging + - a mirror element that follows the pointer + */ + var FeaturefulElementDragging = /** @class */ (function (_super) { + __extends(FeaturefulElementDragging, _super); + function FeaturefulElementDragging(containerEl) { + var _this = _super.call(this, containerEl) || this; + // options that can be directly set by caller + // the caller can also set the PointerDragging's options as well + _this.delay = null; + _this.minDistance = 0; + _this.touchScrollAllowed = true; // prevents drag from starting and blocks scrolling during drag + _this.mirrorNeedsRevert = false; + _this.isInteracting = false; // is the user validly moving the pointer? lasts until pointerup + _this.isDragging = false; // is it INTENTFULLY dragging? lasts until after revert animation + _this.isDelayEnded = false; + _this.isDistanceSurpassed = false; + _this.delayTimeoutId = null; + _this.onPointerDown = function (ev) { + if (!_this.isDragging) { // so new drag doesn't happen while revert animation is going + _this.isInteracting = true; + _this.isDelayEnded = false; + _this.isDistanceSurpassed = false; + core.preventSelection(document.body); + core.preventContextMenu(document.body); + // prevent links from being visited if there's an eventual drag. + // also prevents selection in older browsers (maybe?). + // not necessary for touch, besides, browser would complain about passiveness. + if (!ev.isTouch) { + ev.origEvent.preventDefault(); + } + _this.emitter.trigger('pointerdown', ev); + if (!_this.pointer.shouldIgnoreMove) { + // actions related to initiating dragstart+dragmove+dragend... + _this.mirror.setIsVisible(false); // reset. caller must set-visible + _this.mirror.start(ev.subjectEl, ev.pageX, ev.pageY); // must happen on first pointer down + _this.startDelay(ev); + if (!_this.minDistance) { + _this.handleDistanceSurpassed(ev); + } + } + } + }; + _this.onPointerMove = function (ev) { + if (_this.isInteracting) { // if false, still waiting for previous drag's revert + _this.emitter.trigger('pointermove', ev); + if (!_this.isDistanceSurpassed) { + var minDistance = _this.minDistance; + var distanceSq = void 0; // current distance from the origin, squared + var deltaX = ev.deltaX, deltaY = ev.deltaY; + distanceSq = deltaX * deltaX + deltaY * deltaY; + if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem + _this.handleDistanceSurpassed(ev); + } + } + if (_this.isDragging) { + // a real pointer move? (not one simulated by scrolling) + if (ev.origEvent.type !== 'scroll') { + _this.mirror.handleMove(ev.pageX, ev.pageY); + _this.autoScroller.handleMove(ev.pageX, ev.pageY); + } + _this.emitter.trigger('dragmove', ev); + } + } + }; + _this.onPointerUp = function (ev) { + if (_this.isInteracting) { // if false, still waiting for previous drag's revert + _this.isInteracting = false; + core.allowSelection(document.body); + core.allowContextMenu(document.body); + _this.emitter.trigger('pointerup', ev); // can potentially set mirrorNeedsRevert + if (_this.isDragging) { + _this.autoScroller.stop(); + _this.tryStopDrag(ev); // which will stop the mirror + } + if (_this.delayTimeoutId) { + clearTimeout(_this.delayTimeoutId); + _this.delayTimeoutId = null; + } + } + }; + var pointer = _this.pointer = new PointerDragging(containerEl); + pointer.emitter.on('pointerdown', _this.onPointerDown); + pointer.emitter.on('pointermove', _this.onPointerMove); + pointer.emitter.on('pointerup', _this.onPointerUp); + _this.mirror = new ElementMirror(); + _this.autoScroller = new AutoScroller(); + return _this; + } + FeaturefulElementDragging.prototype.destroy = function () { + this.pointer.destroy(); + }; + FeaturefulElementDragging.prototype.startDelay = function (ev) { + var _this = this; + if (typeof this.delay === 'number') { + this.delayTimeoutId = setTimeout(function () { + _this.delayTimeoutId = null; + _this.handleDelayEnd(ev); + }, this.delay); // not assignable to number! + } + else { + this.handleDelayEnd(ev); + } + }; + FeaturefulElementDragging.prototype.handleDelayEnd = function (ev) { + this.isDelayEnded = true; + this.tryStartDrag(ev); + }; + FeaturefulElementDragging.prototype.handleDistanceSurpassed = function (ev) { + this.isDistanceSurpassed = true; + this.tryStartDrag(ev); + }; + FeaturefulElementDragging.prototype.tryStartDrag = function (ev) { + if (this.isDelayEnded && this.isDistanceSurpassed) { + if (!this.pointer.wasTouchScroll || this.touchScrollAllowed) { + this.isDragging = true; + this.mirrorNeedsRevert = false; + this.autoScroller.start(ev.pageX, ev.pageY); + this.emitter.trigger('dragstart', ev); + if (this.touchScrollAllowed === false) { + this.pointer.cancelTouchScroll(); + } + } + } + }; + FeaturefulElementDragging.prototype.tryStopDrag = function (ev) { + // .stop() is ALWAYS asynchronous, which we NEED because we want all pointerup events + // that come from the document to fire beforehand. much more convenient this way. + this.mirror.stop(this.mirrorNeedsRevert, this.stopDrag.bind(this, ev) // bound with args + ); + }; + FeaturefulElementDragging.prototype.stopDrag = function (ev) { + this.isDragging = false; + this.emitter.trigger('dragend', ev); + }; + // fill in the implementations... + FeaturefulElementDragging.prototype.setIgnoreMove = function (bool) { + this.pointer.shouldIgnoreMove = bool; + }; + FeaturefulElementDragging.prototype.setMirrorIsVisible = function (bool) { + this.mirror.setIsVisible(bool); + }; + FeaturefulElementDragging.prototype.setMirrorNeedsRevert = function (bool) { + this.mirrorNeedsRevert = bool; + }; + FeaturefulElementDragging.prototype.setAutoScrollEnabled = function (bool) { + this.autoScroller.isEnabled = bool; + }; + return FeaturefulElementDragging; + }(core.ElementDragging)); + + /* + When this class is instantiated, it records the offset of an element (relative to the document topleft), + and continues to monitor scrolling, updating the cached coordinates if it needs to. + Does not access the DOM after instantiation, so highly performant. + + Also keeps track of all scrolling/overflow:hidden containers that are parents of the given element + and an determine if a given point is inside the combined clipping rectangle. + */ + var OffsetTracker = /** @class */ (function () { + function OffsetTracker(el) { + this.origRect = core.computeRect(el); + // will work fine for divs that have overflow:hidden + this.scrollCaches = core.getClippingParents(el).map(function (el) { + return new ElementScrollGeomCache(el, true); // listen=true + }); + } + OffsetTracker.prototype.destroy = function () { + for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) { + var scrollCache = _a[_i]; + scrollCache.destroy(); + } + }; + OffsetTracker.prototype.computeLeft = function () { + var left = this.origRect.left; + for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) { + var scrollCache = _a[_i]; + left += scrollCache.origScrollLeft - scrollCache.getScrollLeft(); + } + return left; + }; + OffsetTracker.prototype.computeTop = function () { + var top = this.origRect.top; + for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) { + var scrollCache = _a[_i]; + top += scrollCache.origScrollTop - scrollCache.getScrollTop(); + } + return top; + }; + OffsetTracker.prototype.isWithinClipping = function (pageX, pageY) { + var point = { left: pageX, top: pageY }; + for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) { + var scrollCache = _a[_i]; + if (!isIgnoredClipping(scrollCache.getEventTarget()) && + !core.pointInsideRect(point, scrollCache.clientRect)) { + return false; + } + } + return true; + }; + return OffsetTracker; + }()); + // certain clipping containers should never constrain interactions, like <html> and <body> + // https://github.com/fullcalendar/fullcalendar/issues/3615 + function isIgnoredClipping(node) { + var tagName = node.tagName; + return tagName === 'HTML' || tagName === 'BODY'; + } + + /* + Tracks movement over multiple droppable areas (aka "hits") + that exist in one or more DateComponents. + Relies on an existing draggable. + + emits: + - pointerdown + - dragstart + - hitchange - fires initially, even if not over a hit + - pointerup + - (hitchange - again, to null, if ended over a hit) + - dragend + */ + var HitDragging = /** @class */ (function () { + function HitDragging(dragging, droppableStore) { + var _this = this; + // options that can be set by caller + this.useSubjectCenter = false; + this.requireInitial = true; // if doesn't start out on a hit, won't emit any events + this.initialHit = null; + this.movingHit = null; + this.finalHit = null; // won't ever be populated if shouldIgnoreMove + this.handlePointerDown = function (ev) { + var dragging = _this.dragging; + _this.initialHit = null; + _this.movingHit = null; + _this.finalHit = null; + _this.prepareHits(); + _this.processFirstCoord(ev); + if (_this.initialHit || !_this.requireInitial) { + dragging.setIgnoreMove(false); + _this.emitter.trigger('pointerdown', ev); // TODO: fire this before computing processFirstCoord, so listeners can cancel. this gets fired by almost every handler :( + } + else { + dragging.setIgnoreMove(true); + } + }; + this.handleDragStart = function (ev) { + _this.emitter.trigger('dragstart', ev); + _this.handleMove(ev, true); // force = fire even if initially null + }; + this.handleDragMove = function (ev) { + _this.emitter.trigger('dragmove', ev); + _this.handleMove(ev); + }; + this.handlePointerUp = function (ev) { + _this.releaseHits(); + _this.emitter.trigger('pointerup', ev); + }; + this.handleDragEnd = function (ev) { + if (_this.movingHit) { + _this.emitter.trigger('hitupdate', null, true, ev); + } + _this.finalHit = _this.movingHit; + _this.movingHit = null; + _this.emitter.trigger('dragend', ev); + }; + this.droppableStore = droppableStore; + dragging.emitter.on('pointerdown', this.handlePointerDown); + dragging.emitter.on('dragstart', this.handleDragStart); + dragging.emitter.on('dragmove', this.handleDragMove); + dragging.emitter.on('pointerup', this.handlePointerUp); + dragging.emitter.on('dragend', this.handleDragEnd); + this.dragging = dragging; + this.emitter = new core.EmitterMixin(); + } + // sets initialHit + // sets coordAdjust + HitDragging.prototype.processFirstCoord = function (ev) { + var origPoint = { left: ev.pageX, top: ev.pageY }; + var adjustedPoint = origPoint; + var subjectEl = ev.subjectEl; + var subjectRect; + if (subjectEl !== document) { + subjectRect = core.computeRect(subjectEl); + adjustedPoint = core.constrainPoint(adjustedPoint, subjectRect); + } + var initialHit = this.initialHit = this.queryHitForOffset(adjustedPoint.left, adjustedPoint.top); + if (initialHit) { + if (this.useSubjectCenter && subjectRect) { + var slicedSubjectRect = core.intersectRects(subjectRect, initialHit.rect); + if (slicedSubjectRect) { + adjustedPoint = core.getRectCenter(slicedSubjectRect); + } + } + this.coordAdjust = core.diffPoints(adjustedPoint, origPoint); + } + else { + this.coordAdjust = { left: 0, top: 0 }; + } + }; + HitDragging.prototype.handleMove = function (ev, forceHandle) { + var hit = this.queryHitForOffset(ev.pageX + this.coordAdjust.left, ev.pageY + this.coordAdjust.top); + if (forceHandle || !isHitsEqual(this.movingHit, hit)) { + this.movingHit = hit; + this.emitter.trigger('hitupdate', hit, false, ev); + } + }; + HitDragging.prototype.prepareHits = function () { + this.offsetTrackers = core.mapHash(this.droppableStore, function (interactionSettings) { + interactionSettings.component.buildPositionCaches(); + return new OffsetTracker(interactionSettings.el); + }); + }; + HitDragging.prototype.releaseHits = function () { + var offsetTrackers = this.offsetTrackers; + for (var id in offsetTrackers) { + offsetTrackers[id].destroy(); + } + this.offsetTrackers = {}; + }; + HitDragging.prototype.queryHitForOffset = function (offsetLeft, offsetTop) { + var _a = this, droppableStore = _a.droppableStore, offsetTrackers = _a.offsetTrackers; + var bestHit = null; + for (var id in droppableStore) { + var component = droppableStore[id].component; + var offsetTracker = offsetTrackers[id]; + if (offsetTracker.isWithinClipping(offsetLeft, offsetTop)) { + var originLeft = offsetTracker.computeLeft(); + var originTop = offsetTracker.computeTop(); + var positionLeft = offsetLeft - originLeft; + var positionTop = offsetTop - originTop; + var origRect = offsetTracker.origRect; + var width = origRect.right - origRect.left; + var height = origRect.bottom - origRect.top; + if ( + // must be within the element's bounds + positionLeft >= 0 && positionLeft < width && + positionTop >= 0 && positionTop < height) { + var hit = component.queryHit(positionLeft, positionTop, width, height); + if (hit && + ( + // make sure the hit is within activeRange, meaning it's not a deal cell + !component.props.dateProfile || // hack for DayTile + core.rangeContainsRange(component.props.dateProfile.activeRange, hit.dateSpan.range)) && + (!bestHit || hit.layer > bestHit.layer)) { + // TODO: better way to re-orient rectangle + hit.rect.left += originLeft; + hit.rect.right += originLeft; + hit.rect.top += originTop; + hit.rect.bottom += originTop; + bestHit = hit; + } + } + } + } + return bestHit; + }; + return HitDragging; + }()); + function isHitsEqual(hit0, hit1) { + if (!hit0 && !hit1) { + return true; + } + if (Boolean(hit0) !== Boolean(hit1)) { + return false; + } + return core.isDateSpansEqual(hit0.dateSpan, hit1.dateSpan); + } + + /* + Monitors when the user clicks on a specific date/time of a component. + A pointerdown+pointerup on the same "hit" constitutes a click. + */ + var DateClicking = /** @class */ (function (_super) { + __extends(DateClicking, _super); + function DateClicking(settings) { + var _this = _super.call(this, settings) || this; + _this.handlePointerDown = function (ev) { + var dragging = _this.dragging; + // do this in pointerdown (not dragend) because DOM might be mutated by the time dragend is fired + dragging.setIgnoreMove(!_this.component.isValidDateDownEl(dragging.pointer.downEl)); + }; + // won't even fire if moving was ignored + _this.handleDragEnd = function (ev) { + var component = _this.component; + var _a = component.context, calendar = _a.calendar, view = _a.view; + var pointer = _this.dragging.pointer; + if (!pointer.wasTouchScroll) { + var _b = _this.hitDragging, initialHit = _b.initialHit, finalHit = _b.finalHit; + if (initialHit && finalHit && isHitsEqual(initialHit, finalHit)) { + calendar.triggerDateClick(initialHit.dateSpan, initialHit.dayEl, view, ev.origEvent); + } + } + }; + var component = settings.component; + // we DO want to watch pointer moves because otherwise finalHit won't get populated + _this.dragging = new FeaturefulElementDragging(component.el); + _this.dragging.autoScroller.isEnabled = false; + var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, core.interactionSettingsToStore(settings)); + hitDragging.emitter.on('pointerdown', _this.handlePointerDown); + hitDragging.emitter.on('dragend', _this.handleDragEnd); + return _this; + } + DateClicking.prototype.destroy = function () { + this.dragging.destroy(); + }; + return DateClicking; + }(core.Interaction)); + + /* + Tracks when the user selects a portion of time of a component, + constituted by a drag over date cells, with a possible delay at the beginning of the drag. + */ + var DateSelecting = /** @class */ (function (_super) { + __extends(DateSelecting, _super); + function DateSelecting(settings) { + var _this = _super.call(this, settings) || this; + _this.dragSelection = null; + _this.handlePointerDown = function (ev) { + var _a = _this, component = _a.component, dragging = _a.dragging; + var options = component.context.options; + var canSelect = options.selectable && + component.isValidDateDownEl(ev.origEvent.target); + // don't bother to watch expensive moves if component won't do selection + dragging.setIgnoreMove(!canSelect); + // if touch, require user to hold down + dragging.delay = ev.isTouch ? getComponentTouchDelay(component) : null; + }; + _this.handleDragStart = function (ev) { + _this.component.context.calendar.unselect(ev); // unselect previous selections + }; + _this.handleHitUpdate = function (hit, isFinal) { + var calendar = _this.component.context.calendar; + var dragSelection = null; + var isInvalid = false; + if (hit) { + dragSelection = joinHitsIntoSelection(_this.hitDragging.initialHit, hit, calendar.pluginSystem.hooks.dateSelectionTransformers); + if (!dragSelection || !_this.component.isDateSelectionValid(dragSelection)) { + isInvalid = true; + dragSelection = null; + } + } + if (dragSelection) { + calendar.dispatch({ type: 'SELECT_DATES', selection: dragSelection }); + } + else if (!isFinal) { // only unselect if moved away while dragging + calendar.dispatch({ type: 'UNSELECT_DATES' }); + } + if (!isInvalid) { + core.enableCursor(); + } + else { + core.disableCursor(); + } + if (!isFinal) { + _this.dragSelection = dragSelection; // only clear if moved away from all hits while dragging + } + }; + _this.handlePointerUp = function (pev) { + if (_this.dragSelection) { + // selection is already rendered, so just need to report selection + _this.component.context.calendar.triggerDateSelect(_this.dragSelection, pev); + _this.dragSelection = null; + } + }; + var component = settings.component; + var options = component.context.options; + var dragging = _this.dragging = new FeaturefulElementDragging(component.el); + dragging.touchScrollAllowed = false; + dragging.minDistance = options.selectMinDistance || 0; + dragging.autoScroller.isEnabled = options.dragScroll; + var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, core.interactionSettingsToStore(settings)); + hitDragging.emitter.on('pointerdown', _this.handlePointerDown); + hitDragging.emitter.on('dragstart', _this.handleDragStart); + hitDragging.emitter.on('hitupdate', _this.handleHitUpdate); + hitDragging.emitter.on('pointerup', _this.handlePointerUp); + return _this; + } + DateSelecting.prototype.destroy = function () { + this.dragging.destroy(); + }; + return DateSelecting; + }(core.Interaction)); + function getComponentTouchDelay(component) { + var options = component.context.options; + var delay = options.selectLongPressDelay; + if (delay == null) { + delay = options.longPressDelay; + } + return delay; + } + function joinHitsIntoSelection(hit0, hit1, dateSelectionTransformers) { + var dateSpan0 = hit0.dateSpan; + var dateSpan1 = hit1.dateSpan; + var ms = [ + dateSpan0.range.start, + dateSpan0.range.end, + dateSpan1.range.start, + dateSpan1.range.end + ]; + ms.sort(core.compareNumbers); + var props = {}; + for (var _i = 0, dateSelectionTransformers_1 = dateSelectionTransformers; _i < dateSelectionTransformers_1.length; _i++) { + var transformer = dateSelectionTransformers_1[_i]; + var res = transformer(hit0, hit1); + if (res === false) { + return null; + } + else if (res) { + __assign(props, res); + } + } + props.range = { start: ms[0], end: ms[3] }; + props.allDay = dateSpan0.allDay; + return props; + } + + var EventDragging = /** @class */ (function (_super) { + __extends(EventDragging, _super); + function EventDragging(settings) { + var _this = _super.call(this, settings) || this; + // internal state + _this.subjectSeg = null; // the seg being selected/dragged + _this.isDragging = false; + _this.eventRange = null; + _this.relevantEvents = null; // the events being dragged + _this.receivingCalendar = null; + _this.validMutation = null; + _this.mutatedRelevantEvents = null; + _this.handlePointerDown = function (ev) { + var origTarget = ev.origEvent.target; + var _a = _this, component = _a.component, dragging = _a.dragging; + var mirror = dragging.mirror; + var options = component.context.options; + var initialCalendar = component.context.calendar; + var subjectSeg = _this.subjectSeg = core.getElSeg(ev.subjectEl); + var eventRange = _this.eventRange = subjectSeg.eventRange; + var eventInstanceId = eventRange.instance.instanceId; + _this.relevantEvents = core.getRelevantEvents(initialCalendar.state.eventStore, eventInstanceId); + dragging.minDistance = ev.isTouch ? 0 : options.eventDragMinDistance; + dragging.delay = + // only do a touch delay if touch and this event hasn't been selected yet + (ev.isTouch && eventInstanceId !== component.props.eventSelection) ? + getComponentTouchDelay$1(component) : + null; + mirror.parentNode = initialCalendar.el; + mirror.revertDuration = options.dragRevertDuration; + var isValid = component.isValidSegDownEl(origTarget) && + !core.elementClosest(origTarget, '.fc-resizer'); // NOT on a resizer + dragging.setIgnoreMove(!isValid); + // disable dragging for elements that are resizable (ie, selectable) + // but are not draggable + _this.isDragging = isValid && + ev.subjectEl.classList.contains('fc-draggable'); + }; + _this.handleDragStart = function (ev) { + var context = _this.component.context; + var initialCalendar = context.calendar; + var eventRange = _this.eventRange; + var eventInstanceId = eventRange.instance.instanceId; + if (ev.isTouch) { + // need to select a different event? + if (eventInstanceId !== _this.component.props.eventSelection) { + initialCalendar.dispatch({ type: 'SELECT_EVENT', eventInstanceId: eventInstanceId }); + } + } + else { + // if now using mouse, but was previous touch interaction, clear selected event + initialCalendar.dispatch({ type: 'UNSELECT_EVENT' }); + } + if (_this.isDragging) { + initialCalendar.unselect(ev); // unselect *date* selection + initialCalendar.publiclyTrigger('eventDragStart', [ + { + el: _this.subjectSeg.el, + event: new core.EventApi(initialCalendar, eventRange.def, eventRange.instance), + jsEvent: ev.origEvent, + view: context.view + } + ]); + } + }; + _this.handleHitUpdate = function (hit, isFinal) { + if (!_this.isDragging) { + return; + } + var relevantEvents = _this.relevantEvents; + var initialHit = _this.hitDragging.initialHit; + var initialCalendar = _this.component.context.calendar; + // states based on new hit + var receivingCalendar = null; + var mutation = null; + var mutatedRelevantEvents = null; + var isInvalid = false; + var interaction = { + affectedEvents: relevantEvents, + mutatedEvents: core.createEmptyEventStore(), + isEvent: true, + origSeg: _this.subjectSeg + }; + if (hit) { + var receivingComponent = hit.component; + receivingCalendar = receivingComponent.context.calendar; + var receivingOptions = receivingComponent.context.options; + if (initialCalendar === receivingCalendar || + receivingOptions.editable && receivingOptions.droppable) { + mutation = computeEventMutation(initialHit, hit, receivingCalendar.pluginSystem.hooks.eventDragMutationMassagers); + if (mutation) { + mutatedRelevantEvents = core.applyMutationToEventStore(relevantEvents, receivingCalendar.eventUiBases, mutation, receivingCalendar); + interaction.mutatedEvents = mutatedRelevantEvents; + if (!receivingComponent.isInteractionValid(interaction)) { + isInvalid = true; + mutation = null; + mutatedRelevantEvents = null; + interaction.mutatedEvents = core.createEmptyEventStore(); + } + } + } + else { + receivingCalendar = null; + } + } + _this.displayDrag(receivingCalendar, interaction); + if (!isInvalid) { + core.enableCursor(); + } + else { + core.disableCursor(); + } + if (!isFinal) { + if (initialCalendar === receivingCalendar && // TODO: write test for this + isHitsEqual(initialHit, hit)) { + mutation = null; + } + _this.dragging.setMirrorNeedsRevert(!mutation); + // render the mirror if no already-rendered mirror + // TODO: wish we could somehow wait for dispatch to guarantee render + _this.dragging.setMirrorIsVisible(!hit || !document.querySelector('.fc-mirror')); + // assign states based on new hit + _this.receivingCalendar = receivingCalendar; + _this.validMutation = mutation; + _this.mutatedRelevantEvents = mutatedRelevantEvents; + } + }; + _this.handlePointerUp = function () { + if (!_this.isDragging) { + _this.cleanup(); // because handleDragEnd won't fire + } + }; + _this.handleDragEnd = function (ev) { + if (_this.isDragging) { + var context = _this.component.context; + var initialCalendar_1 = context.calendar; + var initialView = context.view; + var _a = _this, receivingCalendar = _a.receivingCalendar, validMutation = _a.validMutation; + var eventDef = _this.eventRange.def; + var eventInstance = _this.eventRange.instance; + var eventApi = new core.EventApi(initialCalendar_1, eventDef, eventInstance); + var relevantEvents_1 = _this.relevantEvents; + var mutatedRelevantEvents = _this.mutatedRelevantEvents; + var finalHit = _this.hitDragging.finalHit; + _this.clearDrag(); // must happen after revert animation + initialCalendar_1.publiclyTrigger('eventDragStop', [ + { + el: _this.subjectSeg.el, + event: eventApi, + jsEvent: ev.origEvent, + view: initialView + } + ]); + if (validMutation) { + // dropped within same calendar + if (receivingCalendar === initialCalendar_1) { + initialCalendar_1.dispatch({ + type: 'MERGE_EVENTS', + eventStore: mutatedRelevantEvents + }); + var transformed = {}; + for (var _i = 0, _b = initialCalendar_1.pluginSystem.hooks.eventDropTransformers; _i < _b.length; _i++) { + var transformer = _b[_i]; + __assign(transformed, transformer(validMutation, initialCalendar_1)); + } + var eventDropArg = __assign({}, transformed, { el: ev.subjectEl, delta: validMutation.datesDelta, oldEvent: eventApi, event: new core.EventApi(// the data AFTER the mutation + initialCalendar_1, mutatedRelevantEvents.defs[eventDef.defId], eventInstance ? mutatedRelevantEvents.instances[eventInstance.instanceId] : null), revert: function () { + initialCalendar_1.dispatch({ + type: 'MERGE_EVENTS', + eventStore: relevantEvents_1 + }); + }, jsEvent: ev.origEvent, view: initialView }); + initialCalendar_1.publiclyTrigger('eventDrop', [eventDropArg]); + // dropped in different calendar + } + else if (receivingCalendar) { + initialCalendar_1.publiclyTrigger('eventLeave', [ + { + draggedEl: ev.subjectEl, + event: eventApi, + view: initialView + } + ]); + initialCalendar_1.dispatch({ + type: 'REMOVE_EVENT_INSTANCES', + instances: _this.mutatedRelevantEvents.instances + }); + receivingCalendar.dispatch({ + type: 'MERGE_EVENTS', + eventStore: _this.mutatedRelevantEvents + }); + if (ev.isTouch) { + receivingCalendar.dispatch({ + type: 'SELECT_EVENT', + eventInstanceId: eventInstance.instanceId + }); + } + var dropArg = __assign({}, receivingCalendar.buildDatePointApi(finalHit.dateSpan), { draggedEl: ev.subjectEl, jsEvent: ev.origEvent, view: finalHit.component // should this be finalHit.component.view? See #4644 + }); + receivingCalendar.publiclyTrigger('drop', [dropArg]); + receivingCalendar.publiclyTrigger('eventReceive', [ + { + draggedEl: ev.subjectEl, + event: new core.EventApi(// the data AFTER the mutation + receivingCalendar, mutatedRelevantEvents.defs[eventDef.defId], mutatedRelevantEvents.instances[eventInstance.instanceId]), + view: finalHit.component // should this be finalHit.component.view? See #4644 + } + ]); + } + } + else { + initialCalendar_1.publiclyTrigger('_noEventDrop'); + } + } + _this.cleanup(); + }; + var component = _this.component; + var options = component.context.options; + var dragging = _this.dragging = new FeaturefulElementDragging(component.el); + dragging.pointer.selector = EventDragging.SELECTOR; + dragging.touchScrollAllowed = false; + dragging.autoScroller.isEnabled = options.dragScroll; + var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, core.interactionSettingsStore); + hitDragging.useSubjectCenter = settings.useEventCenter; + hitDragging.emitter.on('pointerdown', _this.handlePointerDown); + hitDragging.emitter.on('dragstart', _this.handleDragStart); + hitDragging.emitter.on('hitupdate', _this.handleHitUpdate); + hitDragging.emitter.on('pointerup', _this.handlePointerUp); + hitDragging.emitter.on('dragend', _this.handleDragEnd); + return _this; + } + EventDragging.prototype.destroy = function () { + this.dragging.destroy(); + }; + // render a drag state on the next receivingCalendar + EventDragging.prototype.displayDrag = function (nextCalendar, state) { + var initialCalendar = this.component.context.calendar; + var prevCalendar = this.receivingCalendar; + // does the previous calendar need to be cleared? + if (prevCalendar && prevCalendar !== nextCalendar) { + // does the initial calendar need to be cleared? + // if so, don't clear all the way. we still need to to hide the affectedEvents + if (prevCalendar === initialCalendar) { + prevCalendar.dispatch({ + type: 'SET_EVENT_DRAG', + state: { + affectedEvents: state.affectedEvents, + mutatedEvents: core.createEmptyEventStore(), + isEvent: true, + origSeg: state.origSeg + } + }); + // completely clear the old calendar if it wasn't the initial + } + else { + prevCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' }); + } + } + if (nextCalendar) { + nextCalendar.dispatch({ type: 'SET_EVENT_DRAG', state: state }); + } + }; + EventDragging.prototype.clearDrag = function () { + var initialCalendar = this.component.context.calendar; + var receivingCalendar = this.receivingCalendar; + if (receivingCalendar) { + receivingCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' }); + } + // the initial calendar might have an dummy drag state from displayDrag + if (initialCalendar !== receivingCalendar) { + initialCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' }); + } + }; + EventDragging.prototype.cleanup = function () { + this.subjectSeg = null; + this.isDragging = false; + this.eventRange = null; + this.relevantEvents = null; + this.receivingCalendar = null; + this.validMutation = null; + this.mutatedRelevantEvents = null; + }; + EventDragging.SELECTOR = '.fc-draggable, .fc-resizable'; // TODO: test this in IE11 + return EventDragging; + }(core.Interaction)); + function computeEventMutation(hit0, hit1, massagers) { + var dateSpan0 = hit0.dateSpan; + var dateSpan1 = hit1.dateSpan; + var date0 = dateSpan0.range.start; + var date1 = dateSpan1.range.start; + var standardProps = {}; + if (dateSpan0.allDay !== dateSpan1.allDay) { + standardProps.allDay = dateSpan1.allDay; + standardProps.hasEnd = hit1.component.context.options.allDayMaintainDuration; + if (dateSpan1.allDay) { + // means date1 is already start-of-day, + // but date0 needs to be converted + date0 = core.startOfDay(date0); + } + } + var delta = core.diffDates(date0, date1, hit0.component.context.dateEnv, hit0.component === hit1.component ? + hit0.component.largeUnit : + null); + if (delta.milliseconds) { // has hours/minutes/seconds + standardProps.allDay = false; + } + var mutation = { + datesDelta: delta, + standardProps: standardProps + }; + for (var _i = 0, massagers_1 = massagers; _i < massagers_1.length; _i++) { + var massager = massagers_1[_i]; + massager(mutation, hit0, hit1); + } + return mutation; + } + function getComponentTouchDelay$1(component) { + var options = component.context.options; + var delay = options.eventLongPressDelay; + if (delay == null) { + delay = options.longPressDelay; + } + return delay; + } + + var EventDragging$1 = /** @class */ (function (_super) { + __extends(EventDragging, _super); + function EventDragging(settings) { + var _this = _super.call(this, settings) || this; + // internal state + _this.draggingSeg = null; // TODO: rename to resizingSeg? subjectSeg? + _this.eventRange = null; + _this.relevantEvents = null; + _this.validMutation = null; + _this.mutatedRelevantEvents = null; + _this.handlePointerDown = function (ev) { + var component = _this.component; + var seg = _this.querySeg(ev); + var eventRange = _this.eventRange = seg.eventRange; + _this.dragging.minDistance = component.context.options.eventDragMinDistance; + // if touch, need to be working with a selected event + _this.dragging.setIgnoreMove(!_this.component.isValidSegDownEl(ev.origEvent.target) || + (ev.isTouch && _this.component.props.eventSelection !== eventRange.instance.instanceId)); + }; + _this.handleDragStart = function (ev) { + var _a = _this.component.context, calendar = _a.calendar, view = _a.view; + var eventRange = _this.eventRange; + _this.relevantEvents = core.getRelevantEvents(calendar.state.eventStore, _this.eventRange.instance.instanceId); + _this.draggingSeg = _this.querySeg(ev); + calendar.unselect(); + calendar.publiclyTrigger('eventResizeStart', [ + { + el: _this.draggingSeg.el, + event: new core.EventApi(calendar, eventRange.def, eventRange.instance), + jsEvent: ev.origEvent, + view: view + } + ]); + }; + _this.handleHitUpdate = function (hit, isFinal, ev) { + var calendar = _this.component.context.calendar; + var relevantEvents = _this.relevantEvents; + var initialHit = _this.hitDragging.initialHit; + var eventInstance = _this.eventRange.instance; + var mutation = null; + var mutatedRelevantEvents = null; + var isInvalid = false; + var interaction = { + affectedEvents: relevantEvents, + mutatedEvents: core.createEmptyEventStore(), + isEvent: true, + origSeg: _this.draggingSeg + }; + if (hit) { + mutation = computeMutation(initialHit, hit, ev.subjectEl.classList.contains('fc-start-resizer'), eventInstance.range, calendar.pluginSystem.hooks.eventResizeJoinTransforms); + } + if (mutation) { + mutatedRelevantEvents = core.applyMutationToEventStore(relevantEvents, calendar.eventUiBases, mutation, calendar); + interaction.mutatedEvents = mutatedRelevantEvents; + if (!_this.component.isInteractionValid(interaction)) { + isInvalid = true; + mutation = null; + mutatedRelevantEvents = null; + interaction.mutatedEvents = null; + } + } + if (mutatedRelevantEvents) { + calendar.dispatch({ + type: 'SET_EVENT_RESIZE', + state: interaction + }); + } + else { + calendar.dispatch({ type: 'UNSET_EVENT_RESIZE' }); + } + if (!isInvalid) { + core.enableCursor(); + } + else { + core.disableCursor(); + } + if (!isFinal) { + if (mutation && isHitsEqual(initialHit, hit)) { + mutation = null; + } + _this.validMutation = mutation; + _this.mutatedRelevantEvents = mutatedRelevantEvents; + } + }; + _this.handleDragEnd = function (ev) { + var _a = _this.component.context, calendar = _a.calendar, view = _a.view; + var eventDef = _this.eventRange.def; + var eventInstance = _this.eventRange.instance; + var eventApi = new core.EventApi(calendar, eventDef, eventInstance); + var relevantEvents = _this.relevantEvents; + var mutatedRelevantEvents = _this.mutatedRelevantEvents; + calendar.publiclyTrigger('eventResizeStop', [ + { + el: _this.draggingSeg.el, + event: eventApi, + jsEvent: ev.origEvent, + view: view + } + ]); + if (_this.validMutation) { + calendar.dispatch({ + type: 'MERGE_EVENTS', + eventStore: mutatedRelevantEvents + }); + calendar.publiclyTrigger('eventResize', [ + { + el: _this.draggingSeg.el, + startDelta: _this.validMutation.startDelta || core.createDuration(0), + endDelta: _this.validMutation.endDelta || core.createDuration(0), + prevEvent: eventApi, + event: new core.EventApi(// the data AFTER the mutation + calendar, mutatedRelevantEvents.defs[eventDef.defId], eventInstance ? mutatedRelevantEvents.instances[eventInstance.instanceId] : null), + revert: function () { + calendar.dispatch({ + type: 'MERGE_EVENTS', + eventStore: relevantEvents + }); + }, + jsEvent: ev.origEvent, + view: view + } + ]); + } + else { + calendar.publiclyTrigger('_noEventResize'); + } + // reset all internal state + _this.draggingSeg = null; + _this.relevantEvents = null; + _this.validMutation = null; + // okay to keep eventInstance around. useful to set it in handlePointerDown + }; + var component = settings.component; + var dragging = _this.dragging = new FeaturefulElementDragging(component.el); + dragging.pointer.selector = '.fc-resizer'; + dragging.touchScrollAllowed = false; + dragging.autoScroller.isEnabled = component.context.options.dragScroll; + var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, core.interactionSettingsToStore(settings)); + hitDragging.emitter.on('pointerdown', _this.handlePointerDown); + hitDragging.emitter.on('dragstart', _this.handleDragStart); + hitDragging.emitter.on('hitupdate', _this.handleHitUpdate); + hitDragging.emitter.on('dragend', _this.handleDragEnd); + return _this; + } + EventDragging.prototype.destroy = function () { + this.dragging.destroy(); + }; + EventDragging.prototype.querySeg = function (ev) { + return core.getElSeg(core.elementClosest(ev.subjectEl, this.component.fgSegSelector)); + }; + return EventDragging; + }(core.Interaction)); + function computeMutation(hit0, hit1, isFromStart, instanceRange, transforms) { + var dateEnv = hit0.component.context.dateEnv; + var date0 = hit0.dateSpan.range.start; + var date1 = hit1.dateSpan.range.start; + var delta = core.diffDates(date0, date1, dateEnv, hit0.component.largeUnit); + var props = {}; + for (var _i = 0, transforms_1 = transforms; _i < transforms_1.length; _i++) { + var transform = transforms_1[_i]; + var res = transform(hit0, hit1); + if (res === false) { + return null; + } + else if (res) { + __assign(props, res); + } + } + if (isFromStart) { + if (dateEnv.add(instanceRange.start, delta) < instanceRange.end) { + props.startDelta = delta; + return props; + } + } + else { + if (dateEnv.add(instanceRange.end, delta) > instanceRange.start) { + props.endDelta = delta; + return props; + } + } + return null; + } + + var UnselectAuto = /** @class */ (function () { + function UnselectAuto(calendar) { + var _this = this; + this.isRecentPointerDateSelect = false; // wish we could use a selector to detect date selection, but uses hit system + this.onSelect = function (selectInfo) { + if (selectInfo.jsEvent) { + _this.isRecentPointerDateSelect = true; + } + }; + this.onDocumentPointerUp = function (pev) { + var _a = _this, calendar = _a.calendar, documentPointer = _a.documentPointer; + var state = calendar.state; + // touch-scrolling should never unfocus any type of selection + if (!documentPointer.wasTouchScroll) { + if (state.dateSelection && // an existing date selection? + !_this.isRecentPointerDateSelect // a new pointer-initiated date selection since last onDocumentPointerUp? + ) { + var unselectAuto = calendar.viewOpt('unselectAuto'); + var unselectCancel = calendar.viewOpt('unselectCancel'); + if (unselectAuto && (!unselectAuto || !core.elementClosest(documentPointer.downEl, unselectCancel))) { + calendar.unselect(pev); + } + } + if (state.eventSelection && // an existing event selected? + !core.elementClosest(documentPointer.downEl, EventDragging.SELECTOR) // interaction DIDN'T start on an event + ) { + calendar.dispatch({ type: 'UNSELECT_EVENT' }); + } + } + _this.isRecentPointerDateSelect = false; + }; + this.calendar = calendar; + var documentPointer = this.documentPointer = new PointerDragging(document); + documentPointer.shouldIgnoreMove = true; + documentPointer.shouldWatchScroll = false; + documentPointer.emitter.on('pointerup', this.onDocumentPointerUp); + /* + TODO: better way to know about whether there was a selection with the pointer + */ + calendar.on('select', this.onSelect); + } + UnselectAuto.prototype.destroy = function () { + this.calendar.off('select', this.onSelect); + this.documentPointer.destroy(); + }; + return UnselectAuto; + }()); + + /* + Given an already instantiated draggable object for one-or-more elements, + Interprets any dragging as an attempt to drag an events that lives outside + of a calendar onto a calendar. + */ + var ExternalElementDragging = /** @class */ (function () { + function ExternalElementDragging(dragging, suppliedDragMeta) { + var _this = this; + this.receivingCalendar = null; + this.droppableEvent = null; // will exist for all drags, even if create:false + this.suppliedDragMeta = null; + this.dragMeta = null; + this.handleDragStart = function (ev) { + _this.dragMeta = _this.buildDragMeta(ev.subjectEl); + }; + this.handleHitUpdate = function (hit, isFinal, ev) { + var dragging = _this.hitDragging.dragging; + var receivingCalendar = null; + var droppableEvent = null; + var isInvalid = false; + var interaction = { + affectedEvents: core.createEmptyEventStore(), + mutatedEvents: core.createEmptyEventStore(), + isEvent: _this.dragMeta.create, + origSeg: null + }; + if (hit) { + receivingCalendar = hit.component.context.calendar; + if (_this.canDropElOnCalendar(ev.subjectEl, receivingCalendar)) { + droppableEvent = computeEventForDateSpan(hit.dateSpan, _this.dragMeta, receivingCalendar); + interaction.mutatedEvents = core.eventTupleToStore(droppableEvent); + isInvalid = !core.isInteractionValid(interaction, receivingCalendar); + if (isInvalid) { + interaction.mutatedEvents = core.createEmptyEventStore(); + droppableEvent = null; + } + } + } + _this.displayDrag(receivingCalendar, interaction); + // show mirror if no already-rendered mirror element OR if we are shutting down the mirror (?) + // TODO: wish we could somehow wait for dispatch to guarantee render + dragging.setMirrorIsVisible(isFinal || !droppableEvent || !document.querySelector('.fc-mirror')); + if (!isInvalid) { + core.enableCursor(); + } + else { + core.disableCursor(); + } + if (!isFinal) { + dragging.setMirrorNeedsRevert(!droppableEvent); + _this.receivingCalendar = receivingCalendar; + _this.droppableEvent = droppableEvent; + } + }; + this.handleDragEnd = function (pev) { + var _a = _this, receivingCalendar = _a.receivingCalendar, droppableEvent = _a.droppableEvent; + _this.clearDrag(); + if (receivingCalendar && droppableEvent) { + var finalHit = _this.hitDragging.finalHit; + var finalView = finalHit.component.context.view; + var dragMeta = _this.dragMeta; + var arg = __assign({}, receivingCalendar.buildDatePointApi(finalHit.dateSpan), { draggedEl: pev.subjectEl, jsEvent: pev.origEvent, view: finalView }); + receivingCalendar.publiclyTrigger('drop', [arg]); + if (dragMeta.create) { + receivingCalendar.dispatch({ + type: 'MERGE_EVENTS', + eventStore: core.eventTupleToStore(droppableEvent) + }); + if (pev.isTouch) { + receivingCalendar.dispatch({ + type: 'SELECT_EVENT', + eventInstanceId: droppableEvent.instance.instanceId + }); + } + // signal that an external event landed + receivingCalendar.publiclyTrigger('eventReceive', [ + { + draggedEl: pev.subjectEl, + event: new core.EventApi(receivingCalendar, droppableEvent.def, droppableEvent.instance), + view: finalView + } + ]); + } + } + _this.receivingCalendar = null; + _this.droppableEvent = null; + }; + var hitDragging = this.hitDragging = new HitDragging(dragging, core.interactionSettingsStore); + hitDragging.requireInitial = false; // will start outside of a component + hitDragging.emitter.on('dragstart', this.handleDragStart); + hitDragging.emitter.on('hitupdate', this.handleHitUpdate); + hitDragging.emitter.on('dragend', this.handleDragEnd); + this.suppliedDragMeta = suppliedDragMeta; + } + ExternalElementDragging.prototype.buildDragMeta = function (subjectEl) { + if (typeof this.suppliedDragMeta === 'object') { + return core.parseDragMeta(this.suppliedDragMeta); + } + else if (typeof this.suppliedDragMeta === 'function') { + return core.parseDragMeta(this.suppliedDragMeta(subjectEl)); + } + else { + return getDragMetaFromEl(subjectEl); + } + }; + ExternalElementDragging.prototype.displayDrag = function (nextCalendar, state) { + var prevCalendar = this.receivingCalendar; + if (prevCalendar && prevCalendar !== nextCalendar) { + prevCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' }); + } + if (nextCalendar) { + nextCalendar.dispatch({ type: 'SET_EVENT_DRAG', state: state }); + } + }; + ExternalElementDragging.prototype.clearDrag = function () { + if (this.receivingCalendar) { + this.receivingCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' }); + } + }; + ExternalElementDragging.prototype.canDropElOnCalendar = function (el, receivingCalendar) { + var dropAccept = receivingCalendar.opt('dropAccept'); + if (typeof dropAccept === 'function') { + return dropAccept(el); + } + else if (typeof dropAccept === 'string' && dropAccept) { + return Boolean(core.elementMatches(el, dropAccept)); + } + return true; + }; + return ExternalElementDragging; + }()); + // Utils for computing event store from the DragMeta + // ---------------------------------------------------------------------------------------------------- + function computeEventForDateSpan(dateSpan, dragMeta, calendar) { + var defProps = __assign({}, dragMeta.leftoverProps); + for (var _i = 0, _a = calendar.pluginSystem.hooks.externalDefTransforms; _i < _a.length; _i++) { + var transform = _a[_i]; + __assign(defProps, transform(dateSpan, dragMeta)); + } + var def = core.parseEventDef(defProps, dragMeta.sourceId, dateSpan.allDay, calendar.opt('forceEventDuration') || Boolean(dragMeta.duration), // hasEnd + calendar); + var start = dateSpan.range.start; + // only rely on time info if drop zone is all-day, + // otherwise, we already know the time + if (dateSpan.allDay && dragMeta.startTime) { + start = calendar.dateEnv.add(start, dragMeta.startTime); + } + var end = dragMeta.duration ? + calendar.dateEnv.add(start, dragMeta.duration) : + calendar.getDefaultEventEnd(dateSpan.allDay, start); + var instance = core.createEventInstance(def.defId, { start: start, end: end }); + return { def: def, instance: instance }; + } + // Utils for extracting data from element + // ---------------------------------------------------------------------------------------------------- + function getDragMetaFromEl(el) { + var str = getEmbeddedElData(el, 'event'); + var obj = str ? + JSON.parse(str) : + { create: false }; // if no embedded data, assume no event creation + return core.parseDragMeta(obj); + } + core.config.dataAttrPrefix = ''; + function getEmbeddedElData(el, name) { + var prefix = core.config.dataAttrPrefix; + var prefixedName = (prefix ? prefix + '-' : '') + name; + return el.getAttribute('data-' + prefixedName) || ''; + } + + /* + Makes an element (that is *external* to any calendar) draggable. + Can pass in data that determines how an event will be created when dropped onto a calendar. + Leverages FullCalendar's internal drag-n-drop functionality WITHOUT a third-party drag system. + */ + var ExternalDraggable = /** @class */ (function () { + function ExternalDraggable(el, settings) { + var _this = this; + if (settings === void 0) { settings = {}; } + this.handlePointerDown = function (ev) { + var dragging = _this.dragging; + var _a = _this.settings, minDistance = _a.minDistance, longPressDelay = _a.longPressDelay; + dragging.minDistance = + minDistance != null ? + minDistance : + (ev.isTouch ? 0 : core.globalDefaults.eventDragMinDistance); + dragging.delay = + ev.isTouch ? // TODO: eventually read eventLongPressDelay instead vvv + (longPressDelay != null ? longPressDelay : core.globalDefaults.longPressDelay) : + 0; + }; + this.handleDragStart = function (ev) { + if (ev.isTouch && + _this.dragging.delay && + ev.subjectEl.classList.contains('fc-event')) { + _this.dragging.mirror.getMirrorEl().classList.add('fc-selected'); + } + }; + this.settings = settings; + var dragging = this.dragging = new FeaturefulElementDragging(el); + dragging.touchScrollAllowed = false; + if (settings.itemSelector != null) { + dragging.pointer.selector = settings.itemSelector; + } + if (settings.appendTo != null) { + dragging.mirror.parentNode = settings.appendTo; // TODO: write tests + } + dragging.emitter.on('pointerdown', this.handlePointerDown); + dragging.emitter.on('dragstart', this.handleDragStart); + new ExternalElementDragging(dragging, settings.eventData); + } + ExternalDraggable.prototype.destroy = function () { + this.dragging.destroy(); + }; + return ExternalDraggable; + }()); + + /* + Detects when a *THIRD-PARTY* drag-n-drop system interacts with elements. + The third-party system is responsible for drawing the visuals effects of the drag. + This class simply monitors for pointer movements and fires events. + It also has the ability to hide the moving element (the "mirror") during the drag. + */ + var InferredElementDragging = /** @class */ (function (_super) { + __extends(InferredElementDragging, _super); + function InferredElementDragging(containerEl) { + var _this = _super.call(this, containerEl) || this; + _this.shouldIgnoreMove = false; + _this.mirrorSelector = ''; + _this.currentMirrorEl = null; + _this.handlePointerDown = function (ev) { + _this.emitter.trigger('pointerdown', ev); + if (!_this.shouldIgnoreMove) { + // fire dragstart right away. does not support delay or min-distance + _this.emitter.trigger('dragstart', ev); + } + }; + _this.handlePointerMove = function (ev) { + if (!_this.shouldIgnoreMove) { + _this.emitter.trigger('dragmove', ev); + } + }; + _this.handlePointerUp = function (ev) { + _this.emitter.trigger('pointerup', ev); + if (!_this.shouldIgnoreMove) { + // fire dragend right away. does not support a revert animation + _this.emitter.trigger('dragend', ev); + } + }; + var pointer = _this.pointer = new PointerDragging(containerEl); + pointer.emitter.on('pointerdown', _this.handlePointerDown); + pointer.emitter.on('pointermove', _this.handlePointerMove); + pointer.emitter.on('pointerup', _this.handlePointerUp); + return _this; + } + InferredElementDragging.prototype.destroy = function () { + this.pointer.destroy(); + }; + InferredElementDragging.prototype.setIgnoreMove = function (bool) { + this.shouldIgnoreMove = bool; + }; + InferredElementDragging.prototype.setMirrorIsVisible = function (bool) { + if (bool) { + // restore a previously hidden element. + // use the reference in case the selector class has already been removed. + if (this.currentMirrorEl) { + this.currentMirrorEl.style.visibility = ''; + this.currentMirrorEl = null; + } + } + else { + var mirrorEl = this.mirrorSelector ? + document.querySelector(this.mirrorSelector) : + null; + if (mirrorEl) { + this.currentMirrorEl = mirrorEl; + mirrorEl.style.visibility = 'hidden'; + } + } + }; + return InferredElementDragging; + }(core.ElementDragging)); + + /* + Bridges third-party drag-n-drop systems with FullCalendar. + Must be instantiated and destroyed by caller. + */ + var ThirdPartyDraggable = /** @class */ (function () { + function ThirdPartyDraggable(containerOrSettings, settings) { + var containerEl = document; + if ( + // wish we could just test instanceof EventTarget, but doesn't work in IE11 + containerOrSettings === document || + containerOrSettings instanceof Element) { + containerEl = containerOrSettings; + settings = settings || {}; + } + else { + settings = (containerOrSettings || {}); + } + var dragging = this.dragging = new InferredElementDragging(containerEl); + if (typeof settings.itemSelector === 'string') { + dragging.pointer.selector = settings.itemSelector; + } + else if (containerEl === document) { + dragging.pointer.selector = '[data-event]'; + } + if (typeof settings.mirrorSelector === 'string') { + dragging.mirrorSelector = settings.mirrorSelector; + } + new ExternalElementDragging(dragging, settings.eventData); + } + ThirdPartyDraggable.prototype.destroy = function () { + this.dragging.destroy(); + }; + return ThirdPartyDraggable; + }()); + + var main = core.createPlugin({ + componentInteractions: [DateClicking, DateSelecting, EventDragging, EventDragging$1], + calendarInteractions: [UnselectAuto], + elementDraggingImpl: FeaturefulElementDragging + }); + + exports.Draggable = ExternalDraggable; + exports.FeaturefulElementDragging = FeaturefulElementDragging; + exports.PointerDragging = PointerDragging; + exports.ThirdPartyDraggable = ThirdPartyDraggable; + exports.default = main; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); diff --git a/addons/web/static/lib/fullcalendar/js/fullcalendar.js b/addons/web/static/lib/fullcalendar/js/fullcalendar.js deleted file mode 100644 index 4fdf8482db7..00000000000 --- a/addons/web/static/lib/fullcalendar/js/fullcalendar.js +++ /dev/null @@ -1,13509 +0,0 @@ -/*! - * FullCalendar v3.0.1 - * Docs & License: http://fullcalendar.io/ - * (c) 2016 Adam Shaw - */ - -(function(factory) { - if (typeof define === 'function' && define.amd) { - define([ 'jquery', 'moment' ], factory); - } - else if (typeof exports === 'object') { // Node/CommonJS - module.exports = factory(require('jquery'), require('moment')); - } - else { - factory(jQuery, moment); - } -})(function($, moment) { - -;; - -var FC = $.fullCalendar = { - version: "3.0.1", - internalApiVersion: 6 -}; -var fcViews = FC.views = {}; - - -$.fn.fullCalendar = function(options) { - var args = Array.prototype.slice.call(arguments, 1); // for a possible method call - var res = this; // what this function will return (this jQuery object by default) - - this.each(function(i, _element) { // loop each DOM element involved - var element = $(_element); - var calendar = element.data('fullCalendar'); // get the existing calendar object (if any) - var singleRes; // the returned value of this single method call - - // a method call - if (typeof options === 'string') { - if (calendar && $.isFunction(calendar[options])) { - singleRes = calendar[options].apply(calendar, args); - if (!i) { - res = singleRes; // record the first method call result - } - if (options === 'destroy') { // for the destroy method, must remove Calendar object data - element.removeData('fullCalendar'); - } - } - } - // a new calendar initialization - else if (!calendar) { // don't initialize twice - calendar = new Calendar(element, options); - element.data('fullCalendar', calendar); - calendar.render(); - } - }); - - return res; -}; - - -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); -} - -;; - -// exports -FC.intersectRanges = intersectRanges; -FC.applyAll = applyAll; -FC.debounce = debounce; -FC.isInt = isInt; -FC.htmlEscape = htmlEscape; -FC.cssToStr = cssToStr; -FC.proxy = proxy; -FC.capitaliseFirstLetter = capitaliseFirstLetter; - - -/* FullCalendar-specific DOM Utilities -----------------------------------------------------------------------------------------------------------------------*/ - - -// Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left -// and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that. -function compensateScroll(rowEls, scrollbarWidths) { - if (scrollbarWidths.left) { - rowEls.css({ - 'border-left-width': 1, - 'margin-left': scrollbarWidths.left - 1 - }); - } - if (scrollbarWidths.right) { - rowEls.css({ - 'border-right-width': 1, - 'margin-right': scrollbarWidths.right - 1 - }); - } -} - - -// Undoes compensateScroll and restores all borders/margins -function uncompensateScroll(rowEls) { - rowEls.css({ - 'margin-left': '', - 'margin-right': '', - 'border-left-width': '', - 'border-right-width': '' - }); -} - - -// Make the mouse cursor express that an event is not allowed in the current area -function disableCursor() { - $('body').addClass('fc-not-allowed'); -} - - -// Returns the mouse cursor to its original look -function enableCursor() { - $('body').removeClass('fc-not-allowed'); -} - - -// Given a total available height to fill, have `els` (essentially child rows) expand to accomodate. -// By default, all elements that are shorter than the recommended height are expanded uniformly, not considering -// any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and -// reduces the available height. -function distributeHeight(els, availableHeight, shouldRedistribute) { - - // *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions, - // and it is better to be shorter than taller, to avoid creating unnecessary scrollbars. - - var minOffset1 = Math.floor(availableHeight / els.length); // for non-last element - var minOffset2 = Math.floor(availableHeight - minOffset1 * (els.length - 1)); // for last element *FLOORING NOTE* - var flexEls = []; // elements that are allowed to expand. array of DOM nodes - var flexOffsets = []; // amount of vertical space it takes up - var flexHeights = []; // actual css height - var usedHeight = 0; - - undistributeHeight(els); // give all elements their natural height - - // find elements that are below the recommended height (expandable). - // important to query for heights in a single first pass (to avoid reflow oscillation). - els.each(function(i, el) { - var minOffset = i === els.length - 1 ? minOffset2 : minOffset1; - var naturalOffset = $(el).outerHeight(true); - - if (naturalOffset < minOffset) { - flexEls.push(el); - flexOffsets.push(naturalOffset); - flexHeights.push($(el).height()); - } - else { - // this element stretches past recommended height (non-expandable). mark the space as occupied. - usedHeight += naturalOffset; - } - }); - - // readjust the recommended height to only consider the height available to non-maxed-out rows. - if (shouldRedistribute) { - availableHeight -= usedHeight; - minOffset1 = Math.floor(availableHeight / flexEls.length); - minOffset2 = Math.floor(availableHeight - minOffset1 * (flexEls.length - 1)); // *FLOORING NOTE* - } - - // assign heights to all expandable elements - $(flexEls).each(function(i, el) { - var minOffset = i === flexEls.length - 1 ? minOffset2 : minOffset1; - var naturalOffset = flexOffsets[i]; - var naturalHeight = flexHeights[i]; - var newHeight = minOffset - (naturalOffset - naturalHeight); // subtract the margin/padding - - if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things - $(el).height(newHeight); - } - }); -} - - -// Undoes distrubuteHeight, restoring all els to their natural height -function undistributeHeight(els) { - els.height(''); -} - - -// Given `els`, a jQuery set of <td> cells, find the cell with the largest natural width and set the widths of all the -// cells to be that width. -// PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline -function matchCellWidths(els) { - var maxInnerWidth = 0; - - els.find('> *').each(function(i, innerEl) { - var innerWidth = $(innerEl).outerWidth(); - if (innerWidth > maxInnerWidth) { - maxInnerWidth = innerWidth; - } - }); - - maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance - - els.width(maxInnerWidth); - - return maxInnerWidth; -} - - -// Given one element that resides inside another, -// Subtracts the height of the inner element from the outer element. -function subtractInnerElHeight(outerEl, innerEl) { - var both = outerEl.add(innerEl); - var diff; - - // effin' IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked - both.css({ - position: 'relative', // cause a reflow, which will force fresh dimension recalculation - left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll - }); - diff = outerEl.outerHeight() - innerEl.outerHeight(); // grab the dimensions - both.css({ position: '', left: '' }); // undo hack - - return diff; -} - - -/* Element Geom Utilities -----------------------------------------------------------------------------------------------------------------------*/ - -FC.getOuterRect = getOuterRect; -FC.getClientRect = getClientRect; -FC.getContentRect = getContentRect; -FC.getScrollbarWidths = getScrollbarWidths; - - -// borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51 -function getScrollParent(el) { - var position = el.css('position'), - scrollParent = el.parents().filter(function() { - var parent = $(this); - return (/(auto|scroll)/).test( - parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x') - ); - }).eq(0); - - return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent; -} - - -// Queries the outer bounding area of a jQuery element. -// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive). -// Origin is optional. -function getOuterRect(el, origin) { - var offset = el.offset(); - var left = offset.left - (origin ? origin.left : 0); - var top = offset.top - (origin ? origin.top : 0); - - return { - left: left, - right: left + el.outerWidth(), - top: top, - bottom: top + el.outerHeight() - }; -} - - -// Queries the area within the margin/border/scrollbars of a jQuery element. Does not go within the padding. -// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive). -// Origin is optional. -// NOTE: should use clientLeft/clientTop, but very unreliable cross-browser. -function getClientRect(el, origin) { - var offset = el.offset(); - var scrollbarWidths = getScrollbarWidths(el); - var left = offset.left + getCssFloat(el, 'border-left-width') + scrollbarWidths.left - (origin ? origin.left : 0); - var top = offset.top + getCssFloat(el, 'border-top-width') + scrollbarWidths.top - (origin ? origin.top : 0); - - return { - left: left, - right: left + el[0].clientWidth, // clientWidth includes padding but NOT scrollbars - top: top, - bottom: top + el[0].clientHeight // clientHeight includes padding but NOT scrollbars - }; -} - - -// Queries the area within the margin/border/padding of a jQuery element. Assumed not to have scrollbars. -// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive). -// Origin is optional. -function getContentRect(el, origin) { - var offset = el.offset(); // just outside of border, margin not included - var left = offset.left + getCssFloat(el, 'border-left-width') + getCssFloat(el, 'padding-left') - - (origin ? origin.left : 0); - var top = offset.top + getCssFloat(el, 'border-top-width') + getCssFloat(el, 'padding-top') - - (origin ? origin.top : 0); - - return { - left: left, - right: left + el.width(), - top: top, - bottom: top + el.height() - }; -} - - -// Returns the computed left/right/top/bottom scrollbar widths for the given jQuery element. -// NOTE: should use clientLeft/clientTop, but very unreliable cross-browser. -function getScrollbarWidths(el) { - var leftRightWidth = el.innerWidth() - el[0].clientWidth; // the paddings cancel out, leaving the scrollbars - var widths = { - left: 0, - right: 0, - top: 0, - bottom: el.innerHeight() - el[0].clientHeight // the paddings cancel out, leaving the bottom scrollbar - }; - - if (getIsLeftRtlScrollbars() && el.css('direction') == 'rtl') { // is the scrollbar on the left side? - widths.left = leftRightWidth; - } - else { - widths.right = leftRightWidth; - } - - return widths; -} - - -// Logic for determining if, when the element is right-to-left, the scrollbar appears on the left side - -var _isLeftRtlScrollbars = null; - -function getIsLeftRtlScrollbars() { // responsible for caching the computation - if (_isLeftRtlScrollbars === null) { - _isLeftRtlScrollbars = computeIsLeftRtlScrollbars(); - } - return _isLeftRtlScrollbars; -} - -function computeIsLeftRtlScrollbars() { // creates an offscreen test element, then removes it - var el = $('<div><div/></div>') - .css({ - position: 'absolute', - top: -1000, - left: 0, - border: 0, - padding: 0, - overflow: 'scroll', - direction: 'rtl' - }) - .appendTo('body'); - var innerEl = el.children(); - var res = innerEl.offset().left > el.offset().left; // is the inner div shifted to accommodate a left scrollbar? - el.remove(); - return res; -} - - -// Retrieves a jQuery element's computed CSS value as a floating-point number. -// If the queried value is non-numeric (ex: IE can return "medium" for border width), will just return zero. -function getCssFloat(el, prop) { - return parseFloat(el.css(prop)) || 0; -} - - -/* Mouse / Touch Utilities -----------------------------------------------------------------------------------------------------------------------*/ - -FC.preventDefault = preventDefault; - - -// Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac) -function isPrimaryMouseButton(ev) { - return ev.which == 1 && !ev.ctrlKey; -} - - -function getEvX(ev) { - if (ev.pageX !== undefined) { - return ev.pageX; - } - var touches = ev.originalEvent.touches; - if (touches) { - return touches[0].pageX; - } -} - - -function getEvY(ev) { - if (ev.pageY !== undefined) { - return ev.pageY; - } - var touches = ev.originalEvent.touches; - if (touches) { - return touches[0].pageY; - } -} - - -function getEvIsTouch(ev) { - return /^touch/.test(ev.type); -} - - -function preventSelection(el) { - el.addClass('fc-unselectable') - .on('selectstart', preventDefault); -} - - -// Stops a mouse/touch event from doing it's native browser action -function preventDefault(ev) { - ev.preventDefault(); -} - - -// attach a handler to get called when ANY scroll action happens on the page. -// this was impossible to do with normal on/off because 'scroll' doesn't bubble. -// http://stackoverflow.com/a/32954565/96342 -// returns `true` on success. -function bindAnyScroll(handler) { - if (window.addEventListener) { - window.addEventListener('scroll', handler, true); // useCapture=true - return true; - } - return false; -} - - -// undoes bindAnyScroll. must pass in the original function. -// returns `true` on success. -function unbindAnyScroll(handler) { - if (window.removeEventListener) { - window.removeEventListener('scroll', handler, true); // useCapture=true - return true; - } - return false; -} - - -/* 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. Will return fresh date clones in a range. -// 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]; - } - } -} - - -function hasOwnProp(obj, name) { - return hasOwnPropMethod.call(obj, name); -} - - -// Is the given value a non-object non-function value? -function isAtomic(val) { - return /undefined|null|boolean|number|string/.test($.type(val)); -} - - -function applyAll(functions, thisObj, args) { - if ($.isFunction(functions)) { - functions = [ functions ]; - } - if (functions) { - var i; - var ret; - for (i=0; i<functions.length; i++) { - ret = functions[i].apply(thisObj, args) || ret; - } - return ret; - } -} - - -function firstDefined() { - for (var i=0; i<arguments.length; i++) { - if (arguments[i] !== undefined) { - return arguments[i]; - } - } -} - - -function htmlEscape(s) { - return (s + '').replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/'/g, ''') - .replace(/"/g, '"') - .replace(/\n/g, '<br />'); -} - - -function stripHtmlEntities(text) { - return text.replace(/&.*?;/g, ''); -} - - -// 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(';'); -} - - -// Given an object hash of HTML attribute names to values, -// generates a string that can be injected between < > in HTML -function attrsToStr(attrs) { - var parts = []; - - $.each(attrs, function(name, val) { - if (val != null) { - parts.push(name + '="' + htmlEscape(val) + '"'); - } - }); - - return parts.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; - }; -} - - -// HACK around jQuery's now A+ promises: execute callback synchronously if already resolved. -// thenFunc shouldn't accept args. -// similar to whenResources in Scheduler plugin. -function syncThen(promise, thenFunc) { - // not a promise, or an already-resolved promise? - if (!promise || !promise.then || promise.state() === 'resolved') { - return $.when(thenFunc()); // resolve immediately - } - else if (thenFunc) { - return promise.then(thenFunc); - } -} - -;; - -/* -GENERAL NOTE on moments throughout the *entire rest* of the codebase: -All moments are assumed to be ambiguously-zoned unless otherwise noted, -with the NOTABLE EXCEOPTION of start/end dates that live on *Event Objects*. -Ambiguously-TIMED moments are assumed to be ambiguously-zoned by nature. -*/ - -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 - -// tell momentjs to transfer these properties upon clone -var momentProperties = moment.momentProperties; -momentProperties.push('_fullCalendar'); -momentProperties.push('_ambigTime'); -momentProperties.push('_ambigZone'); - - -// 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) || isNativeDate(input) || input === undefined) { - mom = moment.apply(null, args); - } - else { // "parsing" is required - isAmbigTime = false; - isAmbigZone = false; - - if (isSingleString) { - if (ambigDateOfMonthRegex.test(input)) { - // accept strings like '2014-05', but convert to the first of the month - input += '-01'; - args = [ input ]; // for when we pass it on to moment's constructor - isAmbigTime = true; - isAmbigZone = true; - } - else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) { - isAmbigTime = !ambigMatch[5]; // no time part? - isAmbigZone = true; - } - } - else if ($.isArray(input)) { - // arrays have no timezone information, so assume ambiguous zone - isAmbigZone = true; - } - // otherwise, probably a string with a format - - if (parseAsUTC || isAmbigTime) { - mom = moment.utc.apply(moment, args); - } - else { - mom = moment.apply(null, args); - } - - if (isAmbigTime) { - mom._ambigTime = true; - mom._ambigZone = true; // ambiguous time always means ambiguous zone - } - else if (parseZone) { // let's record the inputted zone somehow - if (isAmbigZone) { - mom._ambigZone = true; - } - else if (isSingleString) { - mom.utcOffset(input); // if not a valid zone, will assign UTC - } - } - } - - mom._fullCalendar = true; // flag for extended functionality - - 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._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() { - - if (!this._ambigTime) { - - this.utc(true); // keepLocalTime=true (for keeping *date* value) - - // set time to zero - this.set({ - hours: 0, - minutes: 0, - seconds: 0, - ms: 0 - }); - - // Mark the time as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(), - // which clears all ambig flags. - this._ambigTime = true; - this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset - } - - return this; // for chaining -}; - -// Returns if the moment has a non-ambiguous time (boolean) -newMomentProto.hasTime = function() { - return !this._ambigTime; -}; - - -// Timezone -// ------------------------------------------------------------------------------------------------- - -// Converts the moment to UTC, stripping out its timezone offset, but preserving its -// YMD and time-of-day. A moment with a stripped timezone offset will display no -// timezone offset when .format() is called. -newMomentProto.stripZone = function() { - var wasAmbigTime; - - if (!this._ambigZone) { - - wasAmbigTime = this._ambigTime; - - this.utc(true); // keepLocalTime=true (for keeping date and time values) - - // 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. - this._ambigZone = true; - } - - return this; // for chaining -}; - -// Returns of the moment has a non-ambiguous timezone offset (boolean) -newMomentProto.hasZone = function() { - return !this._ambigZone; -}; - - -// implicitly marks a zone -newMomentProto.local = function(keepLocalTime) { - - // for when converting from ambiguously-zoned to local, - // keep the time values when converting from UTC -> local - oldMomentProto.local.call(this, this._ambigZone || keepLocalTime); - - // ensure non-ambiguous - // this probably already happened via local() -> utcOffset(), but don't rely on Moment's internals - this._ambigTime = false; - this._ambigZone = false; - - return this; // for chaining -}; - - -// implicitly marks a zone -newMomentProto.utc = function(keepLocalTime) { - - oldMomentProto.utc.call(this, keepLocalTime); - - // 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; -}; - - -// implicitly marks a zone (will probably get called upon .utc() and .local()) -newMomentProto.utcOffset = 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.utcOffset.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); -}; - -;; - -// Single Date Formatting -// ------------------------------------------------------------------------------------------------- - - -// call this if you want Moment's original format method to be used -function oldMomentFormat(mom, formatStr) { - return oldMomentProto.format.call(mom, formatStr); // oldMomentProto defined in moment-ext.js -} - - -// Formats `date` with a Moment formatting string, but allow our non-zero areas and -// additional token. -function formatDate(date, formatStr) { - return formatDateWithChunks(date, getFormatStringChunks(formatStr)); -} - - -function formatDateWithChunks(date, chunks) { - var s = ''; - var i; - - for (i=0; i<chunks.length; i++) { - s += formatDateWithChunk(date, chunks[i]); - } - - return s; -} - - -// addition formatting tokens we want recognized -var tokenOverrides = { - t: function(date) { // "a" or "p" - return oldMomentFormat(date, 'a').charAt(0); - }, - T: function(date) { // "A" or "P" - return oldMomentFormat(date, 'A').charAt(0); - } -}; - - -function formatDateWithChunk(date, chunk) { - var token; - var maybeStr; - - if (typeof chunk === 'string') { // a literal string - return chunk; - } - else if ((token = chunk.token)) { // a token, like "YYYY" - if (tokenOverrides[token]) { - return tokenOverrides[token](date); // use our custom token - } - return oldMomentFormat(date, token); - } - else if (chunk.maybe) { // a grouping of other chunks that must be non-zero - maybeStr = formatDateWithChunks(date, chunk.maybe); - if (maybeStr.match(/[1-9]/)) { - return maybeStr; - } - } - - return ''; -} - - -// Date Range Formatting -// ------------------------------------------------------------------------------------------------- -// TODO: make it work with timezone offset - -// Using a formatting string meant for a single date, generate a range string, like -// "Sep 2 - 9 2013", that intelligently inserts a separator where the dates differ. -// If the dates are the same as far as the format string is concerned, just return a single -// rendering of one date, without any separator. -function formatRange(date1, date2, formatStr, separator, isRTL) { - var localeData; - - date1 = FC.moment.parseZone(date1); - date2 = FC.moment.parseZone(date2); - - localeData = date1.localeData(); - - // Expand localized format strings, like "LL" -> "MMMM D YYYY" - formatStr = localeData.longDateFormat(formatStr) || formatStr; - // BTW, this is not important for `formatDate` because it is impossible to put custom tokens - // or non-zero areas in Moment's localized format strings. - - separator = separator || ' - '; - - return formatRangeWithChunks( - date1, - date2, - getFormatStringChunks(formatStr), - separator, - isRTL - ); -} -FC.formatRange = formatRange; // expose - - -function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) { - var unzonedDate1 = date1.clone().stripZone(); // for formatSimilarChunk - var unzonedDate2 = date2.clone().stripZone(); // " - var chunkStr; // the rendering of the chunk - var leftI; - var leftStr = ''; - var rightI; - var rightStr = ''; - var middleI; - var middleStr1 = ''; - var middleStr2 = ''; - var middleStr = ''; - - // Start at the leftmost side of the formatting string and continue until you hit a token - // that is not the same between dates. - for (leftI=0; leftI<chunks.length; leftI++) { - chunkStr = formatSimilarChunk(date1, date2, unzonedDate1, unzonedDate2, chunks[leftI]); - if (chunkStr === false) { - break; - } - leftStr += chunkStr; - } - - // Similarly, start at the rightmost side of the formatting string and move left - for (rightI=chunks.length-1; rightI>leftI; rightI--) { - chunkStr = formatSimilarChunk(date1, date2, unzonedDate1, unzonedDate2, chunks[rightI]); - if (chunkStr === false) { - break; - } - rightStr = chunkStr + rightStr; - } - - // The area in the middle is different for both of the dates. - // Collect them distinctly so we can jam them together later. - for (middleI=leftI; middleI<=rightI; middleI++) { - middleStr1 += formatDateWithChunk(date1, chunks[middleI]); - middleStr2 += formatDateWithChunk(date2, chunks[middleI]); - } - - if (middleStr1 || middleStr2) { - if (isRTL) { - middleStr = middleStr2 + separator + middleStr1; - } - else { - middleStr = middleStr1 + separator + middleStr2; - } - } - - return leftStr + middleStr + rightStr; -} - - -var similarUnitMap = { - Y: 'year', - M: 'month', - D: 'day', // day of month - d: 'day', // day of week - // prevents a separator between anything time-related... - A: 'second', // AM/PM - a: 'second', // am/pm - T: 'second', // A/P - t: 'second', // a/p - H: 'second', // hour (24) - h: 'second', // hour (12) - m: 'second', // minute - s: 'second' // second -}; -// TODO: week maybe? - - -// Given a formatting chunk, and given that both dates are similar in the regard the -// formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`. -function formatSimilarChunk(date1, date2, unzonedDate1, unzonedDate2, chunk) { - var token; - var unit; - - if (typeof chunk === 'string') { // a literal string - return chunk; - } - else if ((token = chunk.token)) { - unit = similarUnitMap[token.charAt(0)]; - - // are the dates the same for this unit of measurement? - // use the unzoned dates for this calculation because unreliable when near DST (bug #2396) - if (unit && unzonedDate1.isSame(unzonedDate2, unit)) { - return oldMomentFormat(date1, token); // would be the same if we used `date2` - // BTW, don't support custom tokens - } - } - - return false; // the chunk is NOT the same for the two dates - // BTW, don't support splitting on non-zero areas -} - - -// Chunking Utils -// ------------------------------------------------------------------------------------------------- - - -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; -} - - -// Misc Utils -// ------------------------------------------------------------------------------------------------- - - -// granularity only goes up until day -// TODO: unify with similarUnitMap -var tokenGranularities = { - Y: { value: 1, unit: 'year' }, - M: { value: 2, unit: 'month' }, - W: { value: 3, unit: 'week' }, - w: { value: 3, unit: 'week' }, - D: { value: 4, unit: 'day' }, // day of month - d: { value: 4, unit: 'day' } // day of week -}; - -// returns a unit string, either 'year', 'month', 'day', or null -// for the most granular formatting token in the string. -FC.queryMostGranularFormatUnit = function(formatStr) { - var chunks = getFormatStringChunks(formatStr); - var i, chunk; - var candidate; - var best; - - for (i = 0; i < chunks.length; i++) { - chunk = chunks[i]; - if (chunk.token) { - candidate = tokenGranularities[chunk.token.charAt(0)]; - if (candidate) { - if (!best || candidate.value > best.value) { - best = candidate; - } - } - } - } - - if (best) { - return best.unit; - } - - return null; -}; - -;; - -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); - - // copy each member variable/method onto the the subclass's prototype - copyOwnProps(members, subClass.prototype); - - // copy over all class variables/methods to the subclass, such as `extend` and `mixin` - copyOwnProps(superClass, subClass); - - return subClass; -} - - -function mixIntoClass(theClass, members) { - copyOwnProps(members, theClass.prototype); -} -;; - -var EmitterMixin = FC.EmitterMixin = { - - // jQuery-ification via $(this) allows a non-DOM object to have - // the same event handling capabilities (including namespaces). - - - on: function(types, handler) { - - // handlers are always called with an "event" object as their first param. - // sneak the `this` context and arguments into the extra parameter object - // and forward them on to the original handler. - var intercept = function(ev, extra) { - return handler.apply( - extra.context || this, - extra.args || [] - ); - }; - - // mimick jQuery's internal "proxy" system (risky, I know) - // causing all functions with the same .guid to appear to be the same. - // https://github.com/jquery/jquery/blob/2.2.4/src/core.js#L448 - // this is needed for calling .off with the original non-intercept handler. - if (!handler.guid) { - handler.guid = $.guid++; - } - intercept.guid = handler.guid; - - $(this).on(types, intercept); - - return this; // for chaining - }, - - - off: function(types, handler) { - $(this).off(types, handler); - - return this; // for chaining - }, - - - trigger: function(types) { - var args = Array.prototype.slice.call(arguments, 1); // arguments after the first - - // pass in "extra" info to the intercept - $(this).triggerHandler(types, { args: args }); - - return this; // for chaining - }, - - - triggerWith: function(types, context, args) { - - // `triggerHandler` is less reliant on the DOM compared to `trigger`. - // pass in "extra" info to the intercept. - $(this).triggerHandler(types, { context: context, args: args }); - - return this; // for chaining - } - -}; - -;; - -/* -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; -})(); -;; - -// simple class for toggle a `isIgnoringMouse` flag on delay -// initMouseIgnoring must first be called, with a millisecond delay setting. -var MouseIgnorerMixin = { - - isIgnoringMouse: false, // bool - delayUnignoreMouse: null, // method - - - initMouseIgnoring: function(delay) { - this.delayUnignoreMouse = debounce(proxy(this, 'unignoreMouse'), delay || 1000); - }, - - - // temporarily ignore mouse actions on segments - tempIgnoreMouse: function() { - this.isIgnoringMouse = true; - this.delayUnignoreMouse(); - }, - - - // delayUnignoreMouse eventually calls this - unignoreMouse: function() { - this.isIgnoringMouse = false; - } - -}; - -;; - -/* 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 = $('<div class="fc-popover"/>') - .addClass(options.className || '') - .css({ - // position initially to the top left to avoid creating scrollbars - top: 0, - left: 0 - }) - .append(options.content) - .appendTo(options.parentEl); - - // when a click happens on anything inside with a 'fc-close' className, hide the popover - this.el.on('click', '.fc-close', function() { - _this.hide(); - }); - - if (options.autoHide) { - 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(); - } - }, - - - // 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, returns undefined. - getHorizontalIndex: function(leftOffset) { - this.ensureBuilt(); - - var lefts = this.lefts; - var rights = this.rights; - var len = lefts.length; - var i; - - 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, returns undefined. - getVerticalIndex: function(topOffset) { - this.ensureBuilt(); - - var tops = this.tops; - var bottoms = this.bottoms; - var len = tops.length; - var i; - - 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]; - }, - - - // Bounding Rect - // TODO: decouple this from CoordCache - - // 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); - } - }, - - isPointInBounds: function(leftOffset, topOffset) { - return this.isLeftInBounds(leftOffset) && this.isTopInBounds(topOffset); - }, - - isLeftInBounds: function(leftOffset) { - return !this.boundingRect || (leftOffset >= this.boundingRect.left && leftOffset < this.boundingRect.right); - }, - - isTopInBounds: function(topOffset) { - return !this.boundingRect || (topOffset >= this.boundingRect.top && topOffset < this.boundingRect.bottom); - } - -}); - -;; - -/* Tracks a drag's mouse movement, firing various handlers -----------------------------------------------------------------------------------------------------------------------*/ -// TODO: use Emitter - -var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMixin, { - - options: null, - subjectEl: null, - - // coordinates of the initial mousedown - originX: null, - originY: null, - - // the wrapping element that scrolls, or MIGHT scroll if there's overflow. - // TODO: do this for wrappers that have overflow:hidden as well. - scrollEl: null, - - isInteracting: false, - isDistanceSurpassed: false, - isDelayEnded: false, - isDragging: false, - isTouch: false, - - delay: null, - delayTimeoutId: null, - minDistance: null, - - handleTouchScrollProxy: null, // calls handleTouchScroll, always bound to `this` - - - constructor: function(options) { - this.options = options || {}; - this.handleTouchScrollProxy = proxy(this, 'handleTouchScroll'); - this.initMouseIgnoring(500); - }, - - - // Interaction (high-level) - // ----------------------------------------------------------------------------------------------------------------- - - - startInteraction: function(ev, extraOptions) { - var isTouch = getEvIsTouch(ev); - - if (ev.type === 'mousedown') { - if (this.isIgnoringMouse) { - return; - } - else 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, isCancelled) { - 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, isCancelled); - - // a touchstart+touchend on the same element will result in the following addition simulated events: - // mouseover + mouseout + click - // let's ignore these bogus events - if (this.isTouch) { - this.tempIgnoreMouse(); - } - } - }, - - - handleInteractionEnd: function(ev, isCancelled) { - this.trigger('interactionEnd', ev, isCancelled || false); - }, - - - // 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, true); // isCancelled=true - } - } - }); - - // listen to ALL scroll actions on the page - if ( - !bindAnyScroll(this.handleTouchScrollProxy) && // hopefully this works and short-circuits the rest - this.scrollEl // otherwise, attach a single handler to this - ) { - this.listenTo(this.scrollEl, 'scroll', this.handleTouchScroll); - } - } - 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 - }); - }, - - - unbindHandlers: function() { - this.stopListeningTo($(document)); - - // unbind scroll listening - unbindAnyScroll(this.handleTouchScrollProxy); - if (this.scrollEl) { - this.stopListeningTo(this.scrollEl, 'scroll'); - } - }, - - - // 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); - }, - - - 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 - - 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); - } - }, - - - // 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 - }, - - - endDrag: function(ev) { - if (this.isDragging) { - this.isDragging = false; - this.handleDragEnd(ev); - } - }, - - - handleDragEnd: function(ev) { - this.trigger('dragEnd', ev); - }, - - - // Delay - // ----------------------------------------------------------------------------------------------------------------- - - - startDelay: function(initialEv) { - var _this = this; - - if (this.delay) { - this.delayTimeoutId = setTimeout(function() { - _this.handleDelayEnd(initialEv); - }, this.delay); - } - else { - this.handleDelayEnd(initialEv); - } - }, - - - handleDelayEnd: function(initialEv) { - this.isDelayEnded = true; - - if (this.isDistanceSurpassed) { - this.startDrag(initialEv); - } - }, - - - // Distance - // ----------------------------------------------------------------------------------------------------------------- - - - handleDistanceSurpassed: function(ev) { - this.isDistanceSurpassed = true; - - if (this.isDelayEnded) { - this.startDrag(ev); - } - }, - - - // Mouse / Touch - // ----------------------------------------------------------------------------------------------------------------- - - - handleTouchMove: function(ev) { - // prevent inertia and touchmove-scrolling while dragging - if (this.isDragging) { - ev.preventDefault(); - } - - this.handleMove(ev); - }, - - - handleMouseMove: function(ev) { - this.handleMove(ev); - }, - - - // Scrolling (unrelated to auto-scroll) - // ----------------------------------------------------------------------------------------------------------------- - - - 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, true); // isCancelled=true - } - }, - - - // Utils - // ----------------------------------------------------------------------------------------------------------------- - - - // 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; - - 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; - } - - // call the super-method. do it after origHit has been computed - DragListener.prototype.handleInteractionStart.apply(this, arguments); - }, - - - // 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)); - - // report the initial hit the mouse is over - // especially important if no min-distance and drag starts immediately - if (hit) { - this.handleHitOver(hit); - } - }, - - - // Called when the drag moves - handleDrag: function(dx, dy, ev) { - var hit; - - DragListener.prototype.handleDrag.apply(this, arguments); // call the super-method - - 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); - } - } - }, - - - // Called when dragging has been stopped - handleDragEnd: function() { - this.handleHitDone(); - DragListener.prototype.handleDragEnd.apply(this, arguments); // call the super-method - }, - - - // Called when a the mouse has just moved over a new hit - handleHitOver: function(hit) { - var isOrig = isHitsEqual(hit, this.origHit); - - this.hit = hit; - - 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() { // might be called by .animate(), which might change `this` context - _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) { - 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) { - 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(); - } - } - -}); - -;; - -/* An abstract class comprised of a "grid" of areas that each represent a specific datetime -----------------------------------------------------------------------------------------------------------------------*/ - -var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, { - - // self-config, overridable by subclasses - hasDayInteractions: true, // can user click/select ranges of time? - - view: null, // a View object - isRTL: null, // shortcut to the view's isRTL option - - start: null, - end: null, - - 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, - - 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, - - dayDragListener: null, - segDragListener: null, - segResizeListener: null, - externalDragListener: null, - - - constructor: function(view) { - this.view = view; - this.isRTL = view.opt('isRTL'); - this.elsByFill = {}; - - this.dayDragListener = this.buildDayDragListener(); - this.initMouseIgnoring(); - }, - - - /* Options - ------------------------------------------------------------------------------------------------------------------*/ - - - // Generates the format string used for event time text, if not explicitly defined by 'timeFormat' - computeEventTimeFormat: function() { - return this.view.opt('smallTimeFormat'); - }, - - - // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventTime'. - // Only applies to non-all-day events. - computeDisplayEventTime: function() { - return true; - }, - - - // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd' - computeDisplayEventEnd: function() { - return true; - }, - - - /* Dates - ------------------------------------------------------------------------------------------------------------------*/ - - - // 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 when internal variables that rely on the range should be updated - rangeUpdated: function() { - }, - - - // 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(); - - displayEventTime = view.opt('displayEventTime'); - if (displayEventTime == null) { - displayEventTime = this.computeDisplayEventTime(); // might be based off of range - } - - displayEventEnd = view.opt('displayEventEnd'); - if (displayEventEnd == null) { - displayEventEnd = this.computeDisplayEventEnd(); // might be based off of range - } - - this.displayEventTime = displayEventTime; - this.displayEventEnd = displayEventEnd; - }, - - - // 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 - }, - - - // 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); - } - }, - - - /* Hit Area - ------------------------------------------------------------------------------------------------------------------*/ - - - // Called before one or more queryHit calls might happen. Should prepare any cached coordinates for queryHit - prepareHits: function() { - }, - - - // Called when queryHit calls have subsided. Good place to clear any coordinate caches. - releaseHits: function() { - }, - - - // 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) { - }, - - - // 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) { - }, - - - // 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) { - }, - - - /* Rendering - ------------------------------------------------------------------------------------------------------------------*/ - - - // Sets the container element that the grid should render inside of. - // Does other DOM-related initializations. - setElement: function(el) { - this.el = el; - - if (this.hasDayInteractions) { - preventSelection(el); - - this.bindDayHandler('touchstart', this.dayTouchStart); - this.bindDayHandler('mousedown', this.dayMousedown); - } - - // attach event-element-related handlers. in Grid.events - // same garbage collection note as above. - this.bindSegHandlers(); - - this.bindGlobalHandlers(); - }, - - - bindDayHandler: function(name, handler) { - var _this = this; - - // 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( - _this.segSelector + ',' + // directly on an event element - _this.segSelector + ' *,' + // within an event element - '.fc-more,' + // a "more.." link - 'a[data-goto]' // a clickable nav link - ) - ) { - return handler.call(_this, ev); - } - }); - }, - - - // 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) { - if (!this.isIgnoringMouse) { - this.dayDragListener.startInteraction(ev, { - //distance: 5, // needs more work if we want dayClick to fire correctly - }); - } - }, - - - dayTouchStart: function(ev) { - var view = this.view; - - // HACK to prevent a user's clickaway for unselecting a range or an event - // from causing a dayClick. - if (view.isSelected || view.selectedEvent) { - this.tempIgnoreMouse(); - } - - this.dayDragListener.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 = new HitDragListener(this, { - scroll: view.opt('dragScroll'), - interactionStart: function() { - dayClickHit = dragListener.origHit; // for dayClick, where no dragging happens - selectionSpan = null; - }, - 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() { // called before mouse moves to a different hit OR moved out of all hits - dayClickHit = null; - selectionSpan = null; - _this.unrenderSelection(); - }, - hitDone: function() { // called after a hitOut OR before a dragEnd - enableCursor(); - }, - interactionEnd: function(ev, isCancelled) { - if (!isCancelled) { - if ( - dayClickHit && - !_this.isIgnoringMouse // see hack in dayTouchStart - ) { - 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); - } - } - } - }); - - 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() { - this.dayDragListener.endInteraction(); - - 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; - } - - return fakeEvent; - }, - - - // Renders a mock event. Given zoned event date properties. - // Must return all mock event elements. - renderHelper: function(eventLocation, sourceSeg) { - // subclasses must implement - }, - - - // Unrenders a mock event - unrenderHelper: function() { - // subclasses must implement - }, - - - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - // 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); - }, - - - // Unrenders any visual indications of a selection. Will unrender a highlight by default. - unrenderSelection: function() { - this.unrenderHighlight(); - }, - - - // 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; - } - - return span; - }, - - - // 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 - - return { start: dates[0].clone(), end: dates[3].clone() }; - }, - - - /* Highlight - ------------------------------------------------------------------------------------------------------------------*/ - - - // 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)); - }, - - - // Unrenders the emphasis on a date range - unrenderHighlight: function() { - this.unrenderFill('highlight'); - }, - - - // Generates an array of classNames for rendering the highlight. Used by the fill system. - highlightSegClasses: function() { - return [ 'fc-highlight' ]; - }, - - - /* Business Hours - ------------------------------------------------------------------------------------------------------------------*/ - - - renderBusinessHours: function() { - }, - - - unrenderBusinessHours: function() { - }, - - - /* Now Indicator - ------------------------------------------------------------------------------------------------------------------*/ - - - getNowIndicatorUnit: function() { - }, - - - renderNowIndicator: function(date) { - }, - - - unrenderNowIndicator: function() { - }, - - - /* Fill System (highlight, background events, business hours) - -------------------------------------------------------------------------------------------------------------------- - TODO: remove this system. like we did in TimeGrid - */ - - - // 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 - }, - - - // 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]; - } - }, - - - // 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) { - - // 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); - - // allow custom filter methods per-type - if (segElMethod) { - el = segElMethod.call(_this, seg, el); - } - - 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); - } - } - }); - } - - return renderedSegs; - }, - - - fillSegTag: 'div', // subclasses can override - - - // Builds the HTML needed for one fill segment. Generic enough to work with different types. - fillSegHtml: function(type, seg) { - - // 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 + '"' : '') + - ' />'; - }, - - - - /* Generic rendering utilities for subclasses - ------------------------------------------------------------------------------------------------------------------*/ - - - // 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; - } - -}); - -;; - -/* Event-rendering and event-interaction methods for the abstract Grid class -----------------------------------------------------------------------------------------------------------------------*/ - -Grid.mixin({ - - // self-config, overridable by subclasses - segSelector: '.fc-event-container > *', // what constitutes an event element? - - 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` - - - // 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]); - } - - this.segs = [].concat( // record all segs - this.renderBgEvents(bgEvents), - this.renderFgEvents(fgEvents) - ); - }, - - - 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; - }, - - - 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; - }, - - - // 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(); - - this.unrenderFgSegs(); - this.unrenderBgSegs(); - - this.segs = null; - }, - - - // Retrieves all rendered segment objects currently rendered on the grid - getEventSegs: function() { - return this.segs || []; - }, - - - /* Foreground Segment Rendering - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders foreground event segments onto the grid. May return a subset of segs that were rendered. - renderFgSegs: function(segs) { - // subclasses must implement - }, - - - // Unrenders all currently rendered foreground segments - unrenderFgSegs: function() { - // subclasses must implement - }, - - - // 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 - - // 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)); - - if (el) { - el.data('fc-seg', seg); // used by handlers - seg.el = el; - renderedSegs.push(seg); - } - }); - } - - return renderedSegs; - }, - - - // Generates the HTML for the default rendering of a foreground event segment. Used by renderFgSegEls() - fgSegHtml: function(seg, disableResizing) { - // subclasses should implement - }, - - - /* Background Segment Rendering - ------------------------------------------------------------------------------------------------------------------*/ - - - // 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); - }, - - - // Unrenders all the currently rendered background event segments - unrenderBgSegs: function() { - this.unrenderFill('bgEvent'); - }, - - - // 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 - }, - - - // Generates an array of classNames to be used for the default rendering of a background event. - // Called by fillSegHtml. - bgEventSegClasses: function(seg) { - var event = seg.event; - var source = event.source || {}; - - return [ 'fc-bgevent' ].concat( - event.className, - source.className || [] - ); - }, - - - // Generates a semicolon-separated CSS string to be used for the default rendering of a background event. - // Called by fillSegHtml. - bgEventSegCss: function(seg) { - return { - 'background-color': this.getSegSkinCss(seg)['background-color'] - }; - }, - - - // Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system. - // Called by fillSegHtml. - businessHoursSegClasses: function(seg) { - return [ 'fc-nonbusiness', 'fc-bgevent' ]; - }, - - - /* Business Hours - ------------------------------------------------------------------------------------------------------------------*/ - - - // Compute business hour segs for the grid's current date range. - // Caller must ask if whole-day business hours are needed. - buildBusinessHourSegs: function(wholeDay) { - var events = this.view.calendar.getCurrentBusinessHourEvents(wholeDay); - - // HACK. Eventually refactor business hours "events" system. - // If no events are given, but businessHours is activated, this means the entire visible range should be - // marked as *not* business-hours, via inverse-background rendering. - if ( - !events.length && - this.view.calendar.options.businessHours // don't access view option. doesn't update with dynamic options - ) { - events = [ - $.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS, { - start: this.view.end, // guaranteed out-of-range - end: this.view.end, // " - dow: null - }) - ]; - } - - return this.eventsToSegs(events); - }, - - - /* Handlers - ------------------------------------------------------------------------------------------------------------------*/ - - - // Attaches event-element-related handlers for *all* rendered event segments of the view. - bindSegHandlers: function() { - this.bindSegHandlersToEl(this.el); - }, - - - // Attaches event-element-related handlers to an arbitrary container element. leverages bubbling. - bindSegHandlersToEl: function(el) { - this.bindSegHandlerToEl(el, 'touchstart', this.handleSegTouchStart); - this.bindSegHandlerToEl(el, 'touchend', this.handleSegTouchEnd); - this.bindSegHandlerToEl(el, 'mouseenter', this.handleSegMouseover); - this.bindSegHandlerToEl(el, 'mouseleave', this.handleSegMouseout); - this.bindSegHandlerToEl(el, 'mousedown', this.handleSegMousedown); - this.bindSegHandlerToEl(el, 'click', this.handleSegClick); - }, - - - // 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 - bindSegHandlerToEl: function(el, name, handler) { - var _this = this; - - el.on(name, this.segSelector, 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 - } - }); - }, - - - handleSegClick: function(seg, ev) { - var res = this.view.trigger('eventClick', seg.el[0], seg.event, ev); // can return `false` to cancel - if (res === false) { - ev.preventDefault(); - } - }, - - - // Updates internal state and triggers handlers for when an event element is moused over - handleSegMouseover: function(seg, ev) { - if ( - !this.isIgnoringMouse && - !this.mousedOverSeg - ) { - this.mousedOverSeg = seg; - if (this.view.isEventResizable(seg.event)) { - seg.el.addClass('fc-allow-mouse-resize'); - } - 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; - if (this.view.isEventResizable(seg.event)) { - seg.el.removeClass('fc-allow-mouse-resize'); - } - this.view.trigger('eventMouseout', seg.el[0], seg.event, ev); - } - }, - - - handleSegMousedown: function(seg, ev) { - var isResizing = this.startSegResize(seg, ev, { distance: 5 }); - - if (!isResizing && this.view.isEventDraggable(seg.event)) { - this.buildSegDragListener(seg) - .startInteraction(ev, { - distance: 5 - }); - } - }, - - - 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? - - dragListener = isDraggable ? - this.buildSegDragListener(seg) : - this.buildSegSelectListener(seg); // seg isn't draggable, but still needs to be selected - - dragListener.startInteraction(ev, { // won't start if already started - delay: isSelected ? 0 : this.view.opt('longPressDelay') // do delay if not already selected - }); - } - - // a long tap simulates a mouseover. ignore this bogus mouseover. - this.tempIgnoreMouse(); - }, - - - handleSegTouchEnd: function(seg, ev) { - // touchstart+touchend = click, which simulates a mouseover. - // ignore this bogus mouseover. - this.tempIgnoreMouse(); - }, - - - // 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.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. - // Has side effect of setting/unsetting `segDragListener` - 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 - - if (this.segDragListener) { - return this.segDragListener; - } - - // 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) { - seg.component = _this; // for renderDrag - 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) { - if (dragListener.isTouch && !view.isEventSelected(event)) { - // if not previously selected, will fire after a delay. then, select the event - view.selectEvent(event); - } - 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) { - delete seg.component; // prevent side effects - - // 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; - }, - - - // seg isn't draggable, but let's use a generic DragListener - // simply for the delay, so it can be selected. - // Has side effect of setting/unsetting `segDragListener` - buildSegSelectListener: function(seg) { - var _this = this; - var view = this.view; - var event = seg.event; - - if (this.segDragListener) { - return this.segDragListener; - } - - var dragListener = this.segDragListener = new DragListener({ - dragStart: function(ev) { - if (dragListener.isTouch && !view.isEventSelected(event)) { - // if not previously selected, will fire after a delay. then, select the event - view.selectEvent(event); - } - }, - interactionEnd: function(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 = pluckEventDateProps(event); - } - - 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.css('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? (FYI, event dates might have zones) - else if ( - resizeLocation.start.isSame(event.start.clone().stripZone()) && - resizeLocation.end.isSame(eventEnd.clone().stripZone()) - ) { - 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 classes = [ - 'fc-event', - seg.isStart ? 'fc-start' : 'fc-not-start', - seg.isEnd ? 'fc-end' : 'fc-not-end' - ].concat(this.getSegCustomClasses(seg)); - - if (isDraggable) { - classes.push('fc-draggable'); - } - if (isResizable) { - classes.push('fc-resizable'); - } - - // event is currently selected? attach a className. - if (view.isEventSelected(seg.event)) { - classes.push('fc-selected'); - } - - return classes; - }, - - - // List of classes that were defined by the caller of the API in some way - getSegCustomClasses: function(seg) { - var event = seg.event; - - return [].concat( - event.className, // guaranteed to be an array - event.source ? event.source.className : [] - ); - }, - - - // Utility for generating event skin-related CSS properties - getSegSkinCss: function(seg) { - return { - 'background-color': this.getSegBackgroundColor(seg), - 'border-color': this.getSegBorderColor(seg), - color: this.getSegTextColor(seg) - }; - }, - - - // Queries for caller-specified color, then falls back to default - getSegBackgroundColor: function(seg) { - return seg.event.backgroundColor || - seg.event.color || - this.getSegDefaultBackgroundColor(seg); - }, - - - getSegDefaultBackgroundColor: function(seg) { - var source = seg.event.source || {}; - - return source.backgroundColor || - source.color || - this.view.opt('eventBackgroundColor') || - this.view.opt('eventColor'); - }, - - - // Queries for caller-specified color, then falls back to default - getSegBorderColor: function(seg) { - return seg.event.borderColor || - seg.event.color || - this.getSegDefaultBorderColor(seg); - }, - - - getSegDefaultBorderColor: function(seg) { - var source = seg.event.source || {}; - - return source.borderColor || - source.color || - this.view.opt('eventBorderColor') || - this.view.opt('eventColor'); - }, - - - // Queries for caller-specified color, then falls back to default - getSegTextColor: function(seg) { - return seg.event.textColor || - this.getSegDefaultTextColor(seg); - }, - - - getSegDefaultTextColor: function(seg) { - var source = seg.event.source || {}; - - return source.textColor || - this.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) { - var calendar = this.view.calendar; - var start = event.start.clone().stripZone(); - var end = ( - event.end ? - event.end.clone() : - // derive the end from the start and allDay. compute allDay if necessary - calendar.getDefaultEventEnd( - event.allDay != null ? - event.allDay : - !event.start.hasTime(), - event.start - ) - ).stripZone(); - - // hack: dynamic locale change forgets to upate stored event localed - calendar.localizeMoment(start); - calendar.localizeMoment(end); - - return { start: start, end: end }; - }, - - - // 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); - } - -}); - - -/* Utilities -----------------------------------------------------------------------------------------------------------------------*/ - - -function pluckEventDateProps(event) { - return { - start: event.start.clone(), - end: event.end ? event.end.clone() : null, - allDay: event.allDay // keep it the same - }; -} -FC.pluckEventDateProps = pluckEventDateProps; - - -function isBgEvent(event) { // returns true if background OR inverse-background - var rendering = getEventRendering(event); - return rendering === 'background' || rendering === 'inverse-background'; -} -FC.isBgEvent = isBgEvent; // export - - -function isInverseBgEvent(event) { - return getEventRendering(event) === 'inverse-background'; -} - - -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 '' + - '<div class="fc-row ' + view.widgetHeaderClass + '">' + - '<table>' + - '<thead>' + - this.renderHeadTrHtml() + - '</thead>' + - '</table>' + - '</div>'; - }, - - - renderHeadIntroHtml: function() { - return this.renderIntroHtml(); // fall back to generic - }, - - - renderHeadTrHtml: function() { - return '' + - '<tr>' + - (this.isRTL ? '' : this.renderHeadIntroHtml()) + - this.renderHeadDateCellsHtml() + - (this.isRTL ? this.renderHeadIntroHtml() : '') + - '</tr>'; - }, - - - renderHeadDateCellsHtml: function() { - var htmls = []; - var col, date; - - for (col = 0; col < this.colCnt; col++) { - date = this.getCellDate(0, col); - htmls.push(this.renderHeadDateCellHtml(date)); - } - - return htmls.join(''); - }, - - - // TODO: when internalApiVersion, accept an object for HTML attributes - // (colspan should be no different) - renderHeadDateCellHtml: function(date, colspan, otherAttrs) { - var view = this.view; - - return '' + - '<th class="fc-day-header ' + view.widgetHeaderClass + ' fc-' + dayIDs[date.day()] + '"' + - (this.rowCnt === 1 ? - ' data-date="' + date.format('YYYY-MM-DD') + '"' : - '') + - (colspan > 1 ? - ' colspan="' + colspan + '"' : - '') + - (otherAttrs ? - ' ' + otherAttrs : - '') + - '>' + - // don't make a link if the heading could represent multiple days, or if there's only one day (forceOff) - view.buildGotoAnchorHtml( - { date: date, forceOff: this.rowCnt > 1 || this.colCnt === 1 }, - htmlEscape(date.format(this.colHeadFormat)) // inner HTML - ) + - '</th>'; - }, - - - /* Background Rendering - ------------------------------------------------------------------------------------------------------------------*/ - - - renderBgTrHtml: function(row) { - return '' + - '<tr>' + - (this.isRTL ? '' : this.renderBgIntroHtml(row)) + - this.renderBgCellsHtml(row) + - (this.isRTL ? this.renderBgIntroHtml(row) : '') + - '</tr>'; - }, - - - renderBgIntroHtml: function(row) { - return this.renderIntroHtml(); // fall back to generic - }, - - - renderBgCellsHtml: function(row) { - var htmls = []; - var col, date; - - for (col = 0; col < this.colCnt; col++) { - date = this.getCellDate(row, col); - htmls.push(this.renderBgCellHtml(date)); - } - - return htmls.join(''); - }, - - - renderBgCellHtml: function(date, otherAttrs) { - var view = this.view; - var classes = this.getDayClasses(date); - - classes.unshift('fc-day', view.widgetContentClass); - - return '<td class="' + classes.join(' ') + '"' + - ' data-date="' + date.format('YYYY-MM-DD') + '"' + // if date has a time, won't format it - (otherAttrs ? - ' ' + otherAttrs : - '') + - '></td>'; - }, - - - /* Generic - ------------------------------------------------------------------------------------------------------------------*/ - - - // Generates the default HTML intro for any row. User classes should override - renderIntroHtml: function() { - }, - - - // TODO: a generic method for dealing with <tr>, RTL, intro - // when increment internalApiVersion - // wrapTr (scheduler) - - - /* Utils - ------------------------------------------------------------------------------------------------------------------*/ - - - // 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(); - - 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) - ); - } - } - }, - - - unrenderDates: function() { - this.removeSegPopover(); - }, - - - renderBusinessHours: function() { - var segs = this.buildBusinessHourSegs(true); // wholeDay=true - this.renderFill('businessHours', segs, 'bgevent'); - }, - - - unrenderBusinessHours: function() { - this.unrenderFill('businessHours'); - }, - - - // 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'); - } - - return '' + - '<div class="' + classes.join(' ') + '">' + - '<div class="fc-bg">' + - '<table>' + - this.renderBgTrHtml(row) + - '</table>' + - '</div>' + - '<div class="fc-content-skeleton">' + - '<table>' + - (this.numbersVisible ? - '<thead>' + - this.renderNumberTrHtml(row) + - '</thead>' : - '' - ) + - '</table>' + - '</div>' + - '</div>'; - }, - - - /* Grid Number Rendering - ------------------------------------------------------------------------------------------------------------------*/ - - - renderNumberTrHtml: function(row) { - return '' + - '<tr>' + - (this.isRTL ? '' : this.renderNumberIntroHtml(row)) + - this.renderNumberCellsHtml(row) + - (this.isRTL ? this.renderNumberIntroHtml(row) : '') + - '</tr>'; - }, - - - renderNumberIntroHtml: function(row) { - return this.renderIntroHtml(); - }, - - - 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)); - } - - return htmls.join(''); - }, - - - // Generates the HTML for the <td>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 html = ''; - var classes; - var weekCalcFirstDoW; - - if (!this.view.dayNumbersVisible && !this.view.cellWeekNumbersVisible) { - // no numbers in day cell (week number must be along the side) - return '<td/>'; // will create an empty space above events :( - } - - classes = this.getDayClasses(date); - classes.unshift('fc-day-top'); - - if (this.view.cellWeekNumbersVisible) { - // To determine the day of week number change under ISO, we cannot - // rely on moment.js methods such as firstDayOfWeek() or weekday(), - // because they rely on the locale's dow (possibly overridden by - // our firstDay option), which may not be Monday. We cannot change - // dow, because that would affect the calendar start day as well. - if (date._locale._fullCalendar_weekCalc === 'ISO') { - weekCalcFirstDoW = 1; // Monday by ISO 8601 definition - } - else { - weekCalcFirstDoW = date._locale.firstDayOfWeek(); - } - } - - html += '<td class="' + classes.join(' ') + '" data-date="' + date.format() + '">'; - - if (this.view.cellWeekNumbersVisible && (date.day() == weekCalcFirstDoW)) { - html += this.view.buildGotoAnchorHtml( - { date: date, type: 'week' }, - { 'class': 'fc-week-number' }, - date.format('w') // inner HTML - ); - } - - if (this.view.dayNumbersVisible) { - html += this.view.buildGotoAnchorHtml( - date, - { 'class': 'fc-day-number' }, - date.date() // inner HTML - ); - } - - html += '</td>'; - - return html; - }, - - - /* Options - ------------------------------------------------------------------------------------------------------------------*/ - - - // 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 - }, - - - /* Dates - ------------------------------------------------------------------------------------------------------------------*/ - - - rangeUpdated: function() { - this.updateDayTable(); - }, - - - // 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; - }, - - - /* Hit System - ------------------------------------------------------------------------------------------------------------------*/ - - - prepareHits: function() { - this.colCoordCache.build(); - this.rowCoordCache.build(); - this.rowCoordCache.bottoms[this.rowCnt - 1] += this.bottomCoordPadding; // hack - }, - - - releaseHits: function() { - this.colCoordCache.clear(); - this.rowCoordCache.clear(); - }, - - - queryHit: function(leftOffset, topOffset) { - if (this.colCoordCache.isLeftInBounds(leftOffset) && this.rowCoordCache.isTopInBounds(topOffset)) { - var col = this.colCoordCache.getHorizontalIndex(leftOffset); - var row = this.rowCoordCache.getVerticalIndex(topOffset); - - if (row != null && col != null) { - return this.getCellHit(row, col); - } - } - }, - - - getHitSpan: function(hit) { - return this.getCellRange(hit.row, hit.col); - }, - - - getHitEl: function(hit) { - return this.getCellEl(hit.row, hit.col); - }, - - - /* Cell System - ------------------------------------------------------------------------------------------------------------------*/ - // FYI: the first column is the leftmost column, regardless of date - - - 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) - }; - }, - - - getCellEl: function(row, col) { - return this.cellEls.eq(row * this.colCnt + col); - }, - - - /* Event Drag Visualization - ------------------------------------------------------------------------------------------------------------------*/ - // TODO: move to DayGrid.event, similar to what we did with Grid's drag methods - - - // 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)); - - // if a segment from the same calendar but another component is being dragged, render a helper event - if (seg && seg.component !== this) { - return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements - } - }, - - - // Unrenders any visual indication of a hovering event - unrenderDrag: function() { - this.unrenderHighlight(); - this.unrenderHelper(); - }, - - - /* Event Resize Visualization - ------------------------------------------------------------------------------------------------------------------*/ - - - // 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 - }, - - - // Unrenders a visual indication of an event being resized - unrenderEventResize: function() { - this.unrenderHighlight(); - this.unrenderHelper(); - }, - - - /* Event Helper - ------------------------------------------------------------------------------------------------------------------*/ - - - // 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); - - // inject each new event skeleton into each associated row - this.rowEls.each(function(row, rowNode) { - var rowEl = $(rowNode); // the .fc-row - var skeletonEl = $('<div class="fc-helper-skeleton"><table/></div>'); // 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; - } - - skeletonEl.css('top', skeletonTop) - .find('table') - .append(rowStructs[row].tbodyEl); - - rowEl.append(skeletonEl); - helperNodes.push(skeletonEl[0]); - }); - - return ( // must return the elements rendered - this.helperEls = $(helperNodes) // array -> jQuery set - ); - }, - - - // Unrenders any visual indication of a mock helper event - unrenderHelper: function() { - if (this.helperEls) { - this.helperEls.remove(); - this.helperEls = null; - } - }, - - - /* Fill System (highlight, background events, business hours) - ------------------------------------------------------------------------------------------------------------------*/ - - - fillSegTag: 'td', // override the default tag name - - - // 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; - - 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]); - } - - this.elsByFill[type] = $(nodes); - - return segs; - }, - - - // 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(); - - skeletonEl = $( - '<div class="fc-' + className + '-skeleton">' + - '<table><tr/></table>' + - '</div>' - ); - trEl = skeletonEl.find('tr'); - - if (startCol > 0) { - trEl.append('<td colspan="' + startCol + '"/>'); - } - - trEl.append( - seg.el.attr('colspan', endCol - startCol) - ); - - if (endCol < colCnt) { - trEl.append('<td colspan="' + (colCnt - endCol) + '"/>'); - } - - this.bookendCells(trEl); - - return skeletonEl; - } - -}); - -;; - -/* Event-rendering methods for the DayGrid class -----------------------------------------------------------------------------------------------------------------------*/ - -DayGrid.mixin({ - - rowStructs: null, // an array of objects, each holding information about a row's foreground event-rendering - - - // 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 - }, - - - // 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 - }, - - - // 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; - }); - - 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 <tbody> 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 = '<span class="fc-time">' + htmlEscape(timeText) + '</span>'; - } - } - - titleHtml = - '<span class="fc-title">' + - (htmlEscape(event.title || '') || ' ') + // we always want one line of height - '</span>'; - - return '<a class="' + classes.join(' ') + '"' + - (event.url ? - ' href="' + htmlEscape(event.url) + '"' : - '' - ) + - (skinCss ? - ' style="' + skinCss + '"' : - '' - ) + - '>' + - '<div class="fc-content">' + - (this.isRTL ? - titleHtml + ' ' + timeHtml : // put a natural space in between - timeHtml + ' ' + titleHtml // - ) + - '</div>' + - (isResizableFromStart ? - '<div class="fc-resizer fc-start-resizer" />' : - '' - ) + - (isResizableFromEnd ? - '<div class="fc-resizer fc-end-resizer" />' : - '' - ) + - '</a>'; - }, - - - // Given a row # and an array of segments all in the same row, render a <tbody> 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 = $('<tbody/>'); - var segMatrix = []; // lookup for which segments are rendered into which level+col cells - var cellMatrix = []; // lookup for all <td> elements of the level+col matrix - var loneCellMatrix = []; // lookup for <td> 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 = $('<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 = $('<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 = $('<td class="fc-event-container"/>').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 <tr> 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" <a> links and <td> 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 <td> jQuery elements in the row - var limitedNodes; // array of temporarily hidden level <tr> and segment <td> 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" <td> 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 = $('<div/>').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 <tr> 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 <td> for each column the segment occupies. will be one for each colspan - for (j = 0; j < colSegsBelow.length; j++) { - moreTd = $('<td class="fc-more-cell"/>').attr('rowspan', rowspan); - segsBelow = colSegsBelow[j]; - moreLink = this.renderMoreLink( - row, - seg.leftCol + j, - [ seg ].concat(segsBelow) // count seg as hidden too - ); - moreWrap = $('<div/>').append(moreLink); - moreTd.append(moreWrap); - segMoreNodes.push(moreTd[0]); - moreNodes.push(moreTd[0]); - } - - td.addClass('fc-limited').after($(segMoreNodes)); // hide original <td> 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 <a> 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 $('<a class="fc-more"/>') - .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 <div> wrapper around the <a> - 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.view.el, // attach to root of view. guarantees outside of scrollbars. - 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 <td> 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(); - - // the popover doesn't live within the grid's container element, and thus won't get the event - // delegated-handlers for free. attach event-related handlers to the popover. - this.bindSegHandlersToEl(this.segPopover.el); - }, - - - // 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 = $( - '<div class="fc-header ' + view.widgetHeaderClass + '">' + - '<span class="fc-close ' + - (isTheme ? 'ui-icon ui-icon-closethick' : 'fc-icon fc-icon-x') + - '"></span>' + - '<span class="fc-title">' + - htmlEscape(title) + - '</span>' + - '<div class="fc-clear"/>' + - '</div>' + - '<div class="fc-body ' + view.widgetContentClass + '">' + - '<div class="fc-event-container"></div>' + - '</div>' - ); - 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 '' + - '<div class="fc-bg">' + - '<table>' + - this.renderBgTrHtml(0) + // row=0 - '</table>' + - '</div>' + - '<div class="fc-slats">' + - '<table>' + - this.renderSlatRowHtml() + - '</table>' + - '</div>'; - }, - - - // 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 = - '<td class="fc-axis fc-time ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' + - (isLabeled ? - '<span>' + // for matchCellWidths - htmlEscape(slotDate.format(this.labelFormat)) + - '</span>' : - '' - ) + - '</td>'; - - html += - '<tr data-time="' + slotDate.format('HH:mm:ss') + '"' + - (isLabeled ? '' : ' class="fc-minor"') + - '>' + - (!isRTL ? axisHtml : '') + - '<td class="' + view.widgetContentClass + '"/>' + - (isRTL ? axisHtml : '') + - "</tr>"; - - 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('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) - }, - - - // Computes a default `displayEventEnd` value if one is not expliclty defined - computeDisplayEventEnd: function() { - return true; - }, - - - /* Hit System - ------------------------------------------------------------------------------------------------------------------*/ - - - prepareHits: function() { - this.colCoordCache.build(); - this.slatCoordCache.build(); - }, - - - releaseHits: function() { - this.colCoordCache.clear(); - // NOTE: don't clear slatCoordCache because we rely on it for computeTimeTop - }, - - - queryHit: function(leftOffset, topOffset) { - var snapsPerSlot = this.snapsPerSlot; - var colCoordCache = this.colCoordCache; - var slatCoordCache = this.slatCoordCache; - - if (colCoordCache.isLeftInBounds(leftOffset) && slatCoordCache.isTopInBounds(topOffset)) { - var colIndex = colCoordCache.getHorizontalIndex(leftOffset); - var slatIndex = slatCoordCache.getVerticalIndex(topOffset); - - 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 { - col: colIndex, - snap: snapIndex, - component: this, // needed unfortunately :( - left: colCoordCache.getLeftOffset(colIndex), - right: colCoordCache.getRightOffset(colIndex), - top: snapTop, - bottom: snapBottom - }; - } - } - }, - - - 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); - - return { start: start, end: end }; - }, - - - getHitEl: function(hit) { - return this.colEls.eq(hit.col); - }, - - - /* Dates - ------------------------------------------------------------------------------------------------------------------*/ - - - rangeUpdated: function() { - this.updateDayTable(); - }, - - - // 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); - }, - - - // 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; - } - } - - return segs; - }, - - - 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); - } - } - - return segs; - }, - - - /* Coordinates - ------------------------------------------------------------------------------------------------------------------*/ - - - updateSize: function(isResize) { // NOT a standard Grid method - this.slatCoordCache.build(); - - if (isResize) { - this.updateSegVerticals( - [].concat(this.fgSegs || [], this.bgSegs || [], this.businessSegs || []) - ); - } - }, - - - getTotalSlatHeight: function() { - return this.slatContainerEl.outerHeight(); - }, - - - // 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() - ) - ); - }, - - - // 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); - - // 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; - - return this.slatCoordCache.getTopPosition(slatIndex) + - this.slatCoordCache.getHeight(slatIndex) * slatRemainder; - }, - - - - /* Event Drag Visualization - ------------------------------------------------------------------------------------------------------------------*/ - - - // 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) { - - 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)); - } - }, - - - // Unrenders any visual indication of an event being dragged - unrenderDrag: function() { - this.unrenderHelper(); - this.unrenderHighlight(); - }, - - - /* Event Resize Visualization - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event being resized - renderEventResize: function(eventLocation, seg) { - return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements - }, - - - // Unrenders any visual indication of an event being resized - unrenderEventResize: function() { - this.unrenderHelper(); - }, - - - /* Event Helper - ------------------------------------------------------------------------------------------------------------------*/ - - - // 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 - }, - - - // Unrenders any mock helper event - unrenderHelper: function() { - this.unrenderHelperSegs(); - }, - - - /* Business Hours - ------------------------------------------------------------------------------------------------------------------*/ - - - renderBusinessHours: function() { - this.renderBusinessSegs( - this.buildBusinessHourSegs() - ); - }, - - - unrenderBusinessHours: function() { - this.unrenderBusinessSegs(); - }, - - - /* Now Indicator - ------------------------------------------------------------------------------------------------------------------*/ - - - getNowIndicatorUnit: function() { - return 'minute'; // will refresh on the minute - }, - - - 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($('<div class="fc-now-indicator fc-now-indicator-line"></div>') - .css('top', top) - .appendTo(this.colContainerEls.eq(segs[i].col))[0]); - } - - // render an arrow over the axis - if (segs.length > 0) { // is the current time in view? - nodes.push($('<div class="fc-now-indicator fc-now-indicator-arrow"></div>') - .css('top', top) - .appendTo(this.el.find('.fc-content-skeleton'))[0]); - } - - this.nowIndicatorEls = $(nodes); - }, - - - unrenderNowIndicator: function() { - if (this.nowIndicatorEls) { - this.nowIndicatorEls.remove(); - this.nowIndicatorEls = null; - } - }, - - - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - // 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); - } - }, - - - // Unrenders any visual indication of a selection - unrenderSelection: function() { - this.unrenderHelper(); - this.unrenderHighlight(); - }, - - - /* Highlight - ------------------------------------------------------------------------------------------------------------------*/ - - - renderHighlight: function(span) { - this.renderHighlightSegs(this.spanToSegs(span)); - }, - - - unrenderHighlight: function() { - this.unrenderHighlightSegs(); - } - -}); - -;; - -/* 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, - - // arrays of different types of displayed segments - fgSegs: null, - bgSegs: null, - helperSegs: null, - highlightSegs: null, - businessSegs: null, - - - // 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 += - '<td>' + - '<div class="fc-content-col">' + - '<div class="fc-event-container fc-helper-container"></div>' + - '<div class="fc-event-container"></div>' + - '<div class="fc-highlight-container"></div>' + - '<div class="fc-bgevent-container"></div>' + - '<div class="fc-business-container"></div>' + - '</div>' + - '</td>'; - } - - skeletonEl = $( - '<div class="fc-content-skeleton">' + - '<table>' + - '<tr>' + cellHtml + '</tr>' + - '</table>' + - '</div>' - ); - - 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); - }, - - - /* Foreground Events - ------------------------------------------------------------------------------------------------------------------*/ - - - renderFgSegs: function(segs) { - segs = this.renderFgSegsIntoContainers(segs, this.fgContainerEls); - this.fgSegs = segs; - return segs; // needed for Grid::renderEvents - }, - - - unrenderFgSegs: function() { - this.unrenderNamedSegs('fgSegs'); - }, - - - /* Foreground Helper Events - ------------------------------------------------------------------------------------------------------------------*/ - - - renderHelperSegs: function(segs, sourceSeg) { - var helperEls = []; - var i, seg; - var sourceEl; - - segs = this.renderFgSegsIntoContainers(segs, this.helperContainerEls); - - // 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; - - return $(helperEls); // must return rendered helpers - }, - - - unrenderHelperSegs: function() { - this.unrenderNamedSegs('helperSegs'); - }, - - - /* Background Events - ------------------------------------------------------------------------------------------------------------------*/ - - - 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 - }, - - - unrenderBgSegs: function() { - this.unrenderNamedSegs('bgSegs'); - }, - - - /* Highlight - ------------------------------------------------------------------------------------------------------------------*/ - - - 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; - }, - - - unrenderHighlightSegs: function() { - this.unrenderNamedSegs('highlightSegs'); - }, - - - /* Business Hours - ------------------------------------------------------------------------------------------------------------------*/ - - - 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; - }, - - - unrenderBusinessSegs: function() { - this.unrenderNamedSegs('businessSegs'); - }, - - - /* Seg Rendering Utils - ------------------------------------------------------------------------------------------------------------------*/ - - - // 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; - - for (i = 0; i < this.colCnt; i++) { - segsByCol.push([]); - } - - for (i = 0; i < segs.length; i++) { - segsByCol[segs[i].col].push(segs[i]); - } - - return segsByCol; - }, - - - // 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]; - - for (i = 0; i < segs.length; i++) { - containerEls.eq(col).append(segs[i].el); - } - } - }, - - - // 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 '<a class="' + classes.join(' ') + '"' + - (event.url ? - ' href="' + htmlEscape(event.url) + '"' : - '' - ) + - (skinCss ? - ' style="' + skinCss + '"' : - '' - ) + - '>' + - '<div class="fc-content">' + - (timeText ? - '<div class="fc-time"' + - ' data-start="' + htmlEscape(startTimeText) + '"' + - ' data-full="' + htmlEscape(fullTimeText) + '"' + - '>' + - '<span>' + htmlEscape(timeText) + '</span>' + - '</div>' : - '' - ) + - (event.title ? - '<div class="fc-title">' + - htmlEscape(event.title) + - '</div>' : - '' - ) + - '</div>' + - '<div class="fc-bg"/>' + - (isResizableFromStart ? - '<div class="fc-resizer fc-start-resizer" />' : - '' - ) + - (isResizableFromEnd ? - '<div class="fc-resizer fc-end-resizer" />' : - '' - ) + - '</a>'; - }, - - - /* 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<forwardSegs.length; i++) { - this.computeFgSegForwardBack(forwardSegs[i], 0, seg.forwardCoord); - } - } - }, - - - sortForwardSegs: function(forwardSegs) { - forwardSegs.sort(proxy(this, 'compareForwardSegs')); - }, - - - // A cmp function for determining which forward segment to rely on more when computing coordinates. - compareForwardSegs: function(seg1, seg2) { - // put higher-pressure first - return seg2.forwardPressure - seg1.forwardPressure || - // put segments that are closer to initial edge first (and favor ones with no coords yet) - (seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) || - // do normal sorting... - this.compareEventSegs(seg1, seg2); - }, - - - // Given foreground event segments that have already had their position coordinates computed, - // assigns position-related CSS values to their elements. - assignFgSegHorizontals: function(segs) { - var i, seg; - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - seg.el.css(this.generateFgSegHorizontalCss(seg)); - - // if the height is short, add a className for alternate styling - if (seg.bottom - seg.top < 30) { - seg.el.addClass('fc-short'); - } - } - }, - - - // Generates an object with CSS properties/values that should be applied to an event segment element. - // Contains important positioning-related properties that should be applied to any event element, customized or not. - generateFgSegHorizontalCss: function(seg) { - var shouldOverlap = this.view.opt('slotEventOverlap'); - var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point - var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point - var props = this.generateSegVerticalCss(seg); // get top/bottom first - var left; // amount of space from left edge, a fraction of the total width - var right; // amount of space from right edge, a fraction of the total width - - if (shouldOverlap) { - // double the width, but don't go beyond the maximum forward coordinate (1.0) - forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2); - } - - if (this.isRTL) { - left = 1 - forwardCoord; - right = backwardCoord; - } - else { - left = backwardCoord; - right = 1 - forwardCoord; - } - - props.zIndex = seg.level + 1; // convert from 0-base to 1-based - props.left = left * 100 + '%'; - props.right = right * 100 + '%'; - - if (shouldOverlap && seg.forwardPressure) { - // add padding to the edge so that forward stacked events don't cover the resizer's icon - props[this.isRTL ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width - } - - return props; - } - -}); - - -// Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is -// left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date. -function buildSlotSegLevels(segs) { - var levels = []; - var i, seg; - var j; - - for (i=0; i<segs.length; i++) { - seg = segs[i]; - - // go through all the levels and stop on the first level where there are no collisions - for (j=0; j<levels.length; j++) { - if (!computeSlotSegCollisions(seg, levels[j]).length) { - break; - } - } - - seg.level = j; - - (levels[j] || (levels[j] = [])).push(seg); - } - - return levels; -} - - -// For every segment, figure out the other segments that are in subsequent -// levels that also occupy the same vertical space. Accumulate in seg.forwardSegs -function computeForwardSlotSegs(levels) { - var i, level; - var j, seg; - var k; - - for (i=0; i<levels.length; i++) { - level = levels[i]; - - for (j=0; j<level.length; j++) { - seg = level[j]; - - seg.forwardSegs = []; - for (k=i+1; k<levels.length; k++) { - computeSlotSegCollisions(seg, levels[k], seg.forwardSegs); - } - } - } -} - - -// Figure out which path forward (via seg.forwardSegs) results in the longest path until -// the furthest edge is reached. The number of segments in this path will be seg.forwardPressure -function computeSlotSegPressures(seg) { - var forwardSegs = seg.forwardSegs; - var forwardPressure = 0; - var i, forwardSeg; - - if (seg.forwardPressure === undefined) { // not already computed - - for (i=0; i<forwardSegs.length; i++) { - forwardSeg = forwardSegs[i]; - - // figure out the child's maximum forward path - computeSlotSegPressures(forwardSeg); - - // either use the existing maximum, or use the child's forward pressure - // plus one (for the forwardSeg itself) - forwardPressure = Math.max( - forwardPressure, - 1 + forwardSeg.forwardPressure - ); - } - - seg.forwardPressure = forwardPressure; - } -} - - -// Find all the segments in `otherSegs` that vertically collide with `seg`. -// Append into an optionally-supplied `results` array and return. -function computeSlotSegCollisions(seg, otherSegs, results) { - results = results || []; - - for (var i=0; i<otherSegs.length; i++) { - if (isSlotSegCollision(seg, otherSegs[i])) { - results.push(otherSegs[i]); - } - } - - return results; -} - - -// Do these segments occupy the same vertical space? -function isSlotSegCollision(seg1, seg2) { - return seg1.bottom > seg2.top && seg1.top < seg2.bottom; -} - -;; - -/* An abstract class from which other views inherit from -----------------------------------------------------------------------------------------------------------------------*/ - -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 - - 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, - - // 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" - - 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 - - // classNames styled by jqui themes - widgetHeaderClass: null, - widgetContentClass: null, - highlightStateClass: null, - - // 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, // " - - - 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'); - - this.nextDayThreshold = moment.duration(this.opt('nextDayThreshold')); - this.initThemingProps(); - this.initHiddenDays(); - this.isRTL = this.opt('isRTL'); - - this.eventOrderSpecs = parseFieldSpecs(this.opt('eventOrder')); - - this.initialize(); - }, - - - // 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]; - }, - - - // 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 - ) - ); - }, - - - /* Dates - ------------------------------------------------------------------------------------------------------------------*/ - - - // 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')); - }, - - - getAllDayHtml: function() { - return this.opt('allDayHtml') || htmlEscape(this.opt('allDayText')); - }, - - - /* Navigation - ------------------------------------------------------------------------------------------------------------------*/ - - - // Generates HTML for an anchor to another view into the calendar. - // Will either generate an <a> tag or a non-clickable <span> tag, depending on enabled settings. - // `gotoOptions` can either be a moment input, or an object with the form: - // { date, type, forceOff } - // `type` is a view-type like "day" or "week". default value is "day". - // `attrs` and `innerHtml` are use to generate the rest of the HTML tag. - buildGotoAnchorHtml: function(gotoOptions, attrs, innerHtml) { - var date, type, forceOff; - var finalOptions; - - if ($.isPlainObject(gotoOptions)) { - date = gotoOptions.date; - type = gotoOptions.type; - forceOff = gotoOptions.forceOff; - } - else { - date = gotoOptions; // a single moment input - } - date = FC.moment(date); // if a string, parse it - - finalOptions = { // for serialization into the link - date: date.format('YYYY-MM-DD'), - type: type || 'day' - }; - - if (typeof attrs === 'string') { - innerHtml = attrs; - attrs = null; - } - - attrs = attrs ? ' ' + attrsToStr(attrs) : ''; // will have a leading space - innerHtml = innerHtml || ''; - - if (!forceOff && this.opt('navLinks')) { - return '<a' + attrs + - ' data-goto="' + htmlEscape(JSON.stringify(finalOptions)) + '">' + - innerHtml + - '</a>'; - } - else { - return '<span' + attrs + '>' + - innerHtml + - '</span>'; - } - }, - - - /* 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, explicitScrollState) { - var _this = this; - var prevScrollState = null; - - if (explicitScrollState != null && this.displaying) { // don't need prevScrollState if explicitScrollState - prevScrollState = this.queryScroll(); - } - - this.calendar.freezeContentHeight(); - - return syncThen(this.clear(), function() { // clear the content first - return ( - _this.displaying = - syncThen(_this.displayView(date), function() { // displayView might return a promise - - // caller of display() wants a specific scroll state? - if (explicitScrollState != null) { - // we make an assumption that this is NOT the initial render, - // and thus don't need forceScroll (is inconveniently asynchronous) - _this.setScroll(explicitScrollState); - } - else { - _this.forceScroll(_this.computeInitialScroll(prevScrollState)); - } - - _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 syncThen(displaying, 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.processUnselect); - }, - - - // 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 - }, - - - // Unrenders previously-rendered business-hours - unrenderBusinessHours: function() { - // subclasses should implement - }, - - - /* Now Indicator - ------------------------------------------------------------------------------------------------------------------*/ - - - // 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; - - // 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); - } - } - }, - - - // 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 - ); - } - }, - - - // 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) { - - if (this.nowIndicatorTimeoutID) { - clearTimeout(this.nowIndicatorTimeoutID); - this.nowIndicatorTimeoutID = null; - } - if (this.nowIndicatorIntervalID) { - clearTimeout(this.nowIndicatorIntervalID); - this.nowIndicatorIntervalID = null; - } - - this.unrenderNowIndicator(); - this.isNowIndicatorRendered = false; - } - }, - - - // 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 - }, - - - // Renders a current time indicator at the given datetime - renderNowIndicator: function(date) { - // subclasses should implement - }, - - - // Undoes the rendering actions from renderNowIndicator - unrenderNowIndicator: function() { - // subclasses should implement - }, - - - /* Dimensions - ------------------------------------------------------------------------------------------------------------------*/ - - - // Refreshes anything dependant upon sizing of the container element of the grid - updateSize: function(isResize) { - var scrollState; - - if (isResize) { - scrollState = this.queryScroll(); - } - - this.updateHeight(isResize); - this.updateWidth(isResize); - this.updateNowIndicator(); - - if (isResize) { - this.setScroll(scrollState); - } - }, - - - // Refreshes the horizontal dimensions of the calendar - updateWidth: function(isResize) { - // subclasses should implement - }, - - - // 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() - ); - }, - - - // 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 - }, - - - /* Scroller - ------------------------------------------------------------------------------------------------------------------*/ - - - // 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; - }, - - - // Retrieves the view's current natural scroll state. Can return an arbitrary format. - queryScroll: function() { - // subclasses must implement - }, - - - // Sets the view's scroll state. Will accept the same format computeInitialScroll and queryScroll produce. - setScroll: function(scrollState) { - // subclasses must implement - }, - - - // Sets the scroll state, making sure to overcome any predefined scroll value the browser has in mind - forceScroll: function(scrollState) { - var _this = this; - - this.setScroll(scrollState); - setTimeout(function() { - _this.setScroll(scrollState); - }, 0); - }, - - - /* Event Elements / Segments - ------------------------------------------------------------------------------------------------------------------*/ - - - // Does everything necessary to display the given events onto the current view - displayEvents: function(events) { - var scrollState = this.queryScroll(); - - this.clearEvents(); - this.renderEvents(events); - this.isEventsRendered = true; - this.setScroll(scrollState); - this.triggerEventRender(); - }, - - - // Does everything necessary to clear the view's currently-rendered events - clearEvents: function() { - var scrollState; - - if (this.isEventsRendered) { - - // 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; - } - }, - - - // Renders the events onto the view. - renderEvents: function(events) { - // subclasses should implement - }, - - - // Removes event elements from the view. - unrenderEvents: function() { - // subclasses should implement - }, - - - // 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'); - }, - - - // 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); - }); - }, - - - // 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); - - if (custom === false) { // means don't render at all - el = null; - } - else if (custom && custom !== true) { - el = $(custom); - } - - return el; - }, - - - // Hides all rendered event segments linked to the given event - showEvent: function(event) { - this.renderedEventSegEach(function(seg) { - seg.el.css('visibility', ''); - }, event); - }, - - - // Shows all rendered event segments linked to the given event - hideEvent: function(event) { - this.renderedEventSegEach(function(seg) { - seg.el.css('visibility', 'hidden'); - }, event); - }, - - - // 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]); - } - } - } - }, - - - // Retrieves all the rendered segment objects for the view - getEventSegs: function() { - // subclasses must implement - return []; - }, - - - /* Event Drag-n-Drop - ------------------------------------------------------------------------------------------------------------------*/ - - - // Computes if the given event is allowed to be dragged by the user - isEventDraggable: function(event) { - return this.isEventStartEditable(event); - }, - - - isEventStartEditable: function(event) { - return firstDefined( - event.startEditable, - (event.source || {}).startEditable, - this.opt('eventStartEditable'), - this.isEventGenerallyEditable(event) - ); - }, - - - isEventGenerallyEditable: function(event) { - return firstDefined( - event.editable, - (event.source || {}).editable, - this.opt('editable') - ); - }, - - - // 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 - }, - - - // 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 - }, - - - /* External Element Drag-n-Drop - ------------------------------------------------------------------------------------------------------------------*/ - - - // 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 - } - - this.triggerExternalDrop(event, dropLocation, el, ev, ui); - }, - - - // Triggers external-drop handlers that have subscribed via the API - triggerExternalDrop: function(event, dropLocation, el, ev, ui) { - - // 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 - } - }, - - - /* Drag-n-Drop Rendering (for both events and external elements) - ------------------------------------------------------------------------------------------------------------------*/ - - - // 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 - }, - - - // Unrenders a visual indication of an event or external-element being dragged. - unrenderDrag: function() { - // subclasses must implement - }, - - - /* Event Resizing - ------------------------------------------------------------------------------------------------------------------*/ - - - // Computes if the given event is allowed to be resized from its starting edge - isEventResizableFromStart: function(event) { - return this.opt('eventResizableFromStart') && this.isEventResizable(event); - }, - - - // Computes if the given event is allowed to be resized from its ending edge - isEventResizableFromEnd: function(event) { - return this.isEventResizable(event); - }, - - - // 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') - ); - }, - - - // 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(); - }; - - this.triggerEventResize(event, mutateResult.durationDelta, undoFunc, el, ev); - calendar.reportEventChange(); // will rerender events - }, - - - // 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) - ------------------------------------------------------------------------------------------------------------------*/ - - - // 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 visual indication of the selection - renderSelection: function(span) { - // subclasses should implement - }, - - - // Called when a new selection is made. Updates internal state and triggers handlers. - reportSelection: function(span, ev) { - this.isSelected = true; - this.triggerSelect(span, ev); - }, - - - // 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 - ); - }, - - - // 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); - } - }, - - - // Unrenders a visual indication of selection - unrenderSelection: function() { - // subclasses should implement - }, - - - /* Event Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - selectEvent: function(event) { - if (!this.selectedEvent || this.selectedEvent !== event) { - this.unselectEvent(); - this.renderedEventSegEach(function(seg) { - seg.el.addClass('fc-selected'); - }, event); - this.selectedEvent = event; - } - }, - - - unselectEvent: function() { - if (this.selectedEvent) { - this.renderedEventSegEach(function(seg) { - seg.el.removeClass('fc-selected'); - }, this.selectedEvent); - this.selectedEvent = null; - } - }, - - - isEventSelected: function(event) { - // event references might change on refetchEvents(), while selectedEvent doesn't, - // so compare IDs - return this.selectedEvent && this.selectedEvent._id === event._id; - }, - - - /* Mouse / Touch Unselecting (time range & event unselection) - ------------------------------------------------------------------------------------------------------------------*/ - // TODO: move consistently to down/start or up/end? - // TODO: don't kill previous selection if touch scrolling - - - handleDocumentMousedown: function(ev) { - if (isPrimaryMouseButton(ev)) { - this.processUnselect(ev); - } - }, - - - processUnselect: function(ev) { - this.processRangeUnselect(ev); - 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; - } - -}); - -;; - -/* -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, - - - constructor: function(options) { - options = options || {}; - this.overflowX = options.overflowX || options.overflow || 'auto'; - this.overflowY = options.overflowY || options.overflow || 'auto'; - }, - - - render: function() { - this.el = this.renderEl(); - this.applyOverflow(); - }, - - - renderEl: function() { - return (this.scrollEl = $('<div class="fc-scroller"></div>')); - }, - - - // sets to natural height, unlocks overflow - clear: function() { - this.setHeight('auto'); - this.applyOverflow(); - }, - - - destroy: function() { - this.el.remove(); - }, - - - // Overflow - // ----------------------------------------------------------------------------------------------------------------- - - - 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; - - 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'; - } - - 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 }); - }, - - - // Getters / Setters - // ----------------------------------------------------------------------------------------------------------------- - - - setHeight: function(height) { - this.scrollEl.height(height); - }, - - - getScrollTop: function() { - return this.scrollEl.scrollTop(); - }, - - - setScrollTop: function(top) { - this.scrollEl.scrollTop(top); - }, - - - getClientWidth: function() { - return this.scrollEl[0].clientWidth; - }, - - - getClientHeight: function() { - return this.scrollEl[0].clientHeight; - }, - - - getScrollbarWidths: function() { - return getScrollbarWidths(this.scrollEl); - } - -}); - -;; - -var Calendar = FC.Calendar = Class.extend({ - - dirDefaults: null, // option defaults related to LTR or RTL - localeDefaults: null, // option defaults related to current locale - overrides: null, // option overrides given to the fullCalendar constructor - dynamicOverrides: null, // options set with dynamic setter method. higher precedence than view overrides. - 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() { - }, - - - // Computes the flattened options hash for the calendar and assigns to `this.options`. - // Assumes this.overrides and this.dynamicOverrides have already been initialized. - populateOptionsHash: function() { - var locale, localeDefaults; - var isRTL, dirDefaults; - - locale = firstDefined( // explicit locale option given? - this.dynamicOverrides.locale, - this.overrides.locale - ); - localeDefaults = localeOptionHash[locale]; - if (!localeDefaults) { // explicit locale option not given or invalid? - locale = Calendar.defaults.locale; - localeDefaults = localeOptionHash[locale] || {}; - } - - isRTL = firstDefined( // based on options computed so far, is direction RTL? - this.dynamicOverrides.isRTL, - this.overrides.isRTL, - localeDefaults.isRTL, - Calendar.defaults.isRTL - ); - dirDefaults = isRTL ? Calendar.rtlDefaults : {}; - - this.dirDefaults = dirDefaults; - this.localeDefaults = localeDefaults; - this.options = mergeOptions([ // merge defaults and overrides. lowest to highest precedence - Calendar.defaults, // global defaults - dirDefaults, - localeDefaults, - this.overrides, - this.dynamicOverrides - ]); - populateInstanceComputableOptions(this.options); // fill in gaps with computed options - }, - - - // 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.localeDefaults, // 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) - this.dynamicOverrides // dynamically set via setter. highest precedence - ]); - 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] || - // view can decide to look up a certain key - (spec.buttonTextKey ? buttonText[spec.buttonTextKey] : null) || - // a key like "month" - (spec.singleUnit ? buttonText[spec.singleUnit] : null); - } - - // highest to lowest priority - spec.buttonTextOverride = - queryButtonText(this.dynamicOverrides) || - 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.localeDefaults) || - 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; - - - // Exports - // ----------------------------------------------------------------------------------- - - t.render = render; - t.destroy = destroy; - t.refetchEvents = refetchEvents; - t.refetchEventSources = refetchEventSources; - 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; // getter/setter method - t.trigger = trigger; - - - // Options - // ----------------------------------------------------------------------------------- - - t.dynamicOverrides = {}; - t.viewSpecCache = {}; - t.optionHandlers = {}; // for Calendar.options.js - t.overrides = $.extend({}, overrides); // make a copy - - t.populateOptionsHash(); // sets this.options - - - - // Locale-data Internals - // ----------------------------------------------------------------------------------- - // Apply overrides to the current locale's data - - var localeData; - - // Called immediately, and when any of the options change. - // Happens before any internal objects rebuild or rerender, because this is very core. - t.bindOptions([ - 'locale', 'monthNames', 'monthNamesShort', 'dayNames', 'dayNamesShort', 'firstDay', 'weekNumberCalculation' - ], function(locale, monthNames, monthNamesShort, dayNames, dayNamesShort, firstDay, weekNumberCalculation) { - - // normalize - if (weekNumberCalculation === 'iso') { - weekNumberCalculation = 'ISO'; // normalize - } - - localeData = createObject( // make a cheap copy - getMomentLocaleData(locale) // will fall back to en - ); - - if (monthNames) { - localeData._months = monthNames; - } - if (monthNamesShort) { - localeData._monthsShort = monthNamesShort; - } - if (dayNames) { - localeData._weekdays = dayNames; - } - if (dayNamesShort) { - localeData._weekdaysShort = dayNamesShort; - } - - if (firstDay == null && weekNumberCalculation === 'ISO') { - firstDay = 1; - } - if (firstDay != null) { - var _week = createObject(localeData._week); // _week: { dow: # } - _week.dow = firstDay; - localeData._week = _week; - } - - if ( // whitelist certain kinds of input - weekNumberCalculation === 'ISO' || - weekNumberCalculation === 'local' || - typeof weekNumberCalculation === 'function' - ) { - localeData._fullCalendar_weekCalc = weekNumberCalculation; // moment-ext will know what to do with it - } - - // If the internal current date object already exists, move to new locale. - // We do NOT need to do this technique for event dates, because this happens when converting to "segments". - if (date) { - localizeMoment(date); // sets to localeData - } - }); - - - // Calendar-specific Date Utilities - // ----------------------------------------------------------------------------------- - - - t.defaultAllDayEventDuration = moment.duration(t.options.defaultAllDayEventDuration); - t.defaultTimedEventDuration = moment.duration(t.options.defaultTimedEventDuration); - - - // Builds a moment using the settings of the current calendar: timezone and locale. - // Accepts anything the vanilla moment() constructor accepts. - t.moment = function() { - var mom; - - if (t.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 (t.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 - } - - localizeMoment(mom); - - return mom; - }; - - - // Updates the given moment's locale settings to the current calendar locale settings. - function localizeMoment(mom) { - mom._locale = localeData; - } - t.localizeMoment = localizeMoment; - - - // 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 t.options.timezone !== 'local' && t.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 = t.options.now; - if (typeof now === 'function') { - now = now(); - } - return t.moment(now).stripZone(); - }; - - - // 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); - } - }; - - - // 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); - } - - 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(t.options.locale).humanize(); - }; - - - - // Imports - // ----------------------------------------------------------------------------------- - - - EventManager.call(t); - var isFetchNeeded = t.isFetchNeeded; - var fetchEvents = t.fetchEvents; - var fetchEventSources = t.fetchEventSources; - - - - // Locals - // ----------------------------------------------------------------------------------- - - - var _element = element[0]; - var header; - 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 - - - - // Main Rendering - // ----------------------------------------------------------------------------------- - - - // compute the initial ambig-timezone date - if (t.options.defaultDate != null) { - date = t.moment(t.options.defaultDate).stripZone(); - } - else { - date = t.getNow(); // getNow already returns unzoned - } - - - function render() { - if (!content) { - initialRender(); - } - else if (elementVisible()) { - // mainly for the public API - calcSize(); - renderView(); - } - } - - - function initialRender() { - element.addClass('fc'); - - // event delegation for nav links - element.on('click.fc', 'a[data-goto]', function(ev) { - var anchorEl = $(this); - var gotoOptions = anchorEl.data('goto'); // will automatically parse JSON - var date = t.moment(gotoOptions.date); - var viewType = gotoOptions.type; - - // property like "navLinkDayClick". might be a string or a function - var customAction = currentView.opt('navLink' + capitaliseFirstLetter(viewType) + 'Click'); - - if (typeof customAction === 'function') { - customAction(date, ev); - } - else { - if (typeof customAction === 'string') { - viewType = customAction; - } - zoomTo(date, viewType); - } - }); - - // called immediately, and upon option change - t.bindOption('theme', function(theme) { - tm = theme ? 'ui' : 'fc'; // affects a larger scope - element.toggleClass('ui-widget', theme); - element.toggleClass('fc-unthemed', !theme); - }); - - // called immediately, and upon option change. - // HACK: locale often affects isRTL, so we explicitly listen to that too. - t.bindOptions([ 'isRTL', 'locale' ], function(isRTL) { - element.toggleClass('fc-ltr', !isRTL); - element.toggleClass('fc-rtl', isRTL); - }); - - content = $("<div class='fc-view-container'/>").prependTo(element); - - header = t.header = new Header(t); - renderHeader(); - - renderView(t.options.defaultView); - - if (t.options.handleWindowResize) { - windowResizeProxy = debounce(windowResize, t.options.windowResizeDelay); // prevents rapid calls - $(window).resize(windowResizeProxy); - } - } - - - // can be called repeatedly and Header will rerender - function renderHeader() { - header.render(); - if (header.el) { - element.prepend(header.el); - } - } - - - function destroy() { - - if (currentView) { - currentView.removeElement(); - - // 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-ltr fc-rtl fc-unthemed ui-widget'); - - element.off('.fc'); // unbind nav link handlers - - if (windowResizeProxy) { - $(window).unbind('resize', windowResizeProxy); - } - } - - - function elementVisible() { - return element.is(':visible'); - } - - - - // View Rendering - // ----------------------------------------------------------------------------------- - - - // 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. - // Accepts an optional scroll state to restore to. - function renderView(viewType, explicitScrollState) { - ignoreWindowResize++; - - // if viewType is changing, remove the old view's rendering - if (currentView && viewType && currentView.type !== viewType) { - freezeContentHeight(); // prevent a scroll jump when view element is removed - clearView(); - } - - // 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( - $("<div class='fc-view fc-" + viewType + "-view' />").appendTo(content) - ); - header.activateButton(viewType); - } - - if (currentView) { - - // in case the view should render a period of time that is completely hidden - date = currentView.massageCurrentDate(date); - - // render or rerender the view - if ( - !currentView.displaying || - !( // NOT within interval range signals an implicit date window change - date >= currentView.intervalStart && - date < currentView.intervalEnd - ) - ) { - if (elementVisible()) { - - currentView.display(date, explicitScrollState); // will call freezeContentHeight - unfreezeContentHeight(); // immediately unfreeze regardless of whether display is async - - // need to do this after View::render, so dates are calculated - updateHeaderTitle(); - updateTodayButton(); - - getAndRenderEvents(); - } - } - } - - unfreezeContentHeight(); // undo any lone freezeContentHeight calls - ignoreWindowResize--; - } - - - // Unrenders the current view and reflects this change in the Header. - // Unregsiters the `currentView`, but does not remove from viewByType hash. - function clearView() { - header.deactivateButton(currentView.type); - currentView.removeElement(); - currentView = t.view = null; - } - - - // Destroys the view, including the view object. Then, re-instantiates it and renders it. - // Maintains the same scroll state. - // TODO: maintain any other user-manipulated state. - function reinitView() { - ignoreWindowResize++; - freezeContentHeight(); - - var viewType = currentView.type; - var scrollState = currentView.queryScroll(); - clearView(); - renderView(viewType, scrollState); - - unfreezeContentHeight(); - ignoreWindowResize--; - } - - - - // Resizing - // ----------------------------------------------------------------------------------- - - - t.getSuggestedViewHeight = function() { - if (suggestedViewHeight === undefined) { - calcSize(); - } - return suggestedViewHeight; - }; - - - t.isHeightAuto = function() { - return t.options.contentHeight === 'auto' || t.options.height === 'auto'; - }; - - - function updateSize(shouldRecalc) { - if (elementVisible()) { - - if (shouldRecalc) { - _calcSize(); - } - - ignoreWindowResize++; - currentView.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto() - ignoreWindowResize--; - - return true; // signal success - } - } - - - function calcSize() { - if (elementVisible()) { - _calcSize(); - } - } - - - function _calcSize() { // assumes elementVisible - var contentHeightInput = t.options.contentHeight; - var heightInput = t.options.height; - - if (typeof contentHeightInput === 'number') { // exists and not 'auto' - suggestedViewHeight = contentHeightInput; - } - else if (typeof contentHeightInput === 'function') { // exists and is a function - suggestedViewHeight = contentHeightInput(); - } - else if (typeof heightInput === 'number') { // exists and not 'auto' - suggestedViewHeight = heightInput - queryHeaderHeight(); - } - else if (typeof heightInput === 'function') { // exists and is a function - suggestedViewHeight = heightInput() - queryHeaderHeight(); - } - else if (heightInput === 'parent') { // set to height of parent element - suggestedViewHeight = element.parent().height() - queryHeaderHeight(); - } - else { - suggestedViewHeight = Math.round(content.width() / Math.max(t.options.aspectRatio, .5)); - } - } - - - function queryHeaderHeight() { - return header.el ? header.el.outerHeight(true) : 0; // includes margin - } - - - 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); - } - } - } - - - - /* Event Fetching/Rendering - -----------------------------------------------------------------------------*/ - // TODO: going forward, most of this stuff should be directly handled by the view - - - function refetchEvents() { // can be called as an API method - fetchAndRenderEvents(); - } - - - // TODO: move this into EventManager? - function refetchEventSources(matchInputs) { - fetchEventSources(t.getEventSourcesByMatchArray(matchInputs)); - } - - - function renderEvents() { // destroys old events if previously rendered - if (elementVisible()) { - freezeContentHeight(); - currentView.displayEvents(events); - unfreezeContentHeight(); - } - } - - - function getAndRenderEvents() { - if (!t.options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) { - fetchAndRenderEvents(); - } - else { - renderEvents(); - } - } - - - function fetchAndRenderEvents() { - fetchEvents(currentView.start, currentView.end); - // ... will call reportEvents - // ... which will call renderEvents - } - - - // called when event data arrives - function reportEvents(_events) { - events = _events; - renderEvents(); - } - - - // called when a single event's data has been changed - function reportEventChange() { - renderEvents(); - } - - - - /* Header Updating - -----------------------------------------------------------------------------*/ - - - function updateHeaderTitle() { - header.updateTitle(currentView.title); - } - - - function updateTodayButton() { - var now = t.getNow(); - - if (now >= currentView.intervalStart && now < currentView.intervalEnd) { - header.disableButton('today'); - } - else { - header.enableButton('today'); - } - } - - - - /* Selection - -----------------------------------------------------------------------------*/ - - - // this public method receives start/end dates in any format, with any timezone - function select(zonedStartInput, zonedEndInput) { - currentView.select( - t.buildSelectSpan.apply(t, arguments) - ); - } - - - function unselect() { // safe to be called before renderView - if (currentView) { - currentView.unselect(); - } - } - - - - /* Date - -----------------------------------------------------------------------------*/ - - - function prev() { - date = currentView.computePrevDate(date); - renderView(); - } - - - function next() { - date = currentView.computeNextDate(date); - renderView(); - } - - - function prevYear() { - date.add(-1, 'years'); - renderView(); - } - - - function nextYear() { - date.add(1, 'years'); - renderView(); - } - - - function today() { - date = t.getNow(); - renderView(); - } - - - function gotoDate(zonedDateInput) { - date = t.moment(zonedDateInput).stripZone(); - renderView(); - } - - - function incrementDate(delta) { - date.add(moment.duration(delta)); - renderView(); - } - - - // 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; - - viewType = viewType || 'day'; // day is default zoom - spec = t.getViewSpec(viewType) || t.getUnitViewSpec(viewType); - - date = newDate.clone(); - renderView(spec ? spec.type : null); - } - - - // for external API - function getDate() { - return t.applyTimezone(date); // infuse the calendar's timezone - } - - - - /* Height "Freezing" - -----------------------------------------------------------------------------*/ - // TODO: move this into the view - - t.freezeContentHeight = freezeContentHeight; - t.unfreezeContentHeight = unfreezeContentHeight; - - - function freezeContentHeight() { - content.css({ - width: '100%', - height: content.height(), - overflow: 'hidden' - }); - } - - - function unfreezeContentHeight() { - content.css({ - width: '', - height: '', - overflow: '' - }); - } - - - - /* Misc - -----------------------------------------------------------------------------*/ - - - function getCalendar() { - return t; - } - - - function getView() { - return currentView; - } - - - function option(name, value) { - var newOptionHash; - - if (typeof name === 'string') { - if (value === undefined) { // getter - return t.options[name]; - } - else { // setter for individual option - newOptionHash = {}; - newOptionHash[name] = value; - setOptions(newOptionHash); - } - } - else if (typeof name === 'object') { // compound setter with object input - setOptions(name); - } - } - - - function setOptions(newOptionHash) { - var optionCnt = 0; - var optionName; - - for (optionName in newOptionHash) { - t.dynamicOverrides[optionName] = newOptionHash[optionName]; - } - - t.viewSpecCache = {}; // the dynamic override invalidates the options in this cache, so just clear it - t.populateOptionsHash(); // this.options needs to be recomputed after the dynamic override - - // trigger handlers after this.options has been updated - for (optionName in newOptionHash) { - t.triggerOptionHandlers(optionName); // recall bindOption/bindOptions - optionCnt++; - } - - // special-case handling of single option change. - // if only one option change, `optionName` will be its name. - if (optionCnt === 1) { - if (optionName === 'height' || optionName === 'contentHeight' || optionName === 'aspectRatio') { - updateSize(true); // true = allow recalculation of height - return; - } - else if (optionName === 'defaultDate') { - return; // can't change date this way. use gotoDate instead - } - else if (optionName === 'businessHours') { - if (currentView) { - currentView.unrenderBusinessHours(); - currentView.renderBusinessHours(); - } - return; - } - else if (optionName === 'timezone') { - t.rezoneArrayEventSources(); - refetchEvents(); - return; - } - } - - // catch-all. rerender the header and rebuild/rerender the current view - renderHeader(); - viewsByType = {}; // even non-current views will be affected by this option change. do before rerender - reinitView(); - } - - - function trigger(name, thisObj) { // overrides the Emitter's trigger method :( - var args = Array.prototype.slice.call(arguments, 2); - - thisObj = thisObj || _element; - this.triggerWith(name, thisObj, args); // Emitter's method - - if (t.options[name]) { - return t.options[name].apply(thisObj, args); - } - } - - t.initialize(); -} - -;; -/* -Options binding/triggering system. -*/ -Calendar.mixin({ - - // A map of option names to arrays of handler objects. Initialized to {} in Calendar. - // Format for a handler object: - // { - // func // callback function to be called upon change - // names // option names whose values should be given to func - // } - optionHandlers: null, - - // Calls handlerFunc immediately, and when the given option has changed. - // handlerFunc will be given the option value. - bindOption: function(optionName, handlerFunc) { - this.bindOptions([ optionName ], handlerFunc); - }, - - // Calls handlerFunc immediately, and when any of the given options change. - // handlerFunc will be given each option value as ordered function arguments. - bindOptions: function(optionNames, handlerFunc) { - var handlerObj = { func: handlerFunc, names: optionNames }; - var i; - - for (i = 0; i < optionNames.length; i++) { - this.registerOptionHandlerObj(optionNames[i], handlerObj); - } - - this.triggerOptionHandlerObj(handlerObj); - }, - - // Puts the given handler object into the internal hash - registerOptionHandlerObj: function(optionName, handlerObj) { - (this.optionHandlers[optionName] || (this.optionHandlers[optionName] = [])) - .push(handlerObj); - }, - - // Reports that the given option has changed, and calls all appropriate handlers. - triggerOptionHandlers: function(optionName) { - var handlerObjs = this.optionHandlers[optionName] || []; - var i; - - for (i = 0; i < handlerObjs.length; i++) { - this.triggerOptionHandlerObj(handlerObjs[i]); - } - }, - - // Calls the callback for a specific handler object, passing in the appropriate arguments. - triggerOptionHandlerObj: function(handlerObj) { - var optionNames = handlerObj.names; - var optionValues = []; - var i; - - for (i = 0; i < optionNames.length; i++) { - optionValues.push(this.options[optionNames[i]]); - } - - handlerObj.func.apply(this, optionValues); // maintain the Calendar's `this` context - } - -}); - -;; - -Calendar.defaults = { - - titleRangeSeparator: ' \u2013 ', // en dash - monthYearFormat: 'MMMM YYYY', // required for en. other locales 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' - }, - - allDayText: 'all-day', - - // 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: 100, // milliseconds before an updateSize happens - - longPressDelay: 1000 - -}; - - -Calendar.englishDefaults = { // used by locale.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' - } -}; - -;; - -var localeOptionHash = FC.locales = {}; // initialize and expose - - -// TODO: document the structure and ordering of a FullCalendar locale file - - -// Initialize jQuery UI datepicker translations while using some of the translations -// Will set this as the default locales for datepicker. -FC.datepickerLocale = function(localeCode, dpLocaleCode, dpOptions) { - - // get the FullCalendar internal option hash for this locale. create if necessary - var fcOptions = localeOptionHash[localeCode] || (localeOptionHash[localeCode] = {}); - - // transfer some simple options from datepicker to fc - fcOptions.isRTL = dpOptions.isRTL; - fcOptions.weekNumberTitle = dpOptions.weekHeader; - - // compute some more complex options from datepicker - $.each(dpComputableOptions, function(name, func) { - fcOptions[name] = func(dpOptions); - }); - - // is jQuery UI Datepicker is on the page? - if ($.datepicker) { - - // Register the locale data. - // FullCalendar and MomentJS use locale codes like "pt-br" but Datepicker - // does it like "pt-BR" or if it doesn't have the locale, maybe just "pt". - // Make an alias so the locale can be referenced either way. - $.datepicker.regional[dpLocaleCode] = - $.datepicker.regional[localeCode] = // alias - dpOptions; - - // Alias 'en' to the default locale data. Do this every time. - $.datepicker.regional.en = $.datepicker.regional['']; - - // Set as Datepicker's global defaults. - $.datepicker.setDefaults(dpOptions); - } -}; - - -// Sets FullCalendar-specific translations. Will set the locales as the global default. -FC.locale = function(localeCode, newFcOptions) { - var fcOptions; - var momOptions; - - // get the FullCalendar internal option hash for this locale. create if necessary - fcOptions = localeOptionHash[localeCode] || (localeOptionHash[localeCode] = {}); - - // provided new options for this locales? merge them in - if (newFcOptions) { - fcOptions = localeOptionHash[localeCode] = mergeOptions([ fcOptions, newFcOptions ]); - } - - // compute locale 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(localeCode); // will fall back to en - $.each(momComputableOptions, function(name, func) { - if (fcOptions[name] == null) { - fcOptions[name] = func(momOptions, fcOptions); - } - }); - - // set it as the default locale for FullCalendar - Calendar.defaults.locale = localeCode; -}; - - -// NOTE: can't guarantee any of these computations will run because not every locale 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 locales - .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 locales - .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 locales - .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 locale? -// 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. -function getMomentLocaleData(localeCode) { - return moment.localeData(localeCode) || moment.localeData('en'); -} - - -// Initialize English by forcing computation of moment-derived options. -// Also, sets it as the default. -FC.locale('en', Calendar.englishDefaults); - -;; - -/* Top toolbar area with buttons and title -----------------------------------------------------------------------------------------------------------------------*/ -// TODO: rename all header-related things to "toolbar" - -function Header(calendar) { - 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; - t.el = null; // mirrors local `el` - - // locals - var el; - var viewsWithButtons = []; - var tm; - - - // can be called repeatedly and will rerender - function render() { - var options = calendar.options; - var sections = options.header; - - tm = options.theme ? 'ui' : 'fc'; - - if (sections) { - if (!el) { - el = this.el = $("<div class='fc-toolbar'/>"); - } - else { - el.empty(); - } - el.append(renderSection('left')) - .append(renderSection('right')) - .append(renderSection('center')) - .append('<div class="fc-clear"/>'); - } - else { - removeElement(); - } - } - - - function removeElement() { - if (el) { - el.remove(); - el = t.el = null; - } - } - - - function renderSection(position) { - var sectionEl = $('<div class="fc-' + position + '"/>'); - var options = calendar.options; - 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($('<h2> </h2>')); // we always want it to take up height - isOnlyButtons = false; - } - else { - if ((customButtonProps = (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 = "<span class='ui-icon ui-icon-" + themeIcon + "'></span>"; - } - else if (normalIcon && !options.theme) { - innerHtml = "<span class='fc-icon fc-icon-" + normalIcon + "'></span>"; - } - else { - innerHtml = htmlEscape(defaultText); - } - - classes = [ - 'fc-' + buttonName + '-button', - tm + '-button', - tm + '-state-default' - ]; - - button = $( // type="button" so that it doesn't submit a form - '<button type="button" class="' + classes.join(' ') + '">' + - innerHtml + - '</button>' - ) - .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 = $('<div/>'); - 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) { - if (el) { - el.find('h2').text(text); - } - } - - - function activateButton(buttonName) { - if (el) { - el.find('.fc-' + buttonName + '-button') - .addClass(tm + '-state-active'); - } - } - - - function deactivateButton(buttonName) { - if (el) { - el.find('.fc-' + buttonName + '-button') - .removeClass(tm + '-state-active'); - } - } - - - function disableButton(buttonName) { - if (el) { - el.find('.fc-' + buttonName + '-button') - .prop('disabled', true) - .addClass(tm + '-state-disabled'); - } - } - - - function enableButton(buttonName) { - if (el) { - el.find('.fc-' + buttonName + '-button') - .prop('disabled', false) - .removeClass(tm + '-state-disabled'); - } - } - - - function getViewsWithButtons() { - return viewsWithButtons; - } - -} - -;; - -FC.sourceNormalizers = []; -FC.sourceFetchers = []; - -var ajaxDefaults = { - dataType: 'json', - cache: false -}; - -var eventGUID = 1; - - -function EventManager() { // assumed to be a calendar - var t = this; - - - // exports - t.isFetchNeeded = isFetchNeeded; - t.fetchEvents = fetchEvents; - t.fetchEventSources = fetchEventSources; - t.getEventSources = getEventSources; - t.getEventSourceById = getEventSourceById; - t.getEventSourcesByMatchArray = getEventSourcesByMatchArray; - t.getEventSourcesByMatch = getEventSourcesByMatch; - t.addEventSource = addEventSource; - t.removeEventSource = removeEventSource; - t.removeEventSources = removeEventSources; - 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 pendingSourceCnt = 0; // outstanding fetch requests, max one per source - var cache = []; // holds events that have already been expanded - - - $.each( - (t.options.events ? [ t.options.events ] : []).concat(t.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; - fetchEventSources(sources, 'reset'); - } - - - // expects an array of event source objects (the originals, not copies) - // `specialFetchType` is an optimization parameter that affects purging of the event cache. - function fetchEventSources(specificSources, specialFetchType) { - var i, source; - - if (specialFetchType === 'reset') { - cache = []; - } - else if (specialFetchType !== 'add') { - cache = excludeEventsBySources(cache, specificSources); - } - - for (i = 0; i < specificSources.length; i++) { - source = specificSources[i]; - - // already-pending sources have already been accounted for in pendingSourceCnt - if (source._status !== 'pending') { - pendingSourceCnt++; - } - - source._fetchId = (source._fetchId || 0) + 1; - source._status = 'pending'; - } - - for (i = 0; i < specificSources.length; i++) { - source = specificSources[i]; - - tryFetchEventSource(source, source._fetchId); - } - } - - - // fetches an event source and processes its result ONLY if it is still the current fetch. - // caller is responsible for incrementing pendingSourceCnt first. - function tryFetchEventSource(source, fetchId) { - _fetchEventSource(source, function(eventInputs) { - var isArraySource = $.isArray(source.events); - var i, eventInput; - var abstractEvent; - - if ( - // is this the source's most recent fetch? - // if not, rely on an upcoming fetch of this source to decrement pendingSourceCnt - fetchId === source._fetchId && - // event source no longer valid? - source._status !== 'rejected' - ) { - source._status = 'resolved'; - - if (eventInputs) { - for (i = 0; i < eventInputs.length; i++) { - eventInput = eventInputs[i]; - - if (isArraySource) { // array sources have already been convert to Event Objects - abstractEvent = eventInput; - } - else { - abstractEvent = buildEventFromInput(eventInput, source); - } - - if (abstractEvent) { // not false (an invalid event) - cache.push.apply( - cache, - expandEvent(abstractEvent) // add individual expanded events to the cache - ); - } - } - } - - decrementPendingSourceCnt(); - } - }); - } - - - function rejectEventSource(source) { - var wasPending = source._status === 'pending'; - - source._status = 'rejected'; - - if (wasPending) { - decrementPendingSourceCnt(); - } - } - - - function decrementPendingSourceCnt() { - pendingSourceCnt--; - if (!pendingSourceCnt) { - reportEvents(cache); - } - } - - - function _fetchEventSource(source, callback) { - var i; - var fetchers = FC.sourceFetchers; - var res; - - for (i=0; i<fetchers.length; i++) { - res = fetchers[i].call( - t, // this, the Calendar object - source, - rangeStart.clone(), - rangeEnd.clone(), - t.options.timezone, - callback - ); - - if (res === true) { - // the fetcher is in charge. made its own async request - return; - } - else if (typeof res == 'object') { - // the fetcher returned a new source. process it - _fetchEventSource(res, callback); - return; - } - } - - var events = source.events; - if (events) { - if ($.isFunction(events)) { - t.pushLoading(); - events.call( - t, // this, the Calendar object - rangeStart.clone(), - rangeEnd.clone(), - t.options.timezone, - function(events) { - callback(events); - t.popLoading(); - } - ); - } - else if ($.isArray(events)) { - callback(events); - } - else { - callback(); - } - }else{ - var url = source.url; - if (url) { - var success = source.success; - var error = source.error; - var complete = source.complete; - - // retrieve any outbound GET/POST $.ajax data from the options - var customData; - if ($.isFunction(source.data)) { - // supplied as a function that returns a key/value object - customData = source.data(); - } - else { - // supplied as a straight key/value object - customData = source.data; - } - - // use a copy of the custom data so we can modify the parameters - // and not affect the passed-in object. - var data = $.extend({}, customData || {}); - - var startParam = firstDefined(source.startParam, t.options.startParam); - var endParam = firstDefined(source.endParam, t.options.endParam); - var timezoneParam = firstDefined(source.timezoneParam, t.options.timezoneParam); - - if (startParam) { - data[startParam] = rangeStart.format(); - } - if (endParam) { - data[endParam] = rangeEnd.format(); - } - if (t.options.timezone && t.options.timezone != 'local') { - data[timezoneParam] = t.options.timezone; - } - - t.pushLoading(); - $.ajax($.extend({}, ajaxDefaults, source, { - data: data, - success: function(events) { - events = events || []; - var res = applyAll(success, this, arguments); - if ($.isArray(res)) { - events = res; - } - callback(events); - }, - error: function() { - applyAll(error, this, arguments); - callback(); - }, - complete: function() { - applyAll(complete, this, arguments); - t.popLoading(); - } - })); - }else{ - callback(); - } - } - } - - - - /* Sources - -----------------------------------------------------------------------------*/ - - - function addEventSource(sourceInput) { - var source = buildEventSource(sourceInput); - if (source) { - sources.push(source); - fetchEventSources([ source ], 'add'); // will eventually call reportEvents - } - } - - - function buildEventSource(sourceInput) { // will return undefined if invalid source - var normalizers = FC.sourceNormalizers; - var source; - var i; - - if ($.isFunction(sourceInput) || $.isArray(sourceInput)) { - source = { events: sourceInput }; - } - else if (typeof sourceInput === 'string') { - source = { url: sourceInput }; - } - else if (typeof sourceInput === 'object') { - source = $.extend({}, sourceInput); // shallow copy - } - - if (source) { - - // TODO: repeat code, same code for event classNames - if (source.className) { - if (typeof source.className === 'string') { - source.className = source.className.split(/\s+/); - } - // otherwise, assumed to be an array - } - else { - source.className = []; - } - - // for array sources, we convert to standard Event Objects up front - if ($.isArray(source.events)) { - source.origArray = source.events; // for removeEventSource - source.events = $.map(source.events, function(eventInput) { - return buildEventFromInput(eventInput, source); - }); - } - - for (i=0; i<normalizers.length; i++) { - normalizers[i].call(t, source); - } - - return source; - } - } - - - function removeEventSource(matchInput) { - removeSpecificEventSources( - getEventSourcesByMatch(matchInput) - ); - } - - - // if called with no arguments, removes all. - function removeEventSources(matchInputs) { - if (matchInputs == null) { - removeSpecificEventSources(sources, true); // isAll=true - } - else { - removeSpecificEventSources( - getEventSourcesByMatchArray(matchInputs) - ); - } - } - - - function removeSpecificEventSources(targetSources, isAll) { - var i; - - // cancel pending requests - for (i = 0; i < targetSources.length; i++) { - rejectEventSource(targetSources[i]); - } - - if (isAll) { // an optimization - sources = []; - cache = []; - } - else { - // remove from persisted source list - sources = $.grep(sources, function(source) { - for (i = 0; i < targetSources.length; i++) { - if (source === targetSources[i]) { - return false; // exclude - } - } - return true; // include - }); - - cache = excludeEventsBySources(cache, targetSources); - } - - reportEvents(cache); - } - - - function getEventSources() { - return sources.slice(1); // returns a shallow copy of sources with stickySource removed - } - - - function getEventSourceById(id) { - return $.grep(sources, function(source) { - return source.id && source.id === id; - })[0]; - } - - - // like getEventSourcesByMatch, but accepts multple match criteria (like multiple IDs) - function getEventSourcesByMatchArray(matchInputs) { - - // coerce into an array - if (!matchInputs) { - matchInputs = []; - } - else if (!$.isArray(matchInputs)) { - matchInputs = [ matchInputs ]; - } - - var matchingSources = []; - var i; - - // resolve raw inputs to real event source objects - for (i = 0; i < matchInputs.length; i++) { - matchingSources.push.apply( // append - matchingSources, - getEventSourcesByMatch(matchInputs[i]) - ); - } - - return matchingSources; - } - - - // matchInput can either by a real event source object, an ID, or the function/URL for the source. - // returns an array of matching source objects. - function getEventSourcesByMatch(matchInput) { - var i, source; - - // given an proper event source object - for (i = 0; i < sources.length; i++) { - source = sources[i]; - if (source === matchInput) { - return [ source ]; - } - } - - // an ID match - source = getEventSourceById(matchInput); - if (source) { - return [ source ]; - } - - return $.grep(sources, function(source) { - return isSourcesEquivalent(matchInput, source); - }); - } - - - function isSourcesEquivalent(source1, source2) { - return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2); - } - - - function getSourcePrimitive(source) { - return ( - (typeof source === 'object') ? // a normalized event source? - (source.origArray || source.googleCalendarId || source.url || source.events) : // get the primitive - null - ) || - source; // the given argument *is* the primitive - } - - - // util - // returns a filtered array without events that are part of any of the given sources - function excludeEventsBySources(specificEvents, specificSources) { - return $.grep(specificEvents, function(event) { - for (var i = 0; i < specificSources.length; i++) { - if (event.source === specificSources[i]) { - return false; // exclude - } - } - return true; // keep - }); - } - - - - /* Manipulation - -----------------------------------------------------------------------------*/ - - - // Only ever called from the externally-facing API - function updateEvent(event) { - - // massage start/end values, even if date string values - event.start = t.moment(event.start); - if (event.end) { - event.end = t.moment(event.end); - } - else { - event.end = null; - } - - mutateEvent(event, getMiscEventProps(event)); // will handle start/end/allDay normalization - reportEvents(cache); // reports event modifications (so we can redraw) - } - - - // Returns a hash of misc event properties that should be copied over to related events. - function getMiscEventProps(event) { - var props = {}; - - $.each(event, function(name, val) { - if (isMiscEventPropName(name)) { - if (val !== undefined && isAtomic(val)) { // a defined non-object - props[name] = val; - } - } - }); - - return props; - } - - // non-date-related, non-id-related, non-secret - function isMiscEventPropName(name) { - return !/^_|^(id|allDay|start|end)$/.test(name); - } - - - // returns the expanded events that were created - function renderEvent(eventInput, stick) { - var abstractEvent = buildEventFromInput(eventInput); - var events; - var i, event; - - if (abstractEvent) { // not false (a valid input) - events = expandEvent(abstractEvent); - - for (i = 0; i < events.length; i++) { - event = events[i]; - - if (!event.source) { - if (stick) { - stickySource.events.push(event); - event.source = stickySource; - } - cache.push(event); - } - } - - reportEvents(cache); - - return events; - } - - return []; - } - - - function removeEvents(filter) { - var eventID; - var i; - - if (filter == null) { // null or undefined. remove all events - filter = function() { return true; }; // will always match - } - else if (!$.isFunction(filter)) { // an event ID - eventID = filter + ''; - filter = function(event) { - return event._id == eventID; - }; - } - - // Purge event(s) from our local cache - cache = $.grep(cache, filter, true); // inverse=true - - // Remove events from array sources. - // This works because they have been converted to official Event Objects up front. - // (and as a result, event._id has been calculated). - for (i=0; i<sources.length; i++) { - if ($.isArray(sources[i].events)) { - sources[i].events = $.grep(sources[i].events, filter, true); - } - } - - reportEvents(cache); - } - - - function clientEvents(filter) { - if ($.isFunction(filter)) { - return $.grep(cache, filter); - } - else if (filter != null) { // not null, not undefined. an event ID - filter += ''; - return $.grep(cache, function(e) { - return e._id == filter; - }); - } - return cache; // else, return all - } - - - // Makes sure all array event sources have their internal event objects - // converted over to the Calendar's current timezone. - t.rezoneArrayEventSources = function() { - var i; - var events; - var j; - - for (i = 0; i < sources.length; i++) { - events = sources[i].events; - if ($.isArray(events)) { - - for (j = 0; j < events.length; j++) { - rezoneEventDates(events[j]); - } - } - } - }; - - function rezoneEventDates(event) { - event.start = t.moment(event.start); - if (event.end) { - event.end = t.moment(event.end); - } - backupEventDates(event); - } - - - /* Event Normalization - -----------------------------------------------------------------------------*/ - - - // Given a raw object with key/value properties, returns an "abstract" Event object. - // An "abstract" event is an event that, if recurring, will not have been expanded yet. - // Will return `false` when input is invalid. - // `source` is optional - function buildEventFromInput(input, source) { - var out = {}; - var start, end; - var allDay; - - if (t.options.eventDataTransform) { - input = t.options.eventDataTransform(input); - } - if (source && source.eventDataTransform) { - input = source.eventDataTransform(input); - } - - // Copy all properties over to the resulting object. - // The special-case properties will be copied over afterwards. - $.extend(out, input); - - if (source) { - out.source = source; - } - - out._id = input._id || (input.id === undefined ? '_fc' + eventGUID++ : input.id + ''); - - if (input.className) { - if (typeof input.className == 'string') { - out.className = input.className.split(/\s+/); - } - else { // assumed to be an array - out.className = input.className; - } - } - else { - out.className = []; - } - - start = input.start || input.date; // "date" is an alias for "start" - end = input.end; - - // parse as a time (Duration) if applicable - if (isTimeString(start)) { - start = moment.duration(start); - } - if (isTimeString(end)) { - end = moment.duration(end); - } - - if (input.dow || moment.isDuration(start) || moment.isDuration(end)) { - - // the event is "abstract" (recurring) so don't calculate exact start/end dates just yet - out.start = start ? moment.duration(start) : null; // will be a Duration or null - out.end = end ? moment.duration(end) : null; // will be a Duration or null - out._recurring = true; // our internal marker - } - else { - - if (start) { - start = t.moment(start); - if (!start.isValid()) { - return false; - } - } - - if (end) { - end = t.moment(end); - if (!end.isValid()) { - end = null; // let defaults take over - } - } - - allDay = input.allDay; - if (allDay === undefined) { // still undefined? fallback to default - allDay = firstDefined( - source ? source.allDayDefault : undefined, - t.options.allDayDefault - ); - // still undefined? normalizeEventDates will calculate it - } - - assignDatesToEvent(start, end, allDay, out); - } - - t.normalizeEvent(out); // hook for external use. a prototype method - - return out; - } - t.buildEventFromInput = buildEventFromInput; - - - // Normalizes and assigns the given dates to the given partially-formed event object. - // NOTE: mutates the given start/end moments. does not make a copy. - function assignDatesToEvent(start, end, allDay, event) { - event.start = start; - event.end = end; - event.allDay = allDay; - normalizeEventDates(event); - backupEventDates(event); - } - - - // Ensures proper values for allDay/start/end. Accepts an Event object, or a plain object with event-ish properties. - // NOTE: Will modify the given object. - function normalizeEventDates(eventProps) { - - normalizeEventTimes(eventProps); - - if (eventProps.end && !eventProps.end.isAfter(eventProps.start)) { - eventProps.end = null; - } - - if (!eventProps.end) { - if (t.options.forceEventDuration) { - eventProps.end = t.getDefaultEventEnd(eventProps.allDay, eventProps.start); - } - else { - eventProps.end = null; - } - } - } - - - // Ensures the allDay property exists and the timeliness of the start/end dates are consistent - function normalizeEventTimes(eventProps) { - if (eventProps.allDay == null) { - eventProps.allDay = !(eventProps.start.hasTime() || (eventProps.end && eventProps.end.hasTime())); - } - - if (eventProps.allDay) { - eventProps.start.stripTime(); - if (eventProps.end) { - // TODO: consider nextDayThreshold here? If so, will require a lot of testing and adjustment - eventProps.end.stripTime(); - } - } - else { - if (!eventProps.start.hasTime()) { - eventProps.start = t.applyTimezone(eventProps.start.time(0)); // will assign a 00:00 time - } - if (eventProps.end && !eventProps.end.hasTime()) { - eventProps.end = t.applyTimezone(eventProps.end.time(0)); // will assign a 00:00 time - } - } - } - - - // If the given event is a recurring event, break it down into an array of individual instances. - // If not a recurring event, return an array with the single original event. - // If given a falsy input (probably because of a failed buildEventFromInput call), returns an empty array. - // HACK: can override the recurring window by providing custom rangeStart/rangeEnd (for businessHours). - function expandEvent(abstractEvent, _rangeStart, _rangeEnd) { - var events = []; - var dowHash; - var dow; - var i; - var date; - var startTime, endTime; - var start, end; - var event; - - _rangeStart = _rangeStart || rangeStart; - _rangeEnd = _rangeEnd || rangeEnd; - - if (abstractEvent) { - if (abstractEvent._recurring) { - - // make a boolean hash as to whether the event occurs on each day-of-week - if ((dow = abstractEvent.dow)) { - dowHash = {}; - for (i = 0; i < dow.length; i++) { - dowHash[dow[i]] = true; - } - } - - // iterate through every day in the current range - date = _rangeStart.clone().stripTime(); // holds the date of the current day - while (date.isBefore(_rangeEnd)) { - - if (!dowHash || dowHash[date.day()]) { // if everyday, or this particular day-of-week - - startTime = abstractEvent.start; // the stored start and end properties are times (Durations) - endTime = abstractEvent.end; // " - start = date.clone(); - end = null; - - if (startTime) { - start = start.time(startTime); - } - if (endTime) { - end = date.clone().time(endTime); - } - - event = $.extend({}, abstractEvent); // make a copy of the original - assignDatesToEvent( - start, end, - !startTime && !endTime, // allDay? - event - ); - events.push(event); - } - - date.add(1, 'days'); - } - } - else { - events.push(abstractEvent); // return the original event. will be a one-item array - } - } - - return events; - } - t.expandEvent = expandEvent; - - - - /* Event Modification Math - -----------------------------------------------------------------------------------------*/ - - - // Modifies an event and all related events by applying the given properties. - // Special date-diffing logic is used for manipulation of dates. - // If `props` does not contain start/end dates, the updated values are assumed to be the event's current start/end. - // All date comparisons are done against the event's pristine _start and _end dates. - // Returns an object with delta information and a function to undo all operations. - // For making computations in a granularity greater than day/time, specify largeUnit. - // NOTE: The given `newProps` might be mutated for normalization purposes. - function mutateEvent(event, newProps, largeUnit) { - var miscProps = {}; - var oldProps; - var clearEnd; - var startDelta; - var endDelta; - var durationDelta; - var undoFunc; - - // diffs the dates in the appropriate way, returning a duration - function diffDates(date1, date0) { // date1 - date0 - if (largeUnit) { - return diffByUnit(date1, date0, largeUnit); - } - else if (newProps.allDay) { - return diffDay(date1, date0); - } - else { - return diffDayTime(date1, date0); - } - } - - newProps = newProps || {}; - - // normalize new date-related properties - if (!newProps.start) { - newProps.start = event.start.clone(); - } - if (newProps.end === undefined) { - newProps.end = event.end ? event.end.clone() : null; - } - if (newProps.allDay == null) { // is null or undefined? - newProps.allDay = event.allDay; - } - normalizeEventDates(newProps); - - // create normalized versions of the original props to compare against - // need a real end value, for diffing - oldProps = { - start: event._start.clone(), - end: event._end ? event._end.clone() : t.getDefaultEventEnd(event._allDay, event._start), - allDay: newProps.allDay // normalize the dates in the same regard as the new properties - }; - normalizeEventDates(oldProps); - - // need to clear the end date if explicitly changed to null - clearEnd = event._end !== null && newProps.end === null; - - // compute the delta for moving the start date - startDelta = diffDates(newProps.start, oldProps.start); - - // compute the delta for moving the end date - if (newProps.end) { - endDelta = diffDates(newProps.end, oldProps.end); - durationDelta = endDelta.subtract(startDelta); - } - else { - durationDelta = null; - } - - // gather all non-date-related properties - $.each(newProps, function(name, val) { - if (isMiscEventPropName(name)) { - if (val !== undefined) { - miscProps[name] = val; - } - } - }); - - // apply the operations to the event and all related events - undoFunc = mutateEvents( - clientEvents(event._id), // get events with this ID - clearEnd, - newProps.allDay, - startDelta, - durationDelta, - miscProps - ); - - return { - dateDelta: startDelta, - durationDelta: durationDelta, - undo: undoFunc - }; - } - - - // Modifies an array of events in the following ways (operations are in order): - // - clear the event's `end` - // - convert the event to allDay - // - add `dateDelta` to the start and end - // - add `durationDelta` to the event's duration - // - assign `miscProps` to the event - // - // Returns a function that can be called to undo all the operations. - // - // TODO: don't use so many closures. possible memory issues when lots of events with same ID. - // - function mutateEvents(events, clearEnd, allDay, dateDelta, durationDelta, miscProps) { - var isAmbigTimezone = t.getIsAmbigTimezone(); - var undoFunctions = []; - - // normalize zero-length deltas to be null - if (dateDelta && !dateDelta.valueOf()) { dateDelta = null; } - if (durationDelta && !durationDelta.valueOf()) { durationDelta = null; } - - $.each(events, function(i, event) { - var oldProps; - var newProps; - - // build an object holding all the old values, both date-related and misc. - // for the undo function. - oldProps = { - start: event.start.clone(), - end: event.end ? event.end.clone() : null, - allDay: event.allDay - }; - $.each(miscProps, function(name) { - oldProps[name] = event[name]; - }); - - // new date-related properties. work off the original date snapshot. - // ok to use references because they will be thrown away when backupEventDates is called. - newProps = { - start: event._start, - end: event._end, - allDay: allDay // normalize the dates in the same regard as the new properties - }; - normalizeEventDates(newProps); // massages start/end/allDay - - // strip or ensure the end date - if (clearEnd) { - newProps.end = null; - } - else if (durationDelta && !newProps.end) { // the duration translation requires an end date - newProps.end = t.getDefaultEventEnd(newProps.allDay, newProps.start); - } - - if (dateDelta) { - newProps.start.add(dateDelta); - if (newProps.end) { - newProps.end.add(dateDelta); - } - } - - if (durationDelta) { - newProps.end.add(durationDelta); // end already ensured above - } - - // if the dates have changed, and we know it is impossible to recompute the - // timezone offsets, strip the zone. - if ( - isAmbigTimezone && - !newProps.allDay && - (dateDelta || durationDelta) - ) { - newProps.start.stripZone(); - if (newProps.end) { - newProps.end.stripZone(); - } - } - - $.extend(event, miscProps, newProps); // copy over misc props, then date-related props - backupEventDates(event); // regenerate internal _start/_end/_allDay - - undoFunctions.push(function() { - $.extend(event, oldProps); - backupEventDates(event); // regenerate internal _start/_end/_allDay - }); - }); - - return function() { - for (var i = 0; i < undoFunctions.length; i++) { - undoFunctions[i](); - } - }; - } - - - t.getEventCache = function() { - return cache; - }; - -} - - -// hook for external libs to manipulate event properties upon creation. -// should manipulate the event in-place. -Calendar.prototype.normalizeEvent = function(event) { -}; - - -// Does the given span (start, end, and other location information) -// fully contain the other? -Calendar.prototype.spanContainsSpan = function(outerSpan, innerSpan) { - var eventStart = outerSpan.start.clone().stripZone(); - var eventEnd = this.getEventEnd(outerSpan).stripZone(); - - return innerSpan.start >= eventStart && innerSpan.end <= eventEnd; -}; - - -// 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; -}; - - -// 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; -} - - -/* Overlapping / Constraining ------------------------------------------------------------------------------------------*/ - - -// Determines if the given event can be relocated to the given span (unzoned start/end with other misc data) -Calendar.prototype.isEventSpanAllowed = function(span, event) { - var source = event.source || {}; - - var constraint = firstDefined( - event.constraint, - source.constraint, - this.options.eventConstraint - ); - - var overlap = firstDefined( - event.overlap, - source.overlap, - this.options.eventOverlap - ); - - return this.isSpanAllowed(span, constraint, overlap, event) && - (!this.options.eventAllow || this.options.eventAllow(span, event) !== false); -}; - - -// Determines if an external event can be relocated to the given span (unzoned start/end with other misc data) -Calendar.prototype.isExternalSpanAllowed = function(eventSpan, eventLocation, eventProps) { - var eventInput; - var event; - - // note: very similar logic is in View's reportExternalDrop - if (eventProps) { - eventInput = $.extend({}, eventProps, eventLocation); - event = this.expandEvent( - this.buildEventFromInput(eventInput) - )[0]; - } - - if (event) { - return this.isEventSpanAllowed(eventSpan, event); - } - else { // treat it as a selection - - return this.isSelectionSpanAllowed(eventSpan); - } -}; - - -// Determines the given span (unzoned start/end with other misc data) can be selected. -Calendar.prototype.isSelectionSpanAllowed = function(span) { - return this.isSpanAllowed(span, this.options.selectConstraint, this.options.selectOverlap) && - (!this.options.selectAllow || this.options.selectAllow(span) !== false); -}; - - -// Returns true if the given span (caused by an event drop/resize or a selection) is allowed to exist -// according to the constraint/overlap settings. -// `event` is not required if checking a selection. -Calendar.prototype.isSpanAllowed = function(span, constraint, overlap, event) { - var constraintEvents; - var anyContainment; - var peerEvents; - var i, peerEvent; - var peerOverlap; - - // the range must be fully contained by at least one of produced constraint events - if (constraint != null) { - - // not treated as an event! intermediate data structure - // TODO: use ranges in the future - constraintEvents = this.constraintToEvents(constraint); - if (constraintEvents) { // not invalid - - anyContainment = false; - for (i = 0; i < constraintEvents.length; i++) { - if (this.spanContainsSpan(constraintEvents[i], span)) { - anyContainment = true; - break; - } - } - - if (!anyContainment) { - return false; - } - } - } - - peerEvents = this.getPeerEvents(span, event); - - for (i = 0; i < peerEvents.length; i++) { - peerEvent = peerEvents[i]; - - // there needs to be an actual intersection before disallowing anything - if (this.eventIntersectsRange(peerEvent, span)) { - - // evaluate overlap for the given range and short-circuit if necessary - if (overlap === false) { - return false; - } - // if the event's overlap is a test function, pass the peer event in question as the first param - else if (typeof overlap === 'function' && !overlap(peerEvent, event)) { - return false; - } - - // if we are computing if the given range is allowable for an event, consider the other event's - // EventObject-specific or Source-specific `overlap` property - if (event) { - peerOverlap = firstDefined( - peerEvent.overlap, - (peerEvent.source || {}).overlap - // we already considered the global `eventOverlap` - ); - if (peerOverlap === false) { - return false; - } - // if the peer event's overlap is a test function, pass the subject event as the first param - if (typeof peerOverlap === 'function' && !peerOverlap(event, peerEvent)) { - return false; - } - } - } - } - - return true; -}; - - -// Given an event input from the API, produces an array of event objects. Possible event inputs: -// 'businessHours' -// An event ID (number or string) -// An object with specific start/end dates or a recurring event (like what businessHours accepts) -Calendar.prototype.constraintToEvents = function(constraintInput) { - - if (constraintInput === 'businessHours') { - return this.getCurrentBusinessHourEvents(); - } - - if (typeof constraintInput === 'object') { - if (constraintInput.start != null) { // needs to be event-like input - return this.expandEvent(this.buildEventFromInput(constraintInput)); - } - else { - return null; // invalid - } - } - - return this.clientEvents(constraintInput); // probably an ID -}; - - -// Does the event's date range intersect with the given range? -// start/end already assumed to have stripped zones :( -Calendar.prototype.eventIntersectsRange = function(event, range) { - var eventStart = event.start.clone().stripZone(); - var eventEnd = this.getEventEnd(event).stripZone(); - - return range.start < eventEnd && range.end > eventStart; -}; - - -/* Business Hours ------------------------------------------------------------------------------------------*/ - -var BUSINESS_HOUR_EVENT_DEFAULTS = { - id: '_fcBusinessHours', // will relate events from different calls to expandEvent - start: '09:00', - end: '17:00', - dow: [ 1, 2, 3, 4, 5 ], // monday - friday - rendering: 'inverse-background' - // classNames are defined in businessHoursSegClasses -}; - -// Return events objects for business hours within the current view. -// Abuse of our event system :( -Calendar.prototype.getCurrentBusinessHourEvents = function(wholeDay) { - return this.computeBusinessHourEvents(wholeDay, this.options.businessHours); -}; - -// Given a raw input value from options, return events objects for business hours within the current view. -Calendar.prototype.computeBusinessHourEvents = function(wholeDay, input) { - if (input === true) { - return this.expandBusinessHourEvents(wholeDay, [ {} ]); - } - else if ($.isPlainObject(input)) { - return this.expandBusinessHourEvents(wholeDay, [ input ]); - } - else if ($.isArray(input)) { - return this.expandBusinessHourEvents(wholeDay, input, true); - } - else { - return []; - } -}; - -// inputs expected to be an array of objects. -// if ignoreNoDow is true, will ignore entries that don't specify a day-of-week (dow) key. -Calendar.prototype.expandBusinessHourEvents = function(wholeDay, inputs, ignoreNoDow) { - var view = this.getView(); - var events = []; - var i, input; - - for (i = 0; i < inputs.length; i++) { - input = inputs[i]; - - if (ignoreNoDow && !input.dow) { - continue; - } - - // give defaults. will make a copy - input = $.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS, input); - - // if a whole-day series is requested, clear the start/end times - if (wholeDay) { - input.start = null; - input.end = null; - } - - events.push.apply(events, // append - this.expandEvent( - this.buildEventFromInput(input), - view.start, - view.end - ) - ); - } - - return events; -}; - -;; - -/* 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. - -var BasicView = FC.BasicView = View.extend({ - - scroller: null, - - dayGridClass: DayGrid, // class the dayGrid will be instantiated from (overridable by subclasses) - dayGrid: null, // the main subcomponent that does most of the heavy lifting - - dayNumbersVisible: false, // display day numbers on each day cell? - colWeekNumbersVisible: false, // display week numbers along the side? - cellWeekNumbersVisible: false, // display week numbers in day cell? - - weekNumberWidth: null, // width of all the week-number cells running down the side - - headContainerEl: null, // div that hold's the dayGrid's rendered date header - headRowEl: null, // the fake row element of the day-of-week header - - - initialize: function() { - this.dayGrid = this.instantiateDayGrid(); - - this.scroller = new Scroller({ - overflowX: 'hidden', - overflowY: 'auto' - }); - }, - - - // 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); - - 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 - - this.dayGrid.breakOnWeeks = /year|month|week/.test(this.intervalUnit); // do before setRange - this.dayGrid.setRange(range); - }, - - - // 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 - - // 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 - } - } - - return range; - }, - - - // Renders the view into `this.el`, which should already be assigned - renderDates: function() { - - this.dayNumbersVisible = this.dayGrid.rowCnt > 1; // TODO: make grid responsible - if (this.opt('weekNumbers')) { - if (this.opt('weekNumbersWithinDays')) { - this.cellWeekNumbersVisible = true; - this.colWeekNumbersVisible = false; - } - else { - this.cellWeekNumbersVisible = false; - this.colWeekNumbersVisible = true; - }; - } - this.dayGrid.numbersVisible = this.dayNumbersVisible || - this.cellWeekNumbersVisible || this.colWeekNumbersVisible; - - 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 = $('<div class="fc-day-grid" />').appendTo(dayGridContainerEl); - this.el.find('.fc-body > tr > td').append(dayGridContainerEl); - - this.dayGrid.setElement(dayGridEl); - this.dayGrid.renderDates(this.hasRigidRows()); - }, - - - // 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'); - }, - - - // 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(); - }, - - - renderBusinessHours: function() { - this.dayGrid.renderBusinessHours(); - }, - - - unrenderBusinessHours: function() { - this.dayGrid.unrenderBusinessHours(); - }, - - - // Builds the HTML skeleton for the view. - // The day-grid component will render inside of a container defined by this HTML. - renderSkeletonHtml: function() { - return '' + - '<table>' + - '<thead class="fc-head">' + - '<tr>' + - '<td class="fc-head-container ' + this.widgetHeaderClass + '"></td>' + - '</tr>' + - '</thead>' + - '<tbody class="fc-body">' + - '<tr>' + - '<td class="' + this.widgetContentClass + '"></td>' + - '</tr>' + - '</tbody>' + - '</table>'; - }, - - - // 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 ''; - }, - - - // Determines whether each row should have a constant height - hasRigidRows: function() { - var eventLimit = this.opt('eventLimit'); - return eventLimit && typeof eventLimit !== 'number'; - }, - - - /* Dimensions - ------------------------------------------------------------------------------------------------------------------*/ - - - // Refreshes the horizontal dimensions of the view - updateWidth: function() { - if (this.colWeekNumbersVisible) { - // 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') - ); - } - }, - - - // Adjusts the vertical dimensions of the view to the specified values - setHeight: function(totalHeight, isAuto) { - var eventLimit = this.opt('eventLimit'); - var scrollerHeight; - var scrollbarWidths; - - // reset all heights to be natural - this.scroller.clear(); - uncompensateScroll(this.headRowEl); - - this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed - - // 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 - } - - // distribute the height to the rows - // (totalHeight is a "recommended" value if isAuto) - scrollerHeight = this.computeScrollerHeight(totalHeight); - this.setGridHeight(scrollerHeight, isAuto); - - // 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 (!isAuto) { // should we force dimensions of the scroll container? - - this.scroller.setHeight(scrollerHeight); - scrollbarWidths = this.scroller.getScrollbarWidths(); - - if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars? - - 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); - } - - // 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 - }, - - - // 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 - } - }, - - - /* Scroll - ------------------------------------------------------------------------------------------------------------------*/ - - - queryScroll: function() { - return this.scroller.getScrollTop(); - }, - - - setScroll: function(top) { - this.scroller.setScrollTop(top); - }, - - - /* Hit Areas - ------------------------------------------------------------------------------------------------------------------*/ - // forward all hit-related method calls to dayGrid - - - prepareHits: function() { - this.dayGrid.prepareHits(); - }, - - - releaseHits: function() { - this.dayGrid.releaseHits(); - }, - - - queryHit: function(left, top) { - return this.dayGrid.queryHit(left, top); - }, - - - getHitSpan: function(hit) { - return this.dayGrid.getHitSpan(hit); - }, - - - getHitEl: function(hit) { - return this.dayGrid.getHitEl(hit); - }, - - - /* Events - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders the given events onto the view and populates the segments array - renderEvents: function(events) { - this.dayGrid.renderEvents(events); - - this.updateHeight(); // must compensate for events that overflow the row - }, - - - // Retrieves all segment objects that are rendered in the view - getEventSegs: function() { - return this.dayGrid.getEventSegs(); - }, - - - // Unrenders all event elements and clears internal segment data - unrenderEvents: function() { - this.dayGrid.unrenderEvents(); - - // we DON'T need to call updateHeight() because - // a renderEvents() call always happens after this, which will eventually call updateHeight() - }, - - - /* Dragging (for both events and external elements) - ------------------------------------------------------------------------------------------------------------------*/ - - - // A returned value of `true` signals that a mock "helper" event has been rendered. - renderDrag: function(dropLocation, seg) { - return this.dayGrid.renderDrag(dropLocation, seg); - }, - - - unrenderDrag: function() { - this.dayGrid.unrenderDrag(); - }, - - - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - // 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; - - if (view.colWeekNumbersVisible) { - return '' + - '<th class="fc-week-number ' + view.widgetHeaderClass + '" ' + view.weekNumberStyleAttr() + '>' + - '<span>' + // needed for matchCellWidths - htmlEscape(view.opt('weekNumberTitle')) + - '</span>' + - '</th>'; - } - - return ''; - }, - - - // Generates the HTML that will go before content-skeleton cells that display the day/week numbers - renderNumberIntroHtml: function(row) { - var view = this.view; - var weekStart = this.getCellDate(row, 0); - - if (view.colWeekNumbersVisible) { - return '' + - '<td class="fc-week-number" ' + view.weekNumberStyleAttr() + '>' + - view.buildGotoAnchorHtml( // aside from link, important for matchCellWidths - { date: weekStart, type: 'week', forceOff: this.colCnt === 1 }, - weekStart.format('w') // inner HTML - ) + - '</td>'; - } - - return ''; - }, - - - // Generates the HTML that goes before the day bg cells for each day-row - renderBgIntroHtml: function() { - var view = this.view; - - if (view.colWeekNumbersVisible) { - return '<td class="fc-week-number ' + view.widgetContentClass + '" ' + - view.weekNumberStyleAttr() + '></td>'; - } - - 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.colWeekNumbersVisible) { - return '<td class="fc-week-number" ' + view.weekNumberStyleAttr() + '></td>'; - } - - 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; - - // 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'); - } - - return range; - }, - - - // Overrides the default BasicView behavior to have special multi-week auto-height logic - setGridHeight: function(height, isAuto) { - - // 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; - } - - distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows - }, - - - isFixedWeeks: function() { - return this.opt('fixedWeekCount'); - } - -}); - -;; - -fcViews.basic = { - 'class': BasicView -}; - -fcViews.basicDay = { - type: 'basic', - duration: { days: 1 } -}; - -fcViews.basicWeek = { - type: 'basic', - duration: { weeks: 1 } -}; - -fcViews.month = { - 'class': MonthView, - duration: { months: 1 }, // important for prev/next - defaults: { - fixedWeekCount: true - } -}; -;; - -/* 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. - -var AgendaView = FC.AgendaView = View.extend({ - - scroller: null, - - timeGridClass: TimeGrid, // class used to instantiate the timeGrid. subclasses can override - timeGrid: null, // the main time-grid subcomponent of this view - - 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 - - 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 - - // when the time-grid isn't tall enough to occupy the given height, we render an <hr> underneath - bottomRuleEl: null, - - - 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 - } - - this.scroller = new Scroller({ - overflowX: 'hidden', - overflowY: 'auto' - }); - }, - - - // Instantiates the TimeGrid object this view needs. Draws from this.timeGridClass - instantiateTimeGrid: function() { - var subclass = this.timeGridClass.extend(agendaTimeGridMethods); - - return new subclass(this); - }, - - - // Instantiates the DayGrid object this view might need. Draws from this.dayGridClass - instantiateDayGrid: function() { - var subclass = this.dayGridClass.extend(agendaDayGridMethods); - - return new subclass(this); - }, - - - /* Rendering - ------------------------------------------------------------------------------------------------------------------*/ - - - // Sets the display range and computes all necessary dates - setRange: function(range) { - View.prototype.setRange.call(this, range); // call the super-method - - this.timeGrid.setRange(range); - if (this.dayGrid) { - this.dayGrid.setRange(range); - } - }, - - - // Renders the view into `this.el`, which has already been assigned - renderDates: function() { - - 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 = $('<div class="fc-time-grid" />').appendTo(timeGridWrapEl); - this.el.find('.fc-body > tr > td').append(timeGridWrapEl); - - this.timeGrid.setElement(timeGridEl); - this.timeGrid.renderDates(); - - // the <hr> that sometimes displays under the time-grid - this.bottomRuleEl = $('<hr class="fc-divider ' + this.widgetHeaderClass + '"/>') - .appendTo(this.timeGrid.el); // inject it into the time-grid - - 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 <hr> dividing the two grids - this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight(); - } - - this.noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller - }, - - - // render the day-of-week headers - renderHead: function() { - this.headContainerEl = - this.el.find('.fc-head-container') - .html(this.timeGrid.renderHeadHtml()); - }, - - - // 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(); - } - - this.scroller.destroy(); - }, - - - // 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 '' + - '<table>' + - '<thead class="fc-head">' + - '<tr>' + - '<td class="fc-head-container ' + this.widgetHeaderClass + '"></td>' + - '</tr>' + - '</thead>' + - '<tbody class="fc-body">' + - '<tr>' + - '<td class="' + this.widgetContentClass + '">' + - (this.dayGrid ? - '<div class="fc-day-grid"/>' + - '<hr class="fc-divider ' + this.widgetHeaderClass + '"/>' : - '' - ) + - '</td>' + - '</tr>' + - '</tbody>' + - '</table>'; - }, - - - // 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 ''; - }, - - - /* Business Hours - ------------------------------------------------------------------------------------------------------------------*/ - - - renderBusinessHours: function() { - this.timeGrid.renderBusinessHours(); - - if (this.dayGrid) { - this.dayGrid.renderBusinessHours(); - } - }, - - - unrenderBusinessHours: function() { - this.timeGrid.unrenderBusinessHours(); - - if (this.dayGrid) { - this.dayGrid.unrenderBusinessHours(); - } - }, - - - /* Now Indicator - ------------------------------------------------------------------------------------------------------------------*/ - - - getNowIndicatorUnit: function() { - return this.timeGrid.getNowIndicatorUnit(); - }, - - - renderNowIndicator: function(date) { - this.timeGrid.renderNowIndicator(date); - }, - - - unrenderNowIndicator: function() { - this.timeGrid.unrenderNowIndicator(); - }, - - - /* Dimensions - ------------------------------------------------------------------------------------------------------------------*/ - - - updateSize: function(isResize) { - this.timeGrid.updateSize(isResize); - - View.prototype.updateSize.call(this, isResize); // call the super-method - }, - - - // 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')); - }, - - - // Adjusts the vertical dimensions of the view to the specified values - setHeight: function(totalHeight, isAuto) { - var eventLimit; - var scrollerHeight; - var scrollbarWidths; - - // reset all dimensions back to the original state - this.bottomRuleEl.hide(); // .show() will be called later if this <hr> 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 - - 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 (!isAuto) { // should we force dimensions of the scroll container? - - scrollerHeight = this.computeScrollerHeight(totalHeight); - this.scroller.setHeight(scrollerHeight); - scrollbarWidths = this.scroller.getScrollbarWidths(); - - if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars? - - // make the all-day and header rows lines up - compensateScroll(this.noScrollRowEls, scrollbarWidths); - - // 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); - - // 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(); - } - } - }, - - - // 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 - ------------------------------------------------------------------------------------------------------------------*/ - - - // 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); - - // 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 - } - - return top; - }, - - - queryScroll: function() { - return this.scroller.getScrollTop(); - }, - - - setScroll: function(top) { - this.scroller.setScrollTop(top); - }, - - - /* Hit Areas - ------------------------------------------------------------------------------------------------------------------*/ - // forward all hit-related method calls to the grids (dayGrid might not be defined) - - - prepareHits: function() { - this.timeGrid.prepareHits(); - if (this.dayGrid) { - this.dayGrid.prepareHits(); - } - }, - - - releaseHits: function() { - this.timeGrid.releaseHits(); - if (this.dayGrid) { - this.dayGrid.releaseHits(); - } - }, - - - queryHit: function(left, top) { - var hit = this.timeGrid.queryHit(left, top); - - if (!hit && this.dayGrid) { - hit = this.dayGrid.queryHit(left, top); - } - - return hit; - }, - - - getHitSpan: function(hit) { - // TODO: hit.component is set as a hack to identify where the hit came from - return hit.component.getHitSpan(hit); - }, - - - getHitEl: function(hit) { - // TODO: hit.component is set as a hack to identify where the hit came from - return hit.component.getHitEl(hit); - }, - - - /* Events - ------------------------------------------------------------------------------------------------------------------*/ - - - // 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; - - // 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); - } - - // the all-day area is flexible and might have a lot of events, so shift the height - this.updateHeight(); - }, - - - // Retrieves all segment objects that are rendered in the view - getEventSegs: function() { - return this.timeGrid.getEventSegs().concat( - this.dayGrid ? this.dayGrid.getEventSegs() : [] - ); - }, - - - // 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(); - } - - // we DON'T need to call updateHeight() because - // a renderEvents() call always happens after this, which will eventually call updateHeight() - }, - - - /* Dragging (for events and external elements) - ------------------------------------------------------------------------------------------------------------------*/ - - - // 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); - } - }, - - - unrenderDrag: function() { - this.timeGrid.unrenderDrag(); - if (this.dayGrid) { - this.dayGrid.unrenderDrag(); - } - }, - - - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - // 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')); - - return '' + - '<th class="fc-axis fc-week-number ' + view.widgetHeaderClass + '" ' + view.axisStyleAttr() + '>' + - view.buildGotoAnchorHtml( // aside from link, important for matchCellWidths - { date: this.start, type: 'week', forceOff: this.colCnt > 1 }, - htmlEscape(weekText) // inner HTML - ) + - '</th>'; - } - else { - return '<th class="fc-axis ' + view.widgetHeaderClass + '" ' + view.axisStyleAttr() + '></th>'; - } - }, - - - // Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column. - renderBgIntroHtml: function() { - var view = this.view; - - return '<td class="fc-axis ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '></td>'; - }, - - - // 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; - - return '<td class="fc-axis" ' + view.axisStyleAttr() + '></td>'; - } - -}; - - -// Methods that will customize the rendering behavior of the AgendaView's dayGrid -var agendaDayGridMethods = { - - - // Generates the HTML that goes before the all-day cells - renderBgIntroHtml: function() { - var view = this.view; - - return '' + - '<td class="fc-axis ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' + - '<span>' + // needed for matchCellWidths - view.getAllDayHtml() + - '</span>' + - '</td>'; - }, - - - // 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; - - return '<td class="fc-axis" ' + view.axisStyleAttr() + '></td>'; - } - -}; - -;; - -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, - 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.agendaWeek = { - type: 'agenda', - duration: { weeks: 1 } -}; -;; - -/* -Responsible for the scroller, and forwarding event-related actions into the "grid" -*/ -var ListView = View.extend({ - - grid: null, - scroller: null, - - initialize: function() { - this.grid = new ListViewGrid(this); - this.scroller = new Scroller({ - overflowX: 'hidden', - overflowY: 'auto' - }); - }, - - setRange: function(range) { - View.prototype.setRange.call(this, range); // super - - this.grid.setRange(range); // needs to process range-related options - }, - - renderSkeleton: function() { - this.el.addClass( - 'fc-list-view ' + - this.widgetContentClass - ); - - this.scroller.render(); - this.scroller.el.appendTo(this.el); - - this.grid.setElement(this.scroller.scrollEl); - }, - - unrenderSkeleton: function() { - this.scroller.destroy(); // will remove the Grid too - }, - - setHeight: function(totalHeight, isAuto) { - this.scroller.setHeight(this.computeScrollerHeight(totalHeight)); - }, - - computeScrollerHeight: function(totalHeight) { - return totalHeight - - subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller - }, - - renderEvents: function(events) { - this.grid.renderEvents(events); - }, - - unrenderEvents: function() { - this.grid.unrenderEvents(); - }, - - isEventResizable: function(event) { - return false; - }, - - isEventDraggable: function(event) { - return false; - } - -}); - -/* -Responsible for event rendering and user-interaction. -Its "el" is the inner-content of the above view's scroller. -*/ -var ListViewGrid = Grid.extend({ - - segSelector: '.fc-list-item', // which elements accept event actions - hasDayInteractions: false, // no day selection or day clicking - - // slices by day - spanToSegs: function(span) { - var view = this.view; - var dayStart = view.start.clone().time(0); // timed, so segs get times! - var dayIndex = 0; - var seg; - var segs = []; - - while (dayStart < view.end) { - - seg = intersectRanges(span, { - start: dayStart, - end: dayStart.clone().add(1, 'day') - }); - - if (seg) { - seg.dayIndex = dayIndex; - segs.push(seg); - } - - dayStart.add(1, 'day'); - dayIndex++; - - // detect when span won't go fully into the next day, - // and mutate the latest seg to the be the end. - if ( - seg && !seg.isEnd && span.end.hasTime() && - span.end < dayStart.clone().add(this.view.nextDayThreshold) - ) { - seg.end = span.end.clone(); - seg.isEnd = true; - break; - } - } - - return segs; - }, - - // like "4:00am" - computeEventTimeFormat: function() { - return this.view.opt('mediumTimeFormat'); - }, - - // for events with a url, the whole <tr> should be clickable, - // but it's impossible to wrap with an <a> tag. simulate this. - handleSegClick: function(seg, ev) { - var url; - - Grid.prototype.handleSegClick.apply(this, arguments); // super. might prevent the default action - - // not clicking on or within an <a> with an href - if (!$(ev.target).closest('a[href]').length) { - url = seg.event.url; - if (url && !ev.isDefaultPrevented()) { // jsEvent not cancelled in handler - window.location.href = url; // simulate link click - } - } - }, - - // returns list of foreground segs that were actually rendered - renderFgSegs: function(segs) { - segs = this.renderFgSegEls(segs); // might filter away hidden events - - if (!segs.length) { - this.renderEmptyMessage(); - } - else { - this.renderSegList(segs); - } - - return segs; - }, - - renderEmptyMessage: function() { - this.el.html( - '<div class="fc-list-empty-wrap2">' + // TODO: try less wraps - '<div class="fc-list-empty-wrap1">' + - '<div class="fc-list-empty">' + - htmlEscape(this.view.opt('noEventsMessage')) + - '</div>' + - '</div>' + - '</div>' - ); - }, - - // render the event segments in the view - renderSegList: function(allSegs) { - var segsByDay = this.groupSegsByDay(allSegs); // sparse array - var dayIndex; - var daySegs; - var i; - var tableEl = $('<table class="fc-list-table"><tbody/></table>'); - var tbodyEl = tableEl.find('tbody'); - - for (dayIndex = 0; dayIndex < segsByDay.length; dayIndex++) { - daySegs = segsByDay[dayIndex]; - if (daySegs) { // sparse array, so might be undefined - - // append a day header - tbodyEl.append(this.dayHeaderHtml( - this.view.start.clone().add(dayIndex, 'days') - )); - - this.sortEventSegs(daySegs); - - for (i = 0; i < daySegs.length; i++) { - tbodyEl.append(daySegs[i].el); // append event row - } - } - } - - this.el.empty().append(tableEl); - }, - - // Returns a sparse array of arrays, segs grouped by their dayIndex - groupSegsByDay: function(segs) { - var segsByDay = []; // sparse array - var i, seg; - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - (segsByDay[seg.dayIndex] || (segsByDay[seg.dayIndex] = [])) - .push(seg); - } - - return segsByDay; - }, - - // generates the HTML for the day headers that live amongst the event rows - dayHeaderHtml: function(dayDate) { - var view = this.view; - var mainFormat = view.opt('listDayFormat'); - var altFormat = view.opt('listDayAltFormat'); - - return '<tr class="fc-list-heading" data-date="' + dayDate.format('YYYY-MM-DD') + '">' + - '<td class="' + view.widgetHeaderClass + '" colspan="3">' + - (mainFormat ? - view.buildGotoAnchorHtml( - dayDate, - { 'class': 'fc-list-heading-main' }, - htmlEscape(dayDate.format(mainFormat)) // inner HTML - ) : - '') + - (altFormat ? - view.buildGotoAnchorHtml( - dayDate, - { 'class': 'fc-list-heading-alt' }, - htmlEscape(dayDate.format(altFormat)) // inner HTML - ) : - '') + - '</td>' + - '</tr>'; - }, - - // generates the HTML for a single event row - fgSegHtml: function(seg) { - var view = this.view; - var classes = [ 'fc-list-item' ].concat(this.getSegCustomClasses(seg)); - var bgColor = this.getSegBackgroundColor(seg); - var event = seg.event; - var url = event.url; - var timeHtml; - - if (event.allDay) { - timeHtml = view.getAllDayHtml(); - } - else if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day - if (seg.isStart || seg.isEnd) { // outer segment that probably lasts part of the day - timeHtml = htmlEscape(this.getEventTimeText(seg)); - } - else { // inner segment that lasts the whole day - timeHtml = view.getAllDayHtml(); - } - } - else { - // Display the normal time text for the *event's* times - timeHtml = htmlEscape(this.getEventTimeText(event)); - } - - if (url) { - classes.push('fc-has-url'); - } - - return '<tr class="' + classes.join(' ') + '">' + - (this.displayEventTime ? - '<td class="fc-list-item-time ' + view.widgetContentClass + '">' + - (timeHtml || '') + - '</td>' : - '') + - '<td class="fc-list-item-marker ' + view.widgetContentClass + '">' + - '<span class="fc-event-dot"' + - (bgColor ? - ' style="background-color:' + bgColor + '"' : - '') + - '></span>' + - '</td>' + - '<td class="fc-list-item-title ' + view.widgetContentClass + '">' + - '<a' + (url ? ' href="' + htmlEscape(url) + '"' : '') + '>' + - htmlEscape(seg.event.title || '') + - '</a>' + - '</td>' + - '</tr>'; - } - -}); - -;; - -fcViews.list = { - 'class': ListView, - buttonTextKey: 'list', // what to lookup in locale files - defaults: { - buttonText: 'list', // text to display for English - listDayFormat: 'LL', // like "January 1, 2016" - noEventsMessage: 'No events to display' - } -}; - -fcViews.listDay = { - type: 'list', - duration: { days: 1 }, - defaults: { - listDayFormat: 'dddd' // day-of-week is all we need. full date is probably in header - } -}; - -fcViews.listWeek = { - type: 'list', - duration: { weeks: 1 }, - defaults: { - listDayFormat: 'dddd', // day-of-week is more important - listDayAltFormat: 'LL' - } -}; - -fcViews.listMonth = { - type: 'list', - duration: { month: 1 }, - defaults: { - listDayAltFormat: 'dddd' // day-of-week is nice-to-have - } -}; - -fcViews.listYear = { - type: 'list', - duration: { year: 1 }, - defaults: { - listDayAltFormat: 'dddd' // day-of-week is nice-to-have - } -}; - -;; - -return FC; // export for Node/CommonJS -});
\ No newline at end of file diff --git a/addons/web/static/lib/fullcalendar/js/gcal.js b/addons/web/static/lib/fullcalendar/js/gcal.js deleted file mode 100644 index dfe6fa90591..00000000000 --- a/addons/web/static/lib/fullcalendar/js/gcal.js +++ /dev/null @@ -1,180 +0,0 @@ -/*! - * FullCalendar v3.0.1 Google Calendar Plugin - * Docs & License: http://fullcalendar.io/ - * (c) 2016 Adam Shaw - */ - -(function(factory) { - if (typeof define === 'function' && define.amd) { - define([ 'jquery' ], factory); - } - else if (typeof exports === 'object') { // Node/CommonJS - module.exports = factory(require('jquery')); - } - else { - factory(jQuery); - } -})(function($) { - - -var API_BASE = 'https://www.googleapis.com/calendar/v3/calendars'; -var FC = $.fullCalendar; -var applyAll = FC.applyAll; - - -FC.sourceNormalizers.push(function(sourceOptions) { - var googleCalendarId = sourceOptions.googleCalendarId; - var url = sourceOptions.url; - var match; - - // if the Google Calendar ID hasn't been explicitly defined - if (!googleCalendarId && url) { - - // detect if the ID was specified as a single string. - // will match calendars like "asdf1234@calendar.google.com" in addition to person email calendars. - if (/^[^\/]+@([^\/\.]+\.)*(google|googlemail|gmail)\.com$/.test(url)) { - googleCalendarId = url; - } - // try to scrape it out of a V1 or V3 API feed URL - else if ( - (match = /^https:\/\/www.googleapis.com\/calendar\/v3\/calendars\/([^\/]*)/.exec(url)) || - (match = /^https?:\/\/www.google.com\/calendar\/feeds\/([^\/]*)/.exec(url)) - ) { - googleCalendarId = decodeURIComponent(match[1]); - } - - if (googleCalendarId) { - sourceOptions.googleCalendarId = googleCalendarId; - } - } - - - if (googleCalendarId) { // is this a Google Calendar? - - // make each Google Calendar source uneditable by default - if (sourceOptions.editable == null) { - sourceOptions.editable = false; - } - - // We want removeEventSource to work, but it won't know about the googleCalendarId primitive. - // Shoehorn it into the url, which will function as the unique primitive. Won't cause side effects. - // This hack is obsolete since 2.2.3, but keep it so this plugin file is compatible with old versions. - sourceOptions.url = googleCalendarId; - } -}); - - -FC.sourceFetchers.push(function(sourceOptions, start, end, timezone) { - if (sourceOptions.googleCalendarId) { - return transformOptions(sourceOptions, start, end, timezone, this); // `this` is the calendar - } -}); - - -function transformOptions(sourceOptions, start, end, timezone, calendar) { - var url = API_BASE + '/' + encodeURIComponent(sourceOptions.googleCalendarId) + '/events?callback=?'; // jsonp - var apiKey = sourceOptions.googleCalendarApiKey || calendar.options.googleCalendarApiKey; - var success = sourceOptions.success; - var data; - var timezoneArg; // populated when a specific timezone. escaped to Google's liking - - function reportError(message, apiErrorObjs) { - var errorObjs = apiErrorObjs || [ { message: message } ]; // to be passed into error handlers - - // call error handlers - (sourceOptions.googleCalendarError || $.noop).apply(calendar, errorObjs); - (calendar.options.googleCalendarError || $.noop).apply(calendar, errorObjs); - - // print error to debug console - FC.warn.apply(null, [ message ].concat(apiErrorObjs || [])); - } - - if (!apiKey) { - reportError("Specify a googleCalendarApiKey. See http://fullcalendar.io/docs/google_calendar/"); - return {}; // an empty source to use instead. won't fetch anything. - } - - // The API expects an ISO8601 datetime with a time and timezone part. - // Since the calendar's timezone offset isn't always known, request the date in UTC and pad it by a day on each - // side, guaranteeing we will receive all events in the desired range, albeit a superset. - // .utc() will set a zone and give it a 00:00:00 time. - if (!start.hasZone()) { - start = start.clone().utc().add(-1, 'day'); - } - if (!end.hasZone()) { - end = end.clone().utc().add(1, 'day'); - } - - // when sending timezone names to Google, only accepts underscores, not spaces - if (timezone && timezone != 'local') { - timezoneArg = timezone.replace(' ', '_'); - } - - data = $.extend({}, sourceOptions.data || {}, { - key: apiKey, - timeMin: start.format(), - timeMax: end.format(), - timeZone: timezoneArg, - singleEvents: true, - maxResults: 9999 - }); - - return $.extend({}, sourceOptions, { - googleCalendarId: null, // prevents source-normalizing from happening again - url: url, - data: data, - startParam: false, // `false` omits this parameter. we already included it above - endParam: false, // same - timezoneParam: false, // same - success: function(data) { - var events = []; - var successArgs; - var successRes; - - if (data.error) { - reportError('Google Calendar API: ' + data.error.message, data.error.errors); - } - else if (data.items) { - $.each(data.items, function(i, entry) { - var url = entry.htmlLink || null; - - // make the URLs for each event show times in the correct timezone - if (timezoneArg && url !== null) { - url = injectQsComponent(url, 'ctz=' + timezoneArg); - } - - events.push({ - id: entry.id, - title: entry.summary, - start: entry.start.dateTime || entry.start.date, // try timed. will fall back to all-day - end: entry.end.dateTime || entry.end.date, // same - url: url, - location: entry.location, - description: entry.description - }); - }); - - // call the success handler(s) and allow it to return a new events array - successArgs = [ events ].concat(Array.prototype.slice.call(arguments, 1)); // forward other jq args - successRes = applyAll(success, this, successArgs); - if ($.isArray(successRes)) { - return successRes; - } - } - - return events; - } - }); -} - - -// Injects a string like "arg=value" into the querystring of a URL -function injectQsComponent(url, component) { - // inject it after the querystring but before the fragment - return url.replace(/(\?.*?)?(#|$)/, function(whole, qs, hash) { - return (qs ? qs + '&' : '?') + component + hash; - }); -} - - -}); diff --git a/addons/web/static/lib/fullcalendar/list/main.css b/addons/web/static/lib/fullcalendar/list/main.css new file mode 100644 index 00000000000..1a9b8701260 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/list/main.css @@ -0,0 +1,118 @@ +/* List View +--------------------------------------------------------------------------------------------------*/ +/* possibly reusable */ +.fc-event-dot { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 5px; +} + +/* view wrapper */ +.fc-rtl .fc-list-view { + direction: rtl; + /* unlike core views, leverage browser RTL */ +} + +.fc-list-view { + border-width: 1px; + border-style: solid; +} + +/* table resets */ +.fc .fc-list-table { + table-layout: auto; + /* for shrinkwrapping cell content */ +} + +.fc-list-table td { + border-width: 1px 0 0; + padding: 8px 14px; +} + +.fc-list-table tr:first-child td { + border-top-width: 0; +} + +/* day headings with the list */ +.fc-list-heading { + border-bottom-width: 1px; +} + +.fc-list-heading td { + font-weight: bold; +} + +.fc-ltr .fc-list-heading-main { + float: left; +} + +.fc-ltr .fc-list-heading-alt { + float: right; +} + +.fc-rtl .fc-list-heading-main { + float: right; +} + +.fc-rtl .fc-list-heading-alt { + float: left; +} + +/* event list items */ +.fc-list-item.fc-has-url { + cursor: pointer; + /* whole row will be clickable */ +} + +.fc-list-item-marker, +.fc-list-item-time { + white-space: nowrap; + width: 1px; +} + +/* make the dot closer to the event title */ +.fc-ltr .fc-list-item-marker { + padding-right: 0; +} + +.fc-rtl .fc-list-item-marker { + padding-left: 0; +} + +.fc-list-item-title a { + /* every event title cell has an <a> tag */ + text-decoration: none; + color: inherit; +} + +.fc-list-item-title a[href]:hover { + /* hover effect only on titles with hrefs */ + text-decoration: underline; +} + +/* message when no events */ +.fc-list-empty-wrap2 { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +.fc-list-empty-wrap1 { + width: 100%; + height: 100%; + display: table; +} + +.fc-list-empty { + display: table-cell; + vertical-align: middle; + text-align: center; +} + +.fc-unthemed .fc-list-empty { + /* theme will provide own background */ + background-color: #eee; +}
\ No newline at end of file diff --git a/addons/web/static/lib/fullcalendar/list/main.js b/addons/web/static/lib/fullcalendar/list/main.js new file mode 100644 index 00000000000..1f72dee2296 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/list/main.js @@ -0,0 +1,356 @@ +/*! +FullCalendar List View Plugin v4.4.0 +Docs & License: https://fullcalendar.io/ +(c) 2019 Adam Shaw +*/ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@fullcalendar/core')) : + typeof define === 'function' && define.amd ? define(['exports', '@fullcalendar/core'], factory) : + (global = global || self, factory(global.FullCalendarList = {}, global.FullCalendar)); +}(this, function (exports, core) { 'use strict'; + + /*! *****************************************************************************
+ Copyright (c) Microsoft Corporation. All rights reserved.
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+ this file except in compliance with the License. You may obtain a copy of the
+ License at http://www.apache.org/licenses/LICENSE-2.0
+
+ THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
+ WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+ MERCHANTABLITY OR NON-INFRINGEMENT.
+
+ See the Apache Version 2.0 License for specific language governing permissions
+ and limitations under the License.
+ ***************************************************************************** */
+ /* global Reflect, Promise */
+
+ var extendStatics = function(d, b) {
+ extendStatics = Object.setPrototypeOf ||
+ ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+ function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+ return extendStatics(d, b);
+ };
+
+ function __extends(d, b) {
+ extendStatics(d, b);
+ function __() { this.constructor = d; }
+ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+ } + + var ListEventRenderer = /** @class */ (function (_super) { + __extends(ListEventRenderer, _super); + function ListEventRenderer(listView) { + var _this = _super.call(this) || this; + _this.listView = listView; + return _this; + } + ListEventRenderer.prototype.attachSegs = function (segs) { + if (!segs.length) { + this.listView.renderEmptyMessage(); + } + else { + this.listView.renderSegList(segs); + } + }; + ListEventRenderer.prototype.detachSegs = function () { + }; + // generates the HTML for a single event row + ListEventRenderer.prototype.renderSegHtml = function (seg) { + var _a = this.context, theme = _a.theme, options = _a.options; + var eventRange = seg.eventRange; + var eventDef = eventRange.def; + var eventInstance = eventRange.instance; + var eventUi = eventRange.ui; + var url = eventDef.url; + var classes = ['fc-list-item'].concat(eventUi.classNames); + var bgColor = eventUi.backgroundColor; + var timeHtml; + if (eventDef.allDay) { + timeHtml = core.getAllDayHtml(options); + } + else if (core.isMultiDayRange(eventRange.range)) { + if (seg.isStart) { + timeHtml = core.htmlEscape(this._getTimeText(eventInstance.range.start, seg.end, false // allDay + )); + } + else if (seg.isEnd) { + timeHtml = core.htmlEscape(this._getTimeText(seg.start, eventInstance.range.end, false // allDay + )); + } + else { // inner segment that lasts the whole day + timeHtml = core.getAllDayHtml(options); + } + } + else { + // Display the normal time text for the *event's* times + timeHtml = core.htmlEscape(this.getTimeText(eventRange)); + } + if (url) { + classes.push('fc-has-url'); + } + return '<tr class="' + classes.join(' ') + '">' + + (this.displayEventTime ? + '<td class="fc-list-item-time ' + theme.getClass('widgetContent') + '">' + + (timeHtml || '') + + '</td>' : + '') + + '<td class="fc-list-item-marker ' + theme.getClass('widgetContent') + '">' + + '<span class="fc-event-dot"' + + (bgColor ? + ' style="background-color:' + bgColor + '"' : + '') + + '></span>' + + '</td>' + + '<td class="fc-list-item-title ' + theme.getClass('widgetContent') + '">' + + '<a' + (url ? ' href="' + core.htmlEscape(url) + '"' : '') + '>' + + core.htmlEscape(eventDef.title || '') + + '</a>' + + '</td>' + + '</tr>'; + }; + // like "4:00am" + ListEventRenderer.prototype.computeEventTimeFormat = function () { + return { + hour: 'numeric', + minute: '2-digit', + meridiem: 'short' + }; + }; + return ListEventRenderer; + }(core.FgEventRenderer)); + + /* + Responsible for the scroller, and forwarding event-related actions into the "grid". + */ + var ListView = /** @class */ (function (_super) { + __extends(ListView, _super); + function ListView(viewSpec, parentEl) { + var _this = _super.call(this, viewSpec, parentEl) || this; + _this.computeDateVars = core.memoize(computeDateVars); + _this.eventStoreToSegs = core.memoize(_this._eventStoreToSegs); + _this.renderSkeleton = core.memoizeRendering(_this._renderSkeleton, _this._unrenderSkeleton); + var eventRenderer = _this.eventRenderer = new ListEventRenderer(_this); + _this.renderContent = core.memoizeRendering(eventRenderer.renderSegs.bind(eventRenderer), eventRenderer.unrender.bind(eventRenderer), [_this.renderSkeleton]); + return _this; + } + ListView.prototype.firstContext = function (context) { + context.calendar.registerInteractiveComponent(this, { + el: this.el + // TODO: make aware that it doesn't do Hits + }); + }; + ListView.prototype.render = function (props, context) { + _super.prototype.render.call(this, props, context); + var _a = this.computeDateVars(props.dateProfile), dayDates = _a.dayDates, dayRanges = _a.dayRanges; + this.dayDates = dayDates; + this.renderSkeleton(context); + this.renderContent(context, this.eventStoreToSegs(props.eventStore, props.eventUiBases, dayRanges)); + }; + ListView.prototype.destroy = function () { + _super.prototype.destroy.call(this); + this.renderSkeleton.unrender(); + this.renderContent.unrender(); + this.context.calendar.unregisterInteractiveComponent(this); + }; + ListView.prototype._renderSkeleton = function (context) { + var theme = context.theme; + this.el.classList.add('fc-list-view'); + var listViewClassNames = (theme.getClass('listView') || '').split(' '); // wish we didn't have to do this + for (var _i = 0, listViewClassNames_1 = listViewClassNames; _i < listViewClassNames_1.length; _i++) { + var listViewClassName = listViewClassNames_1[_i]; + if (listViewClassName) { // in case input was empty string + this.el.classList.add(listViewClassName); + } + } + this.scroller = new core.ScrollComponent('hidden', // overflow x + 'auto' // overflow y + ); + this.el.appendChild(this.scroller.el); + this.contentEl = this.scroller.el; // shortcut + }; + ListView.prototype._unrenderSkeleton = function () { + // TODO: remove classNames + this.scroller.destroy(); // will remove the Grid too + }; + ListView.prototype.updateSize = function (isResize, viewHeight, isAuto) { + _super.prototype.updateSize.call(this, isResize, viewHeight, isAuto); + this.eventRenderer.computeSizes(isResize); + this.eventRenderer.assignSizes(isResize); + this.scroller.clear(); // sets height to 'auto' and clears overflow + if (!isAuto) { + this.scroller.setHeight(this.computeScrollerHeight(viewHeight)); + } + }; + ListView.prototype.computeScrollerHeight = function (viewHeight) { + return viewHeight - + core.subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller + }; + ListView.prototype._eventStoreToSegs = function (eventStore, eventUiBases, dayRanges) { + return this.eventRangesToSegs(core.sliceEventStore(eventStore, eventUiBases, this.props.dateProfile.activeRange, this.context.nextDayThreshold).fg, dayRanges); + }; + ListView.prototype.eventRangesToSegs = function (eventRanges, dayRanges) { + var segs = []; + for (var _i = 0, eventRanges_1 = eventRanges; _i < eventRanges_1.length; _i++) { + var eventRange = eventRanges_1[_i]; + segs.push.apply(segs, this.eventRangeToSegs(eventRange, dayRanges)); + } + return segs; + }; + ListView.prototype.eventRangeToSegs = function (eventRange, dayRanges) { + var _a = this.context, dateEnv = _a.dateEnv, nextDayThreshold = _a.nextDayThreshold; + var range = eventRange.range; + var allDay = eventRange.def.allDay; + var dayIndex; + var segRange; + var seg; + var segs = []; + for (dayIndex = 0; dayIndex < dayRanges.length; dayIndex++) { + segRange = core.intersectRanges(range, dayRanges[dayIndex]); + if (segRange) { + seg = { + component: this, + eventRange: eventRange, + start: segRange.start, + end: segRange.end, + isStart: eventRange.isStart && segRange.start.valueOf() === range.start.valueOf(), + isEnd: eventRange.isEnd && segRange.end.valueOf() === range.end.valueOf(), + dayIndex: dayIndex + }; + segs.push(seg); + // detect when range won't go fully into the next day, + // and mutate the latest seg to the be the end. + if (!seg.isEnd && !allDay && + dayIndex + 1 < dayRanges.length && + range.end < + dateEnv.add(dayRanges[dayIndex + 1].start, nextDayThreshold)) { + seg.end = range.end; + seg.isEnd = true; + break; + } + } + } + return segs; + }; + ListView.prototype.renderEmptyMessage = function () { + this.contentEl.innerHTML = + '<div class="fc-list-empty-wrap2">' + // TODO: try less wraps + '<div class="fc-list-empty-wrap1">' + + '<div class="fc-list-empty">' + + core.htmlEscape(this.context.options.noEventsMessage) + + '</div>' + + '</div>' + + '</div>'; + }; + // called by ListEventRenderer + ListView.prototype.renderSegList = function (allSegs) { + var theme = this.context.theme; + var segsByDay = this.groupSegsByDay(allSegs); // sparse array + var dayIndex; + var daySegs; + var i; + var tableEl = core.htmlToElement('<table class="fc-list-table ' + theme.getClass('tableList') + '"><tbody></tbody></table>'); + var tbodyEl = tableEl.querySelector('tbody'); + for (dayIndex = 0; dayIndex < segsByDay.length; dayIndex++) { + daySegs = segsByDay[dayIndex]; + if (daySegs) { // sparse array, so might be undefined + // append a day header + tbodyEl.appendChild(this.buildDayHeaderRow(this.dayDates[dayIndex])); + daySegs = this.eventRenderer.sortEventSegs(daySegs); + for (i = 0; i < daySegs.length; i++) { + tbodyEl.appendChild(daySegs[i].el); // append event row + } + } + } + this.contentEl.innerHTML = ''; + this.contentEl.appendChild(tableEl); + }; + // Returns a sparse array of arrays, segs grouped by their dayIndex + ListView.prototype.groupSegsByDay = function (segs) { + var segsByDay = []; // sparse array + var i; + var seg; + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + (segsByDay[seg.dayIndex] || (segsByDay[seg.dayIndex] = [])) + .push(seg); + } + return segsByDay; + }; + // generates the HTML for the day headers that live amongst the event rows + ListView.prototype.buildDayHeaderRow = function (dayDate) { + var _a = this.context, theme = _a.theme, dateEnv = _a.dateEnv, options = _a.options; + var mainFormat = core.createFormatter(options.listDayFormat); // TODO: cache + var altFormat = core.createFormatter(options.listDayAltFormat); // TODO: cache + return core.createElement('tr', { + className: 'fc-list-heading', + 'data-date': dateEnv.formatIso(dayDate, { omitTime: true }) + }, '<td class="' + (theme.getClass('tableListHeading') || + theme.getClass('widgetHeader')) + '" colspan="3">' + + (mainFormat ? + core.buildGotoAnchorHtml(options, dateEnv, dayDate, { 'class': 'fc-list-heading-main' }, core.htmlEscape(dateEnv.format(dayDate, mainFormat)) // inner HTML + ) : + '') + + (altFormat ? + core.buildGotoAnchorHtml(options, dateEnv, dayDate, { 'class': 'fc-list-heading-alt' }, core.htmlEscape(dateEnv.format(dayDate, altFormat)) // inner HTML + ) : + '') + + '</td>'); + }; + return ListView; + }(core.View)); + ListView.prototype.fgSegSelector = '.fc-list-item'; // which elements accept event actions + function computeDateVars(dateProfile) { + var dayStart = core.startOfDay(dateProfile.renderRange.start); + var viewEnd = dateProfile.renderRange.end; + var dayDates = []; + var dayRanges = []; + while (dayStart < viewEnd) { + dayDates.push(dayStart); + dayRanges.push({ + start: dayStart, + end: core.addDays(dayStart, 1) + }); + dayStart = core.addDays(dayStart, 1); + } + return { dayDates: dayDates, dayRanges: dayRanges }; + } + + var main = core.createPlugin({ + views: { + list: { + class: ListView, + buttonTextKey: 'list', + listDayFormat: { month: 'long', day: 'numeric', year: 'numeric' } // like "January 1, 2016" + }, + listDay: { + type: 'list', + duration: { days: 1 }, + listDayFormat: { weekday: 'long' } // day-of-week is all we need. full date is probably in header + }, + listWeek: { + type: 'list', + duration: { weeks: 1 }, + listDayFormat: { weekday: 'long' }, + listDayAltFormat: { month: 'long', day: 'numeric', year: 'numeric' } + }, + listMonth: { + type: 'list', + duration: { month: 1 }, + listDayAltFormat: { weekday: 'long' } // day-of-week is nice-to-have + }, + listYear: { + type: 'list', + duration: { year: 1 }, + listDayAltFormat: { weekday: 'long' } // day-of-week is nice-to-have + } + } + }); + + exports.ListView = ListView; + exports.default = main; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); diff --git a/addons/web/static/lib/fullcalendar/luxon/main.js b/addons/web/static/lib/fullcalendar/luxon/main.js new file mode 100644 index 00000000000..88e3976c7ae --- /dev/null +++ b/addons/web/static/lib/fullcalendar/luxon/main.js @@ -0,0 +1,170 @@ +/*! +FullCalendar Luxon Plugin v4.4.0 +Docs & License: https://fullcalendar.io/ +(c) 2019 Adam Shaw +*/ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('luxon'), require('@fullcalendar/core')) : + typeof define === 'function' && define.amd ? define(['exports', 'luxon', '@fullcalendar/core'], factory) : + (global = global || self, factory(global.FullCalendarLuxon = {}, global.luxon, global.FullCalendar)); +}(this, function (exports, luxon, core) { 'use strict'; + + /*! *****************************************************************************
+ Copyright (c) Microsoft Corporation. All rights reserved.
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+ this file except in compliance with the License. You may obtain a copy of the
+ License at http://www.apache.org/licenses/LICENSE-2.0
+
+ THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
+ WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+ MERCHANTABLITY OR NON-INFRINGEMENT.
+
+ See the Apache Version 2.0 License for specific language governing permissions
+ and limitations under the License.
+ ***************************************************************************** */
+ /* global Reflect, Promise */
+
+ var extendStatics = function(d, b) {
+ extendStatics = Object.setPrototypeOf ||
+ ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+ function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+ return extendStatics(d, b);
+ };
+
+ function __extends(d, b) {
+ extendStatics(d, b);
+ function __() { this.constructor = d; }
+ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+ }
+
+ var __assign = function() {
+ __assign = Object.assign || function __assign(t) {
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
+ s = arguments[i];
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
+ }
+ return t;
+ };
+ return __assign.apply(this, arguments);
+ }; + + function toDateTime(date, calendar) { + if (!(calendar instanceof core.Calendar)) { + throw new Error('must supply a Calendar instance'); + } + return luxon.DateTime.fromJSDate(date, { + zone: calendar.dateEnv.timeZone, + locale: calendar.dateEnv.locale.codes[0] + }); + } + function toDuration(duration, calendar) { + if (!(calendar instanceof core.Calendar)) { + throw new Error('must supply a Calendar instance'); + } + return luxon.Duration.fromObject(__assign({}, duration, { locale: calendar.dateEnv.locale.codes[0] })); + } + var LuxonNamedTimeZone = /** @class */ (function (_super) { + __extends(LuxonNamedTimeZone, _super); + function LuxonNamedTimeZone() { + return _super !== null && _super.apply(this, arguments) || this; + } + LuxonNamedTimeZone.prototype.offsetForArray = function (a) { + return arrayToLuxon(a, this.timeZoneName).offset; + }; + LuxonNamedTimeZone.prototype.timestampToArray = function (ms) { + return luxonToArray(luxon.DateTime.fromMillis(ms, { + zone: this.timeZoneName + })); + }; + return LuxonNamedTimeZone; + }(core.NamedTimeZoneImpl)); + function formatWithCmdStr(cmdStr, arg) { + var cmd = parseCmdStr(cmdStr); + if (arg.end) { + var start = arrayToLuxon(arg.start.array, arg.timeZone, arg.localeCodes[0]); + var end = arrayToLuxon(arg.end.array, arg.timeZone, arg.localeCodes[0]); + return formatRange(cmd, start.toFormat.bind(start), end.toFormat.bind(end), arg.separator); + } + return arrayToLuxon(arg.date.array, arg.timeZone, arg.localeCodes[0]).toFormat(cmd.whole); + } + var main = core.createPlugin({ + cmdFormatter: formatWithCmdStr, + namedTimeZonedImpl: LuxonNamedTimeZone + }); + function luxonToArray(datetime) { + return [ + datetime.year, + datetime.month - 1, + datetime.day, + datetime.hour, + datetime.minute, + datetime.second, + datetime.millisecond + ]; + } + function arrayToLuxon(arr, timeZone, locale) { + return luxon.DateTime.fromObject({ + zone: timeZone, + locale: locale, + year: arr[0], + month: arr[1] + 1, + day: arr[2], + hour: arr[3], + minute: arr[4], + second: arr[5], + millisecond: arr[6] + }); + } + function parseCmdStr(cmdStr) { + var parts = cmdStr.match(/^(.*?)\{(.*)\}(.*)$/); // TODO: lookbehinds for escape characters + if (parts) { + var middle = parseCmdStr(parts[2]); + return { + head: parts[1], + middle: middle, + tail: parts[3], + whole: parts[1] + middle.whole + parts[3] + }; + } + else { + return { + head: null, + middle: null, + tail: null, + whole: cmdStr + }; + } + } + function formatRange(cmd, formatStart, formatEnd, separator) { + if (cmd.middle) { + var startHead = formatStart(cmd.head); + var startMiddle = formatRange(cmd.middle, formatStart, formatEnd, separator); + var startTail = formatStart(cmd.tail); + var endHead = formatEnd(cmd.head); + var endMiddle = formatRange(cmd.middle, formatStart, formatEnd, separator); + var endTail = formatEnd(cmd.tail); + if (startHead === endHead && startTail === endTail) { + return startHead + + (startMiddle === endMiddle ? startMiddle : startMiddle + separator + endMiddle) + + startTail; + } + } + var startWhole = formatStart(cmd.whole); + var endWhole = formatEnd(cmd.whole); + if (startWhole === endWhole) { + return startWhole; + } + else { + return startWhole + separator + endWhole; + } + } + + exports.default = main; + exports.toDateTime = toDateTime; + exports.toDuration = toDuration; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); diff --git a/addons/web/static/lib/fullcalendar/moment-timezone/main.js b/addons/web/static/lib/fullcalendar/moment-timezone/main.js new file mode 100644 index 00000000000..b4d19bd030e --- /dev/null +++ b/addons/web/static/lib/fullcalendar/moment-timezone/main.js @@ -0,0 +1,64 @@ +/*! +FullCalendar Moment Timezone Plugin v4.4.0 +Docs & License: https://fullcalendar.io/ +(c) 2019 Adam Shaw +*/ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('moment'), require('moment-timezone/builds/moment-timezone-with-data'), require('@fullcalendar/core')) : + typeof define === 'function' && define.amd ? define(['exports', 'moment', 'moment-timezone/builds/moment-timezone-with-data', '@fullcalendar/core'], factory) : + (global = global || self, factory(global.FullCalendarMomentTimezone = {}, global.moment, global.moment, global.FullCalendar)); +}(this, function (exports, momentNs, momentTimezoneWithData, core) { 'use strict'; + + /*! *****************************************************************************
+ Copyright (c) Microsoft Corporation. All rights reserved.
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+ this file except in compliance with the License. You may obtain a copy of the
+ License at http://www.apache.org/licenses/LICENSE-2.0
+
+ THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
+ WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+ MERCHANTABLITY OR NON-INFRINGEMENT.
+
+ See the Apache Version 2.0 License for specific language governing permissions
+ and limitations under the License.
+ ***************************************************************************** */
+ /* global Reflect, Promise */
+
+ var extendStatics = function(d, b) {
+ extendStatics = Object.setPrototypeOf ||
+ ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+ function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+ return extendStatics(d, b);
+ };
+
+ function __extends(d, b) {
+ extendStatics(d, b);
+ function __() { this.constructor = d; }
+ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+ } + + var moment = momentNs; // the directly callable function + var MomentNamedTimeZone = /** @class */ (function (_super) { + __extends(MomentNamedTimeZone, _super); + function MomentNamedTimeZone() { + return _super !== null && _super.apply(this, arguments) || this; + } + MomentNamedTimeZone.prototype.offsetForArray = function (a) { + return moment.tz(a, this.timeZoneName).utcOffset(); + }; + MomentNamedTimeZone.prototype.timestampToArray = function (ms) { + return moment.tz(ms, this.timeZoneName).toArray(); + }; + return MomentNamedTimeZone; + }(core.NamedTimeZoneImpl)); + var main = core.createPlugin({ + namedTimeZonedImpl: MomentNamedTimeZone + }); + + exports.default = main; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); diff --git a/addons/web/static/lib/fullcalendar/moment/main.js b/addons/web/static/lib/fullcalendar/moment/main.js new file mode 100644 index 00000000000..96edc5cd15e --- /dev/null +++ b/addons/web/static/lib/fullcalendar/moment/main.js @@ -0,0 +1,110 @@ +/*! +FullCalendar Moment Plugin v4.4.0 +Docs & License: https://fullcalendar.io/ +(c) 2019 Adam Shaw +*/ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('moment'), require('@fullcalendar/core')) : + typeof define === 'function' && define.amd ? define(['exports', 'moment', '@fullcalendar/core'], factory) : + (global = global || self, factory(global.FullCalendarMoment = {}, global.moment, global.FullCalendar)); +}(this, function (exports, momentNs, core) { 'use strict'; + + var moment = momentNs; // the directly callable function + function toMoment(date, calendar) { + if (!(calendar instanceof core.Calendar)) { + throw new Error('must supply a Calendar instance'); + } + return convertToMoment(date, calendar.dateEnv.timeZone, null, calendar.dateEnv.locale.codes[0]); + } + function toDuration(fcDuration) { + return moment.duration(fcDuration); // moment accepts all the props that fc.Duration already has! + } + function formatWithCmdStr(cmdStr, arg) { + var cmd = parseCmdStr(cmdStr); + if (arg.end) { + var startMom = convertToMoment(arg.start.array, arg.timeZone, arg.start.timeZoneOffset, arg.localeCodes[0]); + var endMom = convertToMoment(arg.end.array, arg.timeZone, arg.end.timeZoneOffset, arg.localeCodes[0]); + return formatRange(cmd, createMomentFormatFunc(startMom), createMomentFormatFunc(endMom), arg.separator); + } + return convertToMoment(arg.date.array, arg.timeZone, arg.date.timeZoneOffset, arg.localeCodes[0]).format(cmd.whole); // TODO: test for this + } + var main = core.createPlugin({ + cmdFormatter: formatWithCmdStr + }); + function createMomentFormatFunc(mom) { + return function (cmdStr) { + return cmdStr ? mom.format(cmdStr) : ''; // because calling with blank string results in ISO8601 :( + }; + } + function convertToMoment(input, timeZone, timeZoneOffset, locale) { + var mom; + if (timeZone === 'local') { + mom = moment(input); + } + else if (timeZone === 'UTC') { + mom = moment.utc(input); + } + else if (moment.tz) { + mom = moment.tz(input, timeZone); + } + else { + mom = moment.utc(input); + if (timeZoneOffset != null) { + mom.utcOffset(timeZoneOffset); + } + } + mom.locale(locale); + return mom; + } + function parseCmdStr(cmdStr) { + var parts = cmdStr.match(/^(.*?)\{(.*)\}(.*)$/); // TODO: lookbehinds for escape characters + if (parts) { + var middle = parseCmdStr(parts[2]); + return { + head: parts[1], + middle: middle, + tail: parts[3], + whole: parts[1] + middle.whole + parts[3] + }; + } + else { + return { + head: null, + middle: null, + tail: null, + whole: cmdStr + }; + } + } + function formatRange(cmd, formatStart, formatEnd, separator) { + if (cmd.middle) { + var startHead = formatStart(cmd.head); + var startMiddle = formatRange(cmd.middle, formatStart, formatEnd, separator); + var startTail = formatStart(cmd.tail); + var endHead = formatEnd(cmd.head); + var endMiddle = formatRange(cmd.middle, formatStart, formatEnd, separator); + var endTail = formatEnd(cmd.tail); + if (startHead === endHead && startTail === endTail) { + return startHead + + (startMiddle === endMiddle ? startMiddle : startMiddle + separator + endMiddle) + + startTail; + } + } + var startWhole = formatStart(cmd.whole); + var endWhole = formatEnd(cmd.whole); + if (startWhole === endWhole) { + return startWhole; + } + else { + return startWhole + separator + endWhole; + } + } + + exports.default = main; + exports.toDuration = toDuration; + exports.toMoment = toMoment; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); diff --git a/addons/web/static/lib/fullcalendar/rrule/main.js b/addons/web/static/lib/fullcalendar/rrule/main.js new file mode 100644 index 00000000000..450650ab148 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/rrule/main.js @@ -0,0 +1,128 @@ +/*! +FullCalendar RRule Plugin v4.4.0 +Docs & License: https://fullcalendar.io/ +(c) 2019 Adam Shaw +*/ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('rrule'), require('@fullcalendar/core')) : + typeof define === 'function' && define.amd ? define(['exports', 'rrule', '@fullcalendar/core'], factory) : + (global = global || self, factory(global.FullCalendarRrule = {}, global.rrule, global.FullCalendar)); +}(this, function (exports, rrule, core) { 'use strict'; + + /*! *****************************************************************************
+ Copyright (c) Microsoft Corporation. All rights reserved.
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+ this file except in compliance with the License. You may obtain a copy of the
+ License at http://www.apache.org/licenses/LICENSE-2.0
+
+ THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
+ WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+ MERCHANTABLITY OR NON-INFRINGEMENT.
+
+ See the Apache Version 2.0 License for specific language governing permissions
+ and limitations under the License.
+ ***************************************************************************** */
+
+ var __assign = function() {
+ __assign = Object.assign || function __assign(t) {
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
+ s = arguments[i];
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
+ }
+ return t;
+ };
+ return __assign.apply(this, arguments);
+ }; + + var EVENT_DEF_PROPS = { + rrule: null, + duration: core.createDuration + }; + var recurring = { + parse: function (rawEvent, leftoverProps, dateEnv) { + if (rawEvent.rrule != null) { + var props = core.refineProps(rawEvent, EVENT_DEF_PROPS, {}, leftoverProps); + var parsed = parseRRule(props.rrule, dateEnv); + if (parsed) { + return { + typeData: parsed.rrule, + allDayGuess: parsed.allDayGuess, + duration: props.duration + }; + } + } + return null; + }, + expand: function (rrule, framingRange) { + // we WANT an inclusive start and in exclusive end, but the js rrule lib will only do either BOTH + // inclusive or BOTH exclusive, which is stupid: https://github.com/jakubroztocil/rrule/issues/84 + // Workaround: make inclusive, which will generate extra occurences, and then trim. + return rrule.between(framingRange.start, framingRange.end, true) + .filter(function (date) { + return date.valueOf() < framingRange.end.valueOf(); + }); + } + }; + var main = core.createPlugin({ + recurringTypes: [recurring] + }); + function parseRRule(input, dateEnv) { + var allDayGuess = null; + var rrule$1; + if (typeof input === 'string') { + rrule$1 = rrule.rrulestr(input); + } + else if (typeof input === 'object' && input) { // non-null object + var refined = __assign({}, input); // copy + if (typeof refined.dtstart === 'string') { + var dtstartMeta = dateEnv.createMarkerMeta(refined.dtstart); + if (dtstartMeta) { + refined.dtstart = dtstartMeta.marker; + allDayGuess = dtstartMeta.isTimeUnspecified; + } + else { + delete refined.dtstart; + } + } + if (typeof refined.until === 'string') { + refined.until = dateEnv.createMarker(refined.until); + } + if (refined.freq != null) { + refined.freq = convertConstant(refined.freq); + } + if (refined.wkst != null) { + refined.wkst = convertConstant(refined.wkst); + } + else { + refined.wkst = (dateEnv.weekDow - 1 + 7) % 7; // convert Sunday-first to Monday-first + } + if (refined.byweekday != null) { + refined.byweekday = convertConstants(refined.byweekday); // the plural version + } + rrule$1 = new rrule.RRule(refined); + } + if (rrule$1) { + return { rrule: rrule$1, allDayGuess: allDayGuess }; + } + return null; + } + function convertConstants(input) { + if (Array.isArray(input)) { + return input.map(convertConstant); + } + return convertConstant(input); + } + function convertConstant(input) { + if (typeof input === 'string') { + return rrule.RRule[input.toUpperCase()]; + } + return input; + } + + exports.default = main; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); diff --git a/addons/web/static/lib/fullcalendar/timegrid/main.css b/addons/web/static/lib/fullcalendar/timegrid/main.css new file mode 100644 index 00000000000..4b8bcd6e2cc --- /dev/null +++ b/addons/web/static/lib/fullcalendar/timegrid/main.css @@ -0,0 +1,309 @@ +@charset "UTF-8"; +/* TimeGridView all-day area +--------------------------------------------------------------------------------------------------*/ +.fc-timeGrid-view .fc-day-grid { + position: relative; + z-index: 2; + /* so the "more.." popover will be over the time grid */ +} + +.fc-timeGrid-view .fc-day-grid .fc-row { + min-height: 3em; + /* all-day section will never get shorter than this */ +} + +.fc-timeGrid-view .fc-day-grid .fc-row .fc-content-skeleton { + padding-bottom: 1em; + /* give space underneath events for clicking/selecting days */ +} + +/* TimeGrid axis running down the side (for both the all-day area and the slot area) +--------------------------------------------------------------------------------------------------*/ +.fc .fc-axis { + /* .fc to overcome default cell styles */ + vertical-align: middle; + padding: 0 4px; + white-space: nowrap; +} + +.fc-ltr .fc-axis { + text-align: right; +} + +.fc-rtl .fc-axis { + text-align: left; +} + +/* TimeGrid Structure +--------------------------------------------------------------------------------------------------*/ +.fc-time-grid-container, +.fc-time-grid { + /* so slats/bg/content/etc positions get scoped within here */ + position: relative; + z-index: 1; +} + +.fc-time-grid { + min-height: 100%; + /* so if height setting is 'auto', .fc-bg stretches to fill height */ +} + +.fc-time-grid table { + /* don't put outer borders on slats/bg/content/etc */ + border: 0 hidden transparent; +} + +.fc-time-grid > .fc-bg { + z-index: 1; +} + +.fc-time-grid .fc-slats, +.fc-time-grid > hr { + /* the <hr> TimeGridView injects when grid is shorter than scroller */ + position: relative; + z-index: 2; +} + +.fc-time-grid .fc-content-col { + position: relative; + /* because now-indicator lives directly inside */ +} + +.fc-time-grid .fc-content-skeleton { + position: absolute; + z-index: 3; + top: 0; + left: 0; + right: 0; +} + +/* divs within a cell within the fc-content-skeleton */ +.fc-time-grid .fc-business-container { + position: relative; + z-index: 1; +} + +.fc-time-grid .fc-bgevent-container { + position: relative; + z-index: 2; +} + +.fc-time-grid .fc-highlight-container { + position: relative; + z-index: 3; +} + +.fc-time-grid .fc-event-container { + position: relative; + z-index: 4; +} + +.fc-time-grid .fc-now-indicator-line { + z-index: 5; +} + +.fc-time-grid .fc-mirror-container { + /* also is fc-event-container */ + position: relative; + z-index: 6; +} + +/* TimeGrid Slats (lines that run horizontally) +--------------------------------------------------------------------------------------------------*/ +.fc-time-grid .fc-slats td { + height: 1.5em; + border-bottom: 0; + /* each cell is responsible for its top border */ +} + +.fc-time-grid .fc-slats .fc-minor td { + border-top-style: dotted; +} + +/* TimeGrid Highlighting Slots +--------------------------------------------------------------------------------------------------*/ +.fc-time-grid .fc-highlight-container { + /* a div within a cell within the fc-highlight-skeleton */ + position: relative; + /* scopes the left/right of the fc-highlight to be in the column */ +} + +.fc-time-grid .fc-highlight { + position: absolute; + left: 0; + right: 0; + /* top and bottom will be in by JS */ +} + +/* TimeGrid Event Containment +--------------------------------------------------------------------------------------------------*/ +.fc-ltr .fc-time-grid .fc-event-container { + /* space on the sides of events for LTR (default) */ + margin: 0 2.5% 0 2px; +} + +.fc-rtl .fc-time-grid .fc-event-container { + /* space on the sides of events for RTL */ + margin: 0 2px 0 2.5%; +} + +.fc-time-grid .fc-event, +.fc-time-grid .fc-bgevent { + position: absolute; + z-index: 1; + /* scope inner z-index's */ +} + +.fc-time-grid .fc-bgevent { + /* background events always span full width */ + left: 0; + right: 0; +} + +/* TimeGrid Event Styling +---------------------------------------------------------------------------------------------------- +We use the full "fc-time-grid-event" class instead of using descendants because the event won't +be a descendant of the grid when it is being dragged. +*/ +.fc-time-grid-event { + margin-bottom: 1px; +} + +.fc-time-grid-event-inset { + -webkit-box-shadow: 0px 0px 0px 1px #fff; + box-shadow: 0px 0px 0px 1px #fff; +} + +.fc-time-grid-event.fc-not-start { + /* events that are continuing from another day */ + /* replace space made by the top border with padding */ + border-top-width: 0; + padding-top: 1px; + /* remove top rounded corners */ + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.fc-time-grid-event.fc-not-end { + /* replace space made by the top border with padding */ + border-bottom-width: 0; + padding-bottom: 1px; + /* remove bottom rounded corners */ + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.fc-time-grid-event .fc-content { + overflow: hidden; + max-height: 100%; +} + +.fc-time-grid-event .fc-time, +.fc-time-grid-event .fc-title { + padding: 0 1px; +} + +.fc-time-grid-event .fc-time { + font-size: 0.85em; + white-space: nowrap; +} + +/* short mode, where time and title are on the same line */ +.fc-time-grid-event.fc-short .fc-content { + /* don't wrap to second line (now that contents will be inline) */ + white-space: nowrap; +} + +.fc-time-grid-event.fc-short .fc-time, +.fc-time-grid-event.fc-short .fc-title { + /* put the time and title on the same line */ + display: inline-block; + vertical-align: top; +} + +.fc-time-grid-event.fc-short .fc-time span { + display: none; + /* don't display the full time text... */ +} + +.fc-time-grid-event.fc-short .fc-time:before { + content: attr(data-start); + /* ...instead, display only the start time */ +} + +.fc-time-grid-event.fc-short .fc-time:after { + content: " - "; + /* seperate with a dash, wrapped in nbsp's */ +} + +.fc-time-grid-event.fc-short .fc-title { + font-size: 0.85em; + /* make the title text the same size as the time */ + padding: 0; + /* undo padding from above */ +} + +/* resizer (cursor device) */ +.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer { + left: 0; + right: 0; + bottom: 0; + height: 8px; + overflow: hidden; + line-height: 8px; + font-size: 11px; + font-family: monospace; + text-align: center; + cursor: s-resize; +} + +.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer:after { + content: "="; +} + +/* resizer (touch device) */ +.fc-time-grid-event.fc-selected .fc-resizer { + /* 10x10 dot */ + border-radius: 5px; + border-width: 1px; + width: 8px; + height: 8px; + border-style: solid; + border-color: inherit; + background: #fff; + /* horizontally center */ + left: 50%; + margin-left: -5px; + /* center on the bottom edge */ + bottom: -5px; +} + +/* Now Indicator +--------------------------------------------------------------------------------------------------*/ +.fc-time-grid .fc-now-indicator-line { + border-top-width: 1px; + left: 0; + right: 0; +} + +/* arrow on axis */ +.fc-time-grid .fc-now-indicator-arrow { + margin-top: -5px; + /* vertically center on top coordinate */ +} + +.fc-ltr .fc-time-grid .fc-now-indicator-arrow { + left: 0; + /* triangle pointing right... */ + border-width: 5px 0 5px 6px; + border-top-color: transparent; + border-bottom-color: transparent; +} + +.fc-rtl .fc-time-grid .fc-now-indicator-arrow { + right: 0; + /* triangle pointing left... */ + border-width: 5px 6px 5px 0; + border-top-color: transparent; + border-bottom-color: transparent; +}
\ No newline at end of file diff --git a/addons/web/static/lib/fullcalendar/timegrid/main.js b/addons/web/static/lib/fullcalendar/timegrid/main.js new file mode 100644 index 00000000000..086625fac52 --- /dev/null +++ b/addons/web/static/lib/fullcalendar/timegrid/main.js @@ -0,0 +1,1401 @@ +/*! +FullCalendar Time Grid Plugin v4.4.0 +Docs & License: https://fullcalendar.io/ +(c) 2019 Adam Shaw +*/ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@fullcalendar/core'), require('@fullcalendar/daygrid')) : + typeof define === 'function' && define.amd ? define(['exports', '@fullcalendar/core', '@fullcalendar/daygrid'], factory) : + (global = global || self, factory(global.FullCalendarTimeGrid = {}, global.FullCalendar, global.FullCalendarDayGrid)); +}(this, function (exports, core, daygrid) { 'use strict'; + + /*! *****************************************************************************
+ Copyright (c) Microsoft Corporation. All rights reserved.
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+ this file except in compliance with the License. You may obtain a copy of the
+ License at http://www.apache.org/licenses/LICENSE-2.0
+
+ THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
+ WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+ MERCHANTABLITY OR NON-INFRINGEMENT.
+
+ See the Apache Version 2.0 License for specific language governing permissions
+ and limitations under the License.
+ ***************************************************************************** */
+ /* global Reflect, Promise */
+
+ var extendStatics = function(d, b) {
+ extendStatics = Object.setPrototypeOf ||
+ ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+ function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+ return extendStatics(d, b);
+ };
+
+ function __extends(d, b) {
+ extendStatics(d, b);
+ function __() { this.constructor = d; }
+ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+ }
+
+ var __assign = function() {
+ __assign = Object.assign || function __assign(t) {
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
+ s = arguments[i];
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
+ }
+ return t;
+ };
+ return __assign.apply(this, arguments);
+ }; + + /* + Only handles foreground segs. + Does not own rendering. Use for low-level util methods by TimeGrid. + */ + var TimeGridEventRenderer = /** @class */ (function (_super) { + __extends(TimeGridEventRenderer, _super); + function TimeGridEventRenderer(timeGrid) { + var _this = _super.call(this) || this; + _this.timeGrid = timeGrid; + return _this; + } + TimeGridEventRenderer.prototype.renderSegs = function (context, segs, mirrorInfo) { + _super.prototype.renderSegs.call(this, context, segs, mirrorInfo); + // TODO: dont do every time. memoize + this.fullTimeFormat = core.createFormatter({ + hour: 'numeric', + minute: '2-digit', + separator: this.context.options.defaultRangeSeparator + }); + }; + // Given an array of foreground segments, render a DOM element for each, computes position, + // and attaches to the column inner-container elements. + TimeGridEventRenderer.prototype.attachSegs = function (segs, mirrorInfo) { + var segsByCol = this.timeGrid.groupSegsByCol(segs); + // order the segs within each column + // TODO: have groupSegsByCol do this? + for (var col = 0; col < segsByCol.length; col++) { + segsByCol[col] = this.sortEventSegs(segsByCol[col]); + } + this.segsByCol = segsByCol; + this.timeGrid.attachSegsByCol(segsByCol, this.timeGrid.fgContainerEls); + }; + TimeGridEventRenderer.prototype.detachSegs = function (segs) { + segs.forEach(function (seg) { + core.removeElement(seg.el); + }); + this.segsByCol = null; + }; + TimeGridEventRenderer.prototype.computeSegSizes = function (allSegs) { + var _a = this, timeGrid = _a.timeGrid, segsByCol = _a.segsByCol; + var colCnt = timeGrid.colCnt; + timeGrid.computeSegVerticals(allSegs); // horizontals relies on this + if (segsByCol) { + for (var col = 0; col < colCnt; col++) { + this.computeSegHorizontals(segsByCol[col]); // compute horizontal coordinates, z-index's, and reorder the array + } + } + }; + TimeGridEventRenderer.prototype.assignSegSizes = function (allSegs) { + var _a = this, timeGrid = _a.timeGrid, segsByCol = _a.segsByCol; + var colCnt = timeGrid.colCnt; + timeGrid.assignSegVerticals(allSegs); // horizontals relies on this + if (segsByCol) { + for (var col = 0; col < colCnt; col++) { + this.assignSegCss(segsByCol[col]); + } + } + }; + // Computes a default event time formatting string if `eventTimeFormat` is not explicitly defined + TimeGridEventRenderer.prototype.computeEventTimeFormat = function () { + return { + hour: 'numeric', + minute: '2-digit', + meridiem: false + }; + }; + // Computes a default `displayEventEnd` value if one is not expliclty defined + TimeGridEventRenderer.prototype.computeDisplayEventEnd = function () { + return true; + }; + // Renders the HTML for a single event segment's default rendering + TimeGridEventRenderer.prototype.renderSegHtml = function (seg, mirrorInfo) { + var eventRange = seg.eventRange; + var eventDef = eventRange.def; + var eventUi = eventRange.ui; + var allDay = eventDef.allDay; + var isDraggable = core.computeEventDraggable(this.context, eventDef, eventUi); + var isResizableFromStart = seg.isStart && core.computeEventStartResizable(this.context, eventDef, eventUi); + var isResizableFromEnd = seg.isEnd && core.computeEventEndResizable(this.context, eventDef, eventUi); + var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd, mirrorInfo); + var skinCss = core.cssToStr(this.getSkinCss(eventUi)); + 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'); + // if the event appears to span more than one day... + if (core.isMultiDayRange(eventRange.range)) { + // 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) { + var unzonedStart = seg.start; + var unzonedEnd = seg.end; + timeText = this._getTimeText(unzonedStart, unzonedEnd, allDay); // TODO: give the timezones + fullTimeText = this._getTimeText(unzonedStart, unzonedEnd, allDay, this.fullTimeFormat); + startTimeText = this._getTimeText(unzonedStart, unzonedEnd, allDay, null, false); // displayEnd=false + } + } + else { + // Display the normal time text for the *event's* times + timeText = this.getTimeText(eventRange); + fullTimeText = this.getTimeText(eventRange, this.fullTimeFormat); + startTimeText = this.getTimeText(eventRange, null, false); // displayEnd=false + } + return '<a class="' + classes.join(' ') + '"' + + (eventDef.url ? + ' href="' + core.htmlEscape(eventDef.url) + '"' : + '') + + (skinCss ? + ' style="' + skinCss + '"' : + '') + + '>' + + '<div class="fc-content">' + + (timeText ? + '<div class="fc-time"' + + ' data-start="' + core.htmlEscape(startTimeText) + '"' + + ' data-full="' + core.htmlEscape(fullTimeText) + '"' + + '>' + + '<span>' + core.htmlEscape(timeText) + '</span>' + + '</div>' : + '') + + (eventDef.title ? + '<div class="fc-title">' + + core.htmlEscape(eventDef.title) + + '</div>' : + '') + + '</div>' + + /* TODO: write CSS for this + (isResizableFromStart ? + '<div class="fc-resizer fc-start-resizer"></div>' : + '' + ) + + */ + (isResizableFromEnd ? + '<div class="fc-resizer fc-end-resizer"></div>' : + '') + + '</a>'; + }; + // Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each. + // Assumed the segs are already ordered. + // NOTE: Also reorders the given array by date! + TimeGridEventRenderer.prototype.computeSegHorizontals = function (segs) { + var levels; + var level0; + var i; + 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.computeSegForwardBack(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. + TimeGridEventRenderer.prototype.computeSegForwardBack = 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.computeSegForwardBack(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 < forwardSegs.length; i++) { + this.computeSegForwardBack(forwardSegs[i], 0, seg.forwardCoord); + } + } + }; + TimeGridEventRenderer.prototype.sortForwardSegs = function (forwardSegs) { + var objs = forwardSegs.map(buildTimeGridSegCompareObj); + var specs = [ + // put higher-pressure first + { field: 'forwardPressure', order: -1 }, + // put segments that are closer to initial edge first (and favor ones with no coords yet) + { field: 'backwardCoord', order: 1 } + ].concat(this.context.eventOrderSpecs); + objs.sort(function (obj0, obj1) { + return core.compareByFieldSpecs(obj0, obj1, specs); + }); + return objs.map(function (c) { + return c._seg; + }); + }; + // Given foreground event segments that have already had their position coordinates computed, + // assigns position-related CSS values to their elements. + TimeGridEventRenderer.prototype.assignSegCss = function (segs) { + for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) { + var seg = segs_1[_i]; + core.applyStyle(seg.el, this.generateSegCss(seg)); + if (seg.level > 0) { + seg.el.classList.add('fc-time-grid-event-inset'); + } + // if the event is short that the title will be cut off, + // attach a className that condenses the title into the time area. + if (seg.eventRange.def.title && seg.bottom - seg.top < 30) { + seg.el.classList.add('fc-short'); // TODO: "condensed" is a better name + } + } + }; + // Generates an object with CSS properties/values that should be applied to an event segment element. + // Contains important positioning-related properties that should be applied to any event element, customized or not. + TimeGridEventRenderer.prototype.generateSegCss = function (seg) { + var shouldOverlap = this.context.options.slotEventOverlap; + var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point + var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point + var props = this.timeGrid.generateSegVerticalCss(seg); // get top/bottom first + var isRtl = this.context.isRtl; + var left; // amount of space from left edge, a fraction of the total width + var right; // amount of space from right edge, a fraction of the total width + if (shouldOverlap) { + // double the width, but don't go beyond the maximum forward coordinate (1.0) + forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2); + } + if (isRtl) { + left = 1 - forwardCoord; + right = backwardCoord; + } + else { + left = backwardCoord; + right = 1 - forwardCoord; + } + props.zIndex = seg.level + 1; // convert from 0-base to 1-based + props.left = left * 100 + '%'; + props.right = right * 100 + '%'; + if (shouldOverlap && seg.forwardPressure) { + // add padding to the edge so that forward stacked events don't cover the resizer's icon + props[isRtl ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width + } + return props; + }; + return TimeGridEventRenderer; + }(core.FgEventRenderer)); + // Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is + // left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date. + function buildSlotSegLevels(segs) { + var levels = []; + var i; + var seg; + var j; + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + // go through all the levels and stop on the first level where there are no collisions + for (j = 0; j < levels.length; j++) { + if (!computeSlotSegCollisions(seg, levels[j]).length) { + break; + } + } + seg.level = j; + (levels[j] || (levels[j] = [])).push(seg); + } + return levels; + } + // For every segment, figure out the other segments that are in subsequent + // levels that also occupy the same vertical space. Accumulate in seg.forwardSegs + function computeForwardSlotSegs(levels) { + var i; + var level; + var j; + var seg; + var k; + for (i = 0; i < levels.length; i++) { + level = levels[i]; + for (j = 0; j < level.length; j++) { + seg = level[j]; + seg.forwardSegs = []; + for (k = i + 1; k < levels.length; k++) { + computeSlotSegCollisions(seg, levels[k], seg.forwardSegs); + } + } + } + } + // Figure out which path forward (via seg.forwardSegs) results in the longest path until + // the furthest edge is reached. The number of segments in this path will be seg.forwardPressure + function computeSlotSegPressures(seg) { + var forwardSegs = seg.forwardSegs; + var forwardPressure = 0; + var i; + var forwardSeg; + if (seg.forwardPressure === undefined) { // not already computed + for (i = 0; i < forwardSegs.length; i++) { + forwardSeg = forwardSegs[i]; + // figure out the child's maximum forward path + computeSlotSegPressures(forwardSeg); + // either use the existing maximum, or use the child's forward pressure + // plus one (for the forwardSeg itself) + forwardPressure = Math.max(forwardPressure, 1 + forwardSeg.forwardPressure); + } + seg.forwardPressure = forwardPressure; + } + } + // Find all the segments in `otherSegs` that vertically collide with `seg`. + // Append into an optionally-supplied `results` array and return. + function computeSlotSegCollisions(seg, otherSegs, results) { + if (results === void 0) { results = []; } + for (var i = 0; i < otherSegs.length; i++) { + if (isSlotSegCollision(seg, otherSegs[i])) { + results.push(otherSegs[i]); + } + } + return results; + } + // Do these segments occupy the same vertical space? + function isSlotSegCollision(seg1, seg2) { + return seg1.bottom > seg2.top && seg1.top < seg2.bottom; + } + function buildTimeGridSegCompareObj(seg) { + var obj = core.buildSegCompareObj(seg); + obj.forwardPressure = seg.forwardPressure; + obj.backwardCoord = seg.backwardCoord; + return obj; + } + + var TimeGridMirrorRenderer = /** @class */ (function (_super) { + __extends(TimeGridMirrorRenderer, _super); + function TimeGridMirrorRenderer() { + return _super !== null && _super.apply(this, arguments) || this; + } + TimeGridMirrorRenderer.prototype.attachSegs = function (segs, mirrorInfo) { + this.segsByCol = this.timeGrid.groupSegsByCol(segs); + this.timeGrid.attachSegsByCol(this.segsByCol, this.timeGrid.mirrorContainerEls); + this.sourceSeg = mirrorInfo.sourceSeg; + }; + TimeGridMirrorRenderer.prototype.generateSegCss = function (seg) { + var props = _super.prototype.generateSegCss.call(this, seg); + var sourceSeg = this.sourceSeg; + if (sourceSeg && sourceSeg.col === seg.col) { + var sourceSegProps = _super.prototype.generateSegCss.call(this, sourceSeg); + props.left = sourceSegProps.left; + props.right = sourceSegProps.right; + props.marginLeft = sourceSegProps.marginLeft; + props.marginRight = sourceSegProps.marginRight; + } + return props; + }; + return TimeGridMirrorRenderer; + }(TimeGridEventRenderer)); + + var TimeGridFillRenderer = /** @class */ (function (_super) { + __extends(TimeGridFillRenderer, _super); + function TimeGridFillRenderer(timeGrid) { + var _this = _super.call(this) || this; + _this.timeGrid = timeGrid; + return _this; + } + TimeGridFillRenderer.prototype.attachSegs = function (type, segs) { + var timeGrid = this.timeGrid; + var containerEls; + // TODO: more efficient lookup + if (type === 'bgEvent') { + containerEls = timeGrid.bgContainerEls; + } + else if (type === 'businessHours') { + containerEls = timeGrid.businessContainerEls; + } + else if (type === 'highlight') { + containerEls = timeGrid.highlightContainerEls; + } + timeGrid.attachSegsByCol(timeGrid.groupSegsByCol(segs), containerEls); + return segs.map(function (seg) { + return seg.el; + }); + }; + TimeGridFillRenderer.prototype.computeSegSizes = function (segs) { + this.timeGrid.computeSegVerticals(segs); + }; + TimeGridFillRenderer.prototype.assignSegSizes = function (segs) { + this.timeGrid.assignSegVerticals(segs); + }; + return TimeGridFillRenderer; + }(core.FillRenderer)); + + /* A component that renders one or more columns of vertical time slots + ----------------------------------------------------------------------------------------------------------------------*/ + // 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 TimeGrid = /** @class */ (function (_super) { + __extends(TimeGrid, _super); + function TimeGrid(el, renderProps) { + var _this = _super.call(this, el) || this; + _this.isSlatSizesDirty = false; + _this.isColSizesDirty = false; + _this.processOptions = core.memoize(_this._processOptions); + _this.renderSkeleton = core.memoizeRendering(_this._renderSkeleton); + _this.renderSlats = core.memoizeRendering(_this._renderSlats, null, [_this.renderSkeleton]); + _this.renderColumns = core.memoizeRendering(_this._renderColumns, _this._unrenderColumns, [_this.renderSkeleton]); + _this.renderProps = renderProps; + var renderColumns = _this.renderColumns; + var eventRenderer = _this.eventRenderer = new TimeGridEventRenderer(_this); + var fillRenderer = _this.fillRenderer = new TimeGridFillRenderer(_this); + _this.mirrorRenderer = new TimeGridMirrorRenderer(_this); + _this.renderBusinessHours = core.memoizeRendering(fillRenderer.renderSegs.bind(fillRenderer, 'businessHours'), fillRenderer.unrender.bind(fillRenderer, 'businessHours'), [renderColumns]); + _this.renderDateSelection = core.memoizeRendering(_this._renderDateSelection, _this._unrenderDateSelection, [renderColumns]); + _this.renderFgEvents = core.memoizeRendering(eventRenderer.renderSegs.bind(eventRenderer), eventRenderer.unrender.bind(eventRenderer), [renderColumns]); + _this.renderBgEvents = core.memoizeRendering(fillRenderer.renderSegs.bind(fillRenderer, 'bgEvent'), fillRenderer.unrender.bind(fillRenderer, 'bgEvent'), [renderColumns]); + _this.renderEventSelection = core.memoizeRendering(eventRenderer.selectByInstanceId.bind(eventRenderer), eventRenderer.unselectByInstanceId.bind(eventRenderer), [_this.renderFgEvents]); + _this.renderEventDrag = core.memoizeRendering(_this._renderEventDrag, _this._unrenderEventDrag, [renderColumns]); + _this.renderEventResize = core.memoizeRendering(_this._renderEventResize, _this._unrenderEventResize, [renderColumns]); + return _this; + } + /* Options + ------------------------------------------------------------------------------------------------------------------*/ + // Parses various options into properties of this object + // MUST have context already set + TimeGrid.prototype._processOptions = function (options) { + var slotDuration = options.slotDuration, snapDuration = options.snapDuration; + var snapsPerSlot; + var input; + slotDuration = core.createDuration(slotDuration); + snapDuration = snapDuration ? core.createDuration(snapDuration) : slotDuration; + snapsPerSlot = core.wholeDivideDurations(slotDuration, snapDuration); + if (snapsPerSlot === null) { + snapDuration = slotDuration; + snapsPerSlot = 1; + // TODO: say warning? + } + this.slotDuration = slotDuration; + this.snapDuration = snapDuration; + this.snapsPerSlot = snapsPerSlot; + // might be an array value (for TimelineView). + // if so, getting the most granular entry (the last one probably). + input = options.slotLabelFormat; + if (Array.isArray(input)) { + input = input[input.length - 1]; + } + this.labelFormat = core.createFormatter(input || { + hour: 'numeric', + minute: '2-digit', + omitZeroMinute: true, + meridiem: 'short' + }); + input = options.slotLabelInterval; + this.labelInterval = input ? + core.createDuration(input) : + this.computeLabelInterval(slotDuration); + }; + // Computes an automatic value for slotLabelInterval + TimeGrid.prototype.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 = core.createDuration(AGENDA_STOCK_SUB_DURATIONS[i]); + slotsPerLabel = core.wholeDivideDurations(labelInterval, slotDuration); + if (slotsPerLabel !== null && slotsPerLabel > 1) { + return labelInterval; + } + } + return slotDuration; // fall back + }; + /* Rendering + ------------------------------------------------------------------------------------------------------------------*/ + TimeGrid.prototype.render = function (props, context) { + this.processOptions(context.options); + var cells = props.cells; + this.colCnt = cells.length; + this.renderSkeleton(context.theme); + this.renderSlats(props.dateProfile); + this.renderColumns(props.cells, props.dateProfile); + this.renderBusinessHours(context, props.businessHourSegs); + this.renderDateSelection(props.dateSelectionSegs); + this.renderFgEvents(context, props.fgEventSegs); + this.renderBgEvents(context, props.bgEventSegs); + this.renderEventSelection(props.eventSelection); + this.renderEventDrag(props.eventDrag); + this.renderEventResize(props.eventResize); + }; + TimeGrid.prototype.destroy = function () { + _super.prototype.destroy.call(this); + // should unrender everything else too + this.renderSlats.unrender(); + this.renderColumns.unrender(); + this.renderSkeleton.unrender(); + }; + TimeGrid.prototype.updateSize = function (isResize) { + var _a = this, fillRenderer = _a.fillRenderer, eventRenderer = _a.eventRenderer, mirrorRenderer = _a.mirrorRenderer; + if (isResize || this.isSlatSizesDirty) { + this.buildSlatPositions(); + this.isSlatSizesDirty = false; + } + if (isResize || this.isColSizesDirty) { + this.buildColPositions(); + this.isColSizesDirty = false; + } + fillRenderer.computeSizes(isResize); + eventRenderer.computeSizes(isResize); + mirrorRenderer.computeSizes(isResize); + fillRenderer.assignSizes(isResize); + eventRenderer.assignSizes(isResize); + mirrorRenderer.assignSizes(isResize); + }; + TimeGrid.prototype._renderSkeleton = function (theme) { + var el = this.el; + el.innerHTML = + '<div class="fc-bg"></div>' + + '<div class="fc-slats"></div>' + + '<hr class="fc-divider ' + theme.getClass('widgetHeader') + '" style="display:none" />'; + this.rootBgContainerEl = el.querySelector('.fc-bg'); + this.slatContainerEl = el.querySelector('.fc-slats'); + this.bottomRuleEl = el.querySelector('.fc-divider'); + }; + TimeGrid.prototype._renderSlats = function (dateProfile) { + var theme = this.context.theme; + this.slatContainerEl.innerHTML = + '<table class="' + theme.getClass('tableGrid') + '">' + + this.renderSlatRowHtml(dateProfile) + + '</table>'; + this.slatEls = core.findElements(this.slatContainerEl, 'tr'); + this.slatPositions = new core.PositionCache(this.el, this.slatEls, false, true // vertical + ); + this.isSlatSizesDirty = true; + }; + // Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL. + TimeGrid.prototype.renderSlatRowHtml = function (dateProfile) { + var _a = this.context, dateEnv = _a.dateEnv, theme = _a.theme, isRtl = _a.isRtl; + var html = ''; + var dayStart = core.startOfDay(dateProfile.renderRange.start); + var slotTime = dateProfile.minTime; + var slotIterator = core.createDuration(0); + 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 (core.asRoughMs(slotTime) < core.asRoughMs(dateProfile.maxTime)) { + slotDate = dateEnv.add(dayStart, slotTime); + isLabeled = core.wholeDivideDurations(slotIterator, this.labelInterval) !== null; + axisHtml = + '<td class="fc-axis fc-time ' + theme.getClass('widgetContent') + '">' + + (isLabeled ? + '<span>' + // for matchCellWidths + core.htmlEscape(dateEnv.format(slotDate, this.labelFormat)) + + '</span>' : + '') + + '</td>'; + html += + '<tr data-time="' + core.formatIsoTimeString(slotDate) + '"' + + (isLabeled ? '' : ' class="fc-minor"') + + '>' + + (!isRtl ? axisHtml : '') + + '<td class="' + theme.getClass('widgetContent') + '"></td>' + + (isRtl ? axisHtml : '') + + '</tr>'; + slotTime = core.addDurations(slotTime, this.slotDuration); + slotIterator = core.addDurations(slotIterator, this.slotDuration); + } + return html; + }; + TimeGrid.prototype._renderColumns = function (cells, dateProfile) { + var _a = this.context, calendar = _a.calendar, view = _a.view, isRtl = _a.isRtl, theme = _a.theme, dateEnv = _a.dateEnv; + var bgRow = new daygrid.DayBgRow(this.context); + this.rootBgContainerEl.innerHTML = + '<table class="' + theme.getClass('tableGrid') + '">' + + bgRow.renderHtml({ + cells: cells, + dateProfile: dateProfile, + renderIntroHtml: this.renderProps.renderBgIntroHtml + }) + + '</table>'; + this.colEls = core.findElements(this.el, '.fc-day, .fc-disabled-day'); + for (var col = 0; col < this.colCnt; col++) { + calendar.publiclyTrigger('dayRender', [ + { + date: dateEnv.toDate(cells[col].date), + el: this.colEls[col], + view: view + } + ]); + } + if (isRtl) { + this.colEls.reverse(); + } + this.colPositions = new core.PositionCache(this.el, this.colEls, true, // horizontal + false); + this.renderContentSkeleton(); + this.isColSizesDirty = true; + }; + TimeGrid.prototype._unrenderColumns = function () { + this.unrenderContentSkeleton(); + }; + /* Content Skeleton + ------------------------------------------------------------------------------------------------------------------*/ + // Renders the DOM that the view's content will live in + TimeGrid.prototype.renderContentSkeleton = function () { + var isRtl = this.context.isRtl; + var parts = []; + var skeletonEl; + parts.push(this.renderProps.renderIntroHtml()); + for (var i = 0; i < this.colCnt; i++) { + parts.push('<td>' + + '<div class="fc-content-col">' + + '<div class="fc-event-container fc-mirror-container"></div>' + + '<div class="fc-event-container"></div>' + + '<div class="fc-highlight-container"></div>' + + '<div class="fc-bgevent-container"></div>' + + '<div class="fc-business-container"></div>' + + '</div>' + + '</td>'); + } + if (isRtl) { + parts.reverse(); + } + skeletonEl = this.contentSkeletonEl = core.htmlToElement('<div class="fc-content-skeleton">' + + '<table>' + + '<tr>' + parts.join('') + '</tr>' + + '</table>' + + '</div>'); + this.colContainerEls = core.findElements(skeletonEl, '.fc-content-col'); + this.mirrorContainerEls = core.findElements(skeletonEl, '.fc-mirror-container'); + this.fgContainerEls = core.findElements(skeletonEl, '.fc-event-container:not(.fc-mirror-container)'); + this.bgContainerEls = core.findElements(skeletonEl, '.fc-bgevent-container'); + this.highlightContainerEls = core.findElements(skeletonEl, '.fc-highlight-container'); + this.businessContainerEls = core.findElements(skeletonEl, '.fc-business-container'); + if (isRtl) { + this.colContainerEls.reverse(); + this.mirrorContainerEls.reverse(); + this.fgContainerEls.reverse(); + this.bgContainerEls.reverse(); + this.highlightContainerEls.reverse(); + this.businessContainerEls.reverse(); + } + this.el.appendChild(skeletonEl); + }; + TimeGrid.prototype.unrenderContentSkeleton = function () { + core.removeElement(this.contentSkeletonEl); + }; + // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col + TimeGrid.prototype.groupSegsByCol = function (segs) { + var segsByCol = []; + var i; + for (i = 0; i < this.colCnt; i++) { + segsByCol.push([]); + } + for (i = 0; i < segs.length; i++) { + segsByCol[segs[i].col].push(segs[i]); + } + return segsByCol; + }; + // Given segments grouped by column, insert the segments' elements into a parallel array of container + // elements, each living within a column. + TimeGrid.prototype.attachSegsByCol = function (segsByCol, containerEls) { + var col; + var segs; + var i; + for (col = 0; col < this.colCnt; col++) { // iterate each column grouping + segs = segsByCol[col]; + for (i = 0; i < segs.length; i++) { + containerEls[col].appendChild(segs[i].el); + } + } + }; + /* Now Indicator + ------------------------------------------------------------------------------------------------------------------*/ + TimeGrid.prototype.getNowIndicatorUnit = function () { + return 'minute'; // will refresh on the minute + }; + TimeGrid.prototype.renderNowIndicator = function (segs, date) { + // HACK: if date columns not ready for some reason (scheduler) + if (!this.colContainerEls) { + return; + } + var top = this.computeDateTop(date); + var nodes = []; + var i; + // render lines within the columns + for (i = 0; i < segs.length; i++) { + var lineEl = core.createElement('div', { className: 'fc-now-indicator fc-now-indicator-line' }); + lineEl.style.top = top + 'px'; + this.colContainerEls[segs[i].col].appendChild(lineEl); + nodes.push(lineEl); + } + // render an arrow over the axis + if (segs.length > 0) { // is the current time in view? + var arrowEl = core.createElement('div', { className: 'fc-now-indicator fc-now-indicator-arrow' }); + arrowEl.style.top = top + 'px'; + this.contentSkeletonEl.appendChild(arrowEl); + nodes.push(arrowEl); + } + this.nowIndicatorEls = nodes; + }; + TimeGrid.prototype.unrenderNowIndicator = function () { + if (this.nowIndicatorEls) { + this.nowIndicatorEls.forEach(core.removeElement); + this.nowIndicatorEls = null; + } + }; + /* Coordinates + ------------------------------------------------------------------------------------------------------------------*/ + TimeGrid.prototype.getTotalSlatHeight = function () { + return this.slatContainerEl.getBoundingClientRect().height; + }; + // 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. + TimeGrid.prototype.computeDateTop = function (when, startOfDayDate) { + if (!startOfDayDate) { + startOfDayDate = core.startOfDay(when); + } + return this.computeTimeTop(core.createDuration(when.valueOf() - startOfDayDate.valueOf())); + }; + // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration). + TimeGrid.prototype.computeTimeTop = function (duration) { + var len = this.slatEls.length; + var dateProfile = this.props.dateProfile; + var slatCoverage = (duration.milliseconds - core.asRoughMs(dateProfile.minTime)) / core.asRoughMs(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); + // 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; + return this.slatPositions.tops[slatIndex] + + this.slatPositions.getHeight(slatIndex) * slatRemainder; + }; + // For each segment in an array, computes and assigns its top and bottom properties + TimeGrid.prototype.computeSegVerticals = function (segs) { + var options = this.context.options; + var eventMinHeight = options.timeGridEventMinHeight; + var i; + var seg; + var dayDate; + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + dayDate = this.props.cells[seg.col].date; + seg.top = this.computeDateTop(seg.start, dayDate); + seg.bottom = Math.max(seg.top + eventMinHeight, this.computeDateTop(seg.end, dayDate)); + } + }; + // Given segments that already have their top/bottom properties computed, applies those values to + // the segments' elements. + TimeGrid.prototype.assignSegVerticals = function (segs) { + var i; + var seg; + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + core.applyStyle(seg.el, this.generateSegVerticalCss(seg)); + } + }; + // Generates an object with CSS properties for the top/bottom coordinates of a segment element + TimeGrid.prototype.generateSegVerticalCss = function (seg) { + return { + top: seg.top, + bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container + }; + }; + /* Sizing + ------------------------------------------------------------------------------------------------------------------*/ + TimeGrid.prototype.buildPositionCaches = function () { + this.buildColPositions(); + this.buildSlatPositions(); + }; + TimeGrid.prototype.buildColPositions = function () { + this.colPositions.build(); + }; + TimeGrid.prototype.buildSlatPositions = function () { + this.slatPositions.build(); + }; + /* Hit System + ------------------------------------------------------------------------------------------------------------------*/ + TimeGrid.prototype.positionToHit = function (positionLeft, positionTop) { + var dateEnv = this.context.dateEnv; + var _a = this, snapsPerSlot = _a.snapsPerSlot, slatPositions = _a.slatPositions, colPositions = _a.colPositions; + var colIndex = colPositions.leftToIndex(positionLeft); + var slatIndex = slatPositions.topToIndex(positionTop); + if (colIndex != null && slatIndex != null) { + var slatTop = slatPositions.tops[slatIndex]; + var slatHeight = slatPositions.getHeight(slatIndex); + var partial = (positionTop - 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 dayDate = this.props.cells[colIndex].date; + var time = core.addDurations(this.props.dateProfile.minTime, core.multiplyDuration(this.snapDuration, snapIndex)); + var start = dateEnv.add(dayDate, time); + var end = dateEnv.add(start, this.snapDuration); + return { + col: colIndex, + dateSpan: { + range: { start: start, end: end }, + allDay: false + }, + dayEl: this.colEls[colIndex], + relativeRect: { + left: colPositions.lefts[colIndex], + right: colPositions.rights[colIndex], + top: slatTop, + bottom: slatTop + slatHeight + } + }; + } + }; + /* Event Drag Visualization + ------------------------------------------------------------------------------------------------------------------*/ + TimeGrid.prototype._renderEventDrag = function (state) { + if (state) { + this.eventRenderer.hideByHash(state.affectedInstances); + if (state.isEvent) { + this.mirrorRenderer.renderSegs(this.context, state.segs, { isDragging: true, sourceSeg: state.sourceSeg }); + } + else { + this.fillRenderer.renderSegs('highlight', this.context, state.segs); + } + } + }; + TimeGrid.prototype._unrenderEventDrag = function (state) { + if (state) { + this.eventRenderer.showByHash(state.affectedInstances); + if (state.isEvent) { + this.mirrorRenderer.unrender(this.context, state.segs, { isDragging: true, sourceSeg: state.sourceSeg }); + } + else { + this.fillRenderer.unrender('highlight', this.context); + } + } + }; + /* Event Resize Visualization + ------------------------------------------------------------------------------------------------------------------*/ + TimeGrid.prototype._renderEventResize = function (state) { + if (state) { + this.eventRenderer.hideByHash(state.affectedInstances); + this.mirrorRenderer.renderSegs(this.context, state.segs, { isResizing: true, sourceSeg: state.sourceSeg }); + } + }; + TimeGrid.prototype._unrenderEventResize = function (state) { + if (state) { + this.eventRenderer.showByHash(state.affectedInstances); + this.mirrorRenderer.unrender(this.context, state.segs, { isResizing: true, sourceSeg: state.sourceSeg }); + } + }; + /* Selection + ------------------------------------------------------------------------------------------------------------------*/ + // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight. + TimeGrid.prototype._renderDateSelection = function (segs) { + if (segs) { + if (this.context.options.selectMirror) { + this.mirrorRenderer.renderSegs(this.context, segs, { isSelecting: true }); + } + else { + this.fillRenderer.renderSegs('highlight', this.context, segs); + } + } + }; + TimeGrid.prototype._unrenderDateSelection = function (segs) { + if (segs) { + if (this.context.options.selectMirror) { + this.mirrorRenderer.unrender(this.context, segs, { isSelecting: true }); + } + else { + this.fillRenderer.unrender('highlight', this.context); + } + } + }; + return TimeGrid; + }(core.DateComponent)); + + var AllDaySplitter = /** @class */ (function (_super) { + __extends(AllDaySplitter, _super); + function AllDaySplitter() { + return _super !== null && _super.apply(this, arguments) || this; + } + AllDaySplitter.prototype.getKeyInfo = function () { + return { + allDay: {}, + timed: {} + }; + }; + AllDaySplitter.prototype.getKeysForDateSpan = function (dateSpan) { + if (dateSpan.allDay) { + return ['allDay']; + } + else { + return ['timed']; + } + }; + AllDaySplitter.prototype.getKeysForEventDef = function (eventDef) { + if (!eventDef.allDay) { + return ['timed']; + } + else if (core.hasBgRendering(eventDef)) { + return ['timed', 'allDay']; + } + else { + return ['allDay']; + } + }; + return AllDaySplitter; + }(core.Splitter)); + + var TIMEGRID_ALL_DAY_EVENT_LIMIT = 5; + var WEEK_HEADER_FORMAT = core.createFormatter({ week: 'short' }); + /* An abstract class for all timegrid-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. + var AbstractTimeGridView = /** @class */ (function (_super) { + __extends(AbstractTimeGridView, _super); + function AbstractTimeGridView() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.splitter = new AllDaySplitter(); + _this.renderSkeleton = core.memoizeRendering(_this._renderSkeleton, _this._unrenderSkeleton); + /* Header Render Methods + ------------------------------------------------------------------------------------------------------------------*/ + // Generates the HTML that will go before the day-of week header cells + _this.renderHeadIntroHtml = function () { + var _a = _this.context, theme = _a.theme, dateEnv = _a.dateEnv, options = _a.options; + var range = _this.props.dateProfile.renderRange; + var dayCnt = core.diffDays(range.start, range.end); + var weekText; + if (options.weekNumbers) { + weekText = dateEnv.format(range.start, WEEK_HEADER_FORMAT); + return '' + + '<th class="fc-axis fc-week-number ' + theme.getClass('widgetHeader') + '" ' + _this.axisStyleAttr() + '>' + + core.buildGotoAnchorHtml(// aside from link, important for matchCellWidths + options, dateEnv, { date: range.start, type: 'week', forceOff: dayCnt > 1 }, core.htmlEscape(weekText) // inner HTML + ) + + '</th>'; + } + else { + return '<th class="fc-axis ' + theme.getClass('widgetHeader') + '" ' + _this.axisStyleAttr() + '></th>'; + } + }; + /* Time Grid Render Methods + ------------------------------------------------------------------------------------------------------------------*/ + // Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column. + _this.renderTimeGridBgIntroHtml = function () { + var theme = _this.context.theme; + return '<td class="fc-axis ' + theme.getClass('widgetContent') + '" ' + _this.axisStyleAttr() + '></td>'; + }; + // Generates the HTML that goes before all other types of cells. + // Affects content-skeleton, mirror-skeleton, highlight-skeleton for both the time-grid and day-grid. + _this.renderTimeGridIntroHtml = function () { + return '<td class="fc-axis" ' + _this.axisStyleAttr() + '></td>'; + }; + /* Day Grid Render Methods + ------------------------------------------------------------------------------------------------------------------*/ + // Generates the HTML that goes before the all-day cells + _this.renderDayGridBgIntroHtml = function () { + var _a = _this.context, theme = _a.theme, options = _a.options; + return '' + + '<td class="fc-axis ' + theme.getClass('widgetContent') + '" ' + _this.axisStyleAttr() + '>' + + '<span>' + // needed for matchCellWidths + core.getAllDayHtml(options) + + '</span>' + + '</td>'; + }; + // Generates the HTML that goes before all other types of cells. + // Affects content-skeleton, mirror-skeleton, highlight-skeleton for both the time-grid and day-grid. + _this.renderDayGridIntroHtml = function () { + return '<td class="fc-axis" ' + _this.axisStyleAttr() + '></td>'; + }; + return _this; + } + AbstractTimeGridView.prototype.render = function (props, context) { + _super.prototype.render.call(this, props, context); + this.renderSkeleton(context); + }; + AbstractTimeGridView.prototype.destroy = function () { + _super.prototype.destroy.call(this); + this.renderSkeleton.unrender(); + }; + AbstractTimeGridView.prototype._renderSkeleton = function (context) { + this.el.classList.add('fc-timeGrid-view'); + this.el.innerHTML = this.renderSkeletonHtml(); + this.scroller = new core.ScrollComponent('hidden', // overflow x + 'auto' // overflow y + ); + var timeGridWrapEl = this.scroller.el; + this.el.querySelector('.fc-body > tr > td').appendChild(timeGridWrapEl); + timeGridWrapEl.classList.add('fc-time-grid-container'); + var timeGridEl = core.createElement('div', { className: 'fc-time-grid' }); + timeGridWrapEl.appendChild(timeGridEl); + this.timeGrid = new TimeGrid(timeGridEl, { + renderBgIntroHtml: this.renderTimeGridBgIntroHtml, + renderIntroHtml: this.renderTimeGridIntroHtml + }); + if (context.options.allDaySlot) { // should we display the "all-day" area? + this.dayGrid = new daygrid.DayGrid(// the all-day subcomponent of this view + this.el.querySelector('.fc-day-grid'), { + renderNumberIntroHtml: this.renderDayGridIntroHtml, + renderBgIntroHtml: this.renderDayGridBgIntroHtml, + renderIntroHtml: this.renderDayGridIntroHtml, + colWeekNumbersVisible: false, + cellWeekNumbersVisible: false + }); + // have the day-grid extend it's coordinate area over the <hr> dividing the two grids + var dividerEl = this.el.querySelector('.fc-divider'); + this.dayGrid.bottomCoordPadding = dividerEl.getBoundingClientRect().height; + } + }; + AbstractTimeGridView.prototype._unrenderSkeleton = function () { + this.el.classList.remove('fc-timeGrid-view'); + this.timeGrid.destroy(); + if (this.dayGrid) { + this.dayGrid.destroy(); + } + this.scroller.destroy(); + }; + /* Rendering + ------------------------------------------------------------------------------------------------------------------*/ + // Builds the HTML skeleton for the view. + // The day-grid and time-grid components will render inside containers defined by this HTML. + AbstractTimeGridView.prototype.renderSkeletonHtml = function () { + var _a = this.context, theme = _a.theme, options = _a.options; + return '' + + '<table class="' + theme.getClass('tableGrid') + '">' + + (options.columnHeader ? + '<thead class="fc-head">' + + '<tr>' + + '<td class="fc-head-container ' + theme.getClass('widgetHeader') + '"> </td>' + + '</tr>' + + '</thead>' : + '') + + '<tbody class="fc-body">' + + '<tr>' + + '<td class="' + theme.getClass('widgetContent') + '">' + + (options.allDaySlot ? + '<div class="fc-day-grid"></div>' + + '<hr class="fc-divider ' + theme.getClass('widgetHeader') + '" />' : + '') + + '</td>' + + '</tr>' + + '</tbody>' + + '</table>'; + }; + /* Now Indicator + ------------------------------------------------------------------------------------------------------------------*/ + AbstractTimeGridView.prototype.getNowIndicatorUnit = function () { + return this.timeGrid.getNowIndicatorUnit(); + }; + // subclasses should implement + // renderNowIndicator(date: DateMarker) { + // } + AbstractTimeGridView.prototype.unrenderNowIndicator = function () { + this.timeGrid.unrenderNowIndicator(); + }; + /* Dimensions + ------------------------------------------------------------------------------------------------------------------*/ + AbstractTimeGridView.prototype.updateSize = function (isResize, viewHeight, isAuto) { + _super.prototype.updateSize.call(this, isResize, viewHeight, isAuto); // will call updateBaseSize. important that executes first + this.timeGrid.updateSize(isResize); + if (this.dayGrid) { + this.dayGrid.updateSize(isResize); + } + }; + // Adjusts the vertical dimensions of the view to the specified values + AbstractTimeGridView.prototype.updateBaseSize = function (isResize, viewHeight, isAuto) { + var _this = this; + var eventLimit; + var scrollerHeight; + var scrollbarWidths; + // make all axis cells line up + this.axisWidth = core.matchCellWidths(core.findElements(this.el, '.fc-axis')); + // hack to give the view some height prior to timeGrid's columns being rendered + // TODO: separate setting height from scroller VS timeGrid. + if (!this.timeGrid.colEls) { + if (!isAuto) { + scrollerHeight = this.computeScrollerHeight(viewHeight); + this.scroller.setHeight(scrollerHeight); + } + return; + } + // set of fake row elements that must compensate when scroller has scrollbars + var noScrollRowEls = core.findElements(this.el, '.fc-row').filter(function (node) { + return !_this.scroller.el.contains(node); + }); + // reset all dimensions back to the original state + this.timeGrid.bottomRuleEl.style.display = 'none'; // will be shown later if this <hr> is necessary + this.scroller.clear(); // sets height to 'auto' and clears overflow + noScrollRowEls.forEach(core.uncompensateScroll); + // limit number of events in the all-day area + if (this.dayGrid) { + this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed + eventLimit = this.context.options.eventLimit; + if (eventLimit && typeof eventLimit !== 'number') { + eventLimit = TIMEGRID_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number + } + if (eventLimit) { + this.dayGrid.limitRows(eventLimit); + } + } + if (!isAuto) { // should we force dimensions of the scroll container? + scrollerHeight = this.computeScrollerHeight(viewHeight); + this.scroller.setHeight(scrollerHeight); + scrollbarWidths = this.scroller.getScrollbarWidths(); + if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars? + // make the all-day and header rows lines up + noScrollRowEls.forEach(function (rowEl) { + core.compensateScroll(rowEl, scrollbarWidths); + }); + // 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(viewHeight); + this.scroller.setHeight(scrollerHeight); + } + // guarantees the same scrollbar widths + this.scroller.lockOverflow(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.timeGrid.bottomRuleEl.style.display = ''; + } + } + }; + // given a desired total height of the view, returns what the height of the scroller should be + AbstractTimeGridView.prototype.computeScrollerHeight = function (viewHeight) { + return viewHeight - + core.subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller + }; + /* Scroll + ------------------------------------------------------------------------------------------------------------------*/ + // Computes the initial pre-configured scroll state prior to allowing the user to change it + AbstractTimeGridView.prototype.computeDateScroll = function (duration) { + var top = this.timeGrid.computeTimeTop(duration); + // 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 + } + return { top: top }; + }; + AbstractTimeGridView.prototype.queryDateScroll = function () { + return { top: this.scroller.getScrollTop() }; + }; + AbstractTimeGridView.prototype.applyDateScroll = function (scroll) { + if (scroll.top !== undefined) { + this.scroller.setScrollTop(scroll.top); + } + }; + // Generates an HTML attribute string for setting the width of the axis, if it is known + AbstractTimeGridView.prototype.axisStyleAttr = function () { + if (this.axisWidth != null) { + return 'style="width:' + this.axisWidth + 'px"'; + } + return ''; + }; + return AbstractTimeGridView; + }(core.View)); + AbstractTimeGridView.prototype.usesMinMaxTime = true; // indicates that minTime/maxTime affects rendering + + var SimpleTimeGrid = /** @class */ (function (_super) { + __extends(SimpleTimeGrid, _super); + function SimpleTimeGrid(timeGrid) { + var _this = _super.call(this, timeGrid.el) || this; + _this.buildDayRanges = core.memoize(buildDayRanges); + _this.slicer = new TimeGridSlicer(); + _this.timeGrid = timeGrid; + return _this; + } + SimpleTimeGrid.prototype.firstContext = function (context) { + context.calendar.registerInteractiveComponent(this, { + el: this.timeGrid.el + }); + }; + SimpleTimeGrid.prototype.destroy = function () { + _super.prototype.destroy.call(this); + this.context.calendar.unregisterInteractiveComponent(this); + }; + SimpleTimeGrid.prototype.render = function (props, context) { + var dateEnv = this.context.dateEnv; + var dateProfile = props.dateProfile, dayTable = props.dayTable; + var dayRanges = this.dayRanges = this.buildDayRanges(dayTable, dateProfile, dateEnv); + this.timeGrid.receiveProps(__assign({}, this.slicer.sliceProps(props, dateProfile, null, context.calendar, this.timeGrid, dayRanges), { dateProfile: dateProfile, cells: dayTable.cells[0] }), context); + }; + SimpleTimeGrid.prototype.renderNowIndicator = function (date) { + this.timeGrid.renderNowIndicator(this.slicer.sliceNowDate(date, this.timeGrid, this.dayRanges), date); + }; + SimpleTimeGrid.prototype.buildPositionCaches = function () { + this.timeGrid.buildPositionCaches(); + }; + SimpleTimeGrid.prototype.queryHit = function (positionLeft, positionTop) { + var rawHit = this.timeGrid.positionToHit(positionLeft, positionTop); + if (rawHit) { + return { + component: this.timeGrid, + dateSpan: rawHit.dateSpan, + dayEl: rawHit.dayEl, + rect: { + left: rawHit.relativeRect.left, + right: rawHit.relativeRect.right, + top: rawHit.relativeRect.top, + bottom: rawHit.relativeRect.bottom + }, + layer: 0 + }; + } + }; + return SimpleTimeGrid; + }(core.DateComponent)); + function buildDayRanges(dayTable, dateProfile, dateEnv) { + var ranges = []; + for (var _i = 0, _a = dayTable.headerDates; _i < _a.length; _i++) { + var date = _a[_i]; + ranges.push({ + start: dateEnv.add(date, dateProfile.minTime), + end: dateEnv.add(date, dateProfile.maxTime) + }); + } + return ranges; + } + var TimeGridSlicer = /** @class */ (function (_super) { + __extends(TimeGridSlicer, _super); + function TimeGridSlicer() { + return _super !== null && _super.apply(this, arguments) || this; + } + TimeGridSlicer.prototype.sliceRange = function (range, dayRanges) { + var segs = []; + for (var col = 0; col < dayRanges.length; col++) { + var segRange = core.intersectRanges(range, dayRanges[col]); + if (segRange) { + segs.push({ + start: segRange.start, + end: segRange.end, + isStart: segRange.start.valueOf() === range.start.valueOf(), + isEnd: segRange.end.valueOf() === range.end.valueOf(), + col: col + }); + } + } + return segs; + }; + return TimeGridSlicer; + }(core.Slicer)); + + var TimeGridView = /** @class */ (function (_super) { + __extends(TimeGridView, _super); + function TimeGridView() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.buildDayTable = core.memoize(buildDayTable); + return _this; + } + TimeGridView.prototype.render = function (props, context) { + _super.prototype.render.call(this, props, context); // for flags for updateSize. also _renderSkeleton/_unrenderSkeleton + var _a = this.props, dateProfile = _a.dateProfile, dateProfileGenerator = _a.dateProfileGenerator; + var nextDayThreshold = context.nextDayThreshold; + var dayTable = this.buildDayTable(dateProfile, dateProfileGenerator); + var splitProps = this.splitter.splitProps(props); + if (this.header) { + this.header.receiveProps({ + dateProfile: dateProfile, + dates: dayTable.headerDates, + datesRepDistinctDays: true, + renderIntroHtml: this.renderHeadIntroHtml + }, context); + } + this.simpleTimeGrid.receiveProps(__assign({}, splitProps['timed'], { dateProfile: dateProfile, + dayTable: dayTable }), context); + if (this.simpleDayGrid) { + this.simpleDayGrid.receiveProps(__assign({}, splitProps['allDay'], { dateProfile: dateProfile, + dayTable: dayTable, + nextDayThreshold: nextDayThreshold, isRigid: false }), context); + } + this.startNowIndicator(dateProfile, dateProfileGenerator); + }; + TimeGridView.prototype._renderSkeleton = function (context) { + _super.prototype._renderSkeleton.call(this, context); + if (context.options.columnHeader) { + this.header = new core.DayHeader(this.el.querySelector('.fc-head-container')); + } + this.simpleTimeGrid = new SimpleTimeGrid(this.timeGrid); + if (this.dayGrid) { + this.simpleDayGrid = new daygrid.SimpleDayGrid(this.dayGrid); + } + }; + TimeGridView.prototype._unrenderSkeleton = function () { + _super.prototype._unrenderSkeleton.call(this); + if (this.header) { + this.header.destroy(); + } + this.simpleTimeGrid.destroy(); + if (this.simpleDayGrid) { + this.simpleDayGrid.destroy(); + } + }; + TimeGridView.prototype.renderNowIndicator = function (date) { + this.simpleTimeGrid.renderNowIndicator(date); + }; + return TimeGridView; + }(AbstractTimeGridView)); + function buildDayTable(dateProfile, dateProfileGenerator) { + var daySeries = new core.DaySeries(dateProfile.renderRange, dateProfileGenerator); + return new core.DayTable(daySeries, false); + } + + var main = core.createPlugin({ + defaultView: 'timeGridWeek', + views: { + timeGrid: { + class: TimeGridView, + allDaySlot: true, + slotDuration: '00:30:00', + slotEventOverlap: true // a bad name. confused with overlap/constraint system + }, + timeGridDay: { + type: 'timeGrid', + duration: { days: 1 } + }, + timeGridWeek: { + type: 'timeGrid', + duration: { weeks: 1 } + } + } + }); + + exports.AbstractTimeGridView = AbstractTimeGridView; + exports.TimeGrid = TimeGrid; + exports.TimeGridSlicer = TimeGridSlicer; + exports.TimeGridView = TimeGridView; + exports.buildDayRanges = buildDayRanges; + exports.buildDayTable = buildDayTable; + exports.default = main; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); diff --git a/addons/web/static/src/js/views/calendar/calendar_model.js b/addons/web/static/src/js/views/calendar/calendar_model.js index 7ed57ceafa9..78fa6fdaf5e 100644 --- a/addons/web/static/src/js/views/calendar/calendar_model.js +++ b/addons/web/static/src/js/views/calendar/calendar_model.js @@ -356,7 +356,7 @@ return AbstractModel.extend({ return this._rpc({ model: this.modelName, method: 'write', - args: [[record.id], data], + args: [[parseInt(record.id, 10)], data], context: context }); }, @@ -419,11 +419,22 @@ return AbstractModel.extend({ * @returns {Object} */ _getFullCalendarOptions: function () { + var format12Hour = { + hour: 'numeric', + minute: '2-digit', + omitZeroMinute: true, + meridiem: 'short' + }; + var format24Hour = { + hour: 'numeric', + minute: '2-digit', + hour12: false, + }; return { - defaultView: (this.mode === "month")? "month" : ((this.mode === "week")? "agendaWeek" : ((this.mode === "day")? "agendaDay" : "agendaWeek")), + defaultView: (this.mode === "month")? "dayGridMonth" : ((this.mode === "week")? "timeGridWeek" : ((this.mode === "day")? "timeGridDay" : "timeGridWeek")), header: false, selectable: this.creatable && this.create_right, - selectHelper: true, + selectMirror: true, editable: this.editable, droppable: true, navLinks: false, @@ -434,14 +445,18 @@ return AbstractModel.extend({ nowIndicator: true, weekNumbers: true, weekNumbersWithinDays: true, - weekNumberTitle: _t("Week") + " ", + weekNumberCalculation: function (date) { + // Since FullCalendar v4 ISO 8601 week date is preferred so we force the old system + return moment(date).week(); + }, + weekLabel: _t("Week"), allDayText: _t("All day"), monthNames: moment.months(), monthNamesShort: moment.monthsShort(), dayNames: moment.weekdays(), dayNamesShort: moment.weekdaysShort(), firstDay: this.week_start, - slotLabelFormat: _t.database.parameters.time_format.search("%H") != -1 ? 'H:mm': 'h(:mm)a', + slotLabelFormat: _t.database.parameters.time_format.search("%H") !== -1 ? format24Hour : format12Hour, }; }, /** @@ -733,10 +748,10 @@ return AbstractModel.extend({ } var r = { 'record': evt, - 'start': date_start, - 'end': date_stop, - 'r_start': date_start.clone(), - 'r_end': date_stop.clone(), + 'start': date_start.local(true).toDate(), + 'end': date_stop.local(true).toDate(), + 'r_start': date_start.clone().local(true).toDate(), + 'r_end': date_stop.clone().local(true).toDate(), 'title': the_title, 'allDay': all_day, 'id': evt.id, @@ -754,8 +769,8 @@ return AbstractModel.extend({ // allow to resize in month mode r.reset_allday = r.allDay; r.allDay = true; - r.start = date_start.format('YYYY-MM-DD'); - r.end = date_stop.startOf('day').format('YYYY-MM-DD'); + r.start = date_start.toDate(); + r.end = date_stop.startOf('day').toDate(); r.showTime = true; } diff --git a/addons/web/static/src/js/views/calendar/calendar_popover.js b/addons/web/static/src/js/views/calendar/calendar_popover.js index 9b4af4bb065..8bfadbc7c4d 100644 --- a/addons/web/static/src/js/views/calendar/calendar_popover.js +++ b/addons/web/static/src/js/views/calendar/calendar_popover.js @@ -112,7 +112,7 @@ var CalendarPopover = Widget.extend(WidgetAdapterMixin, StandaloneFieldManagerMi var field = { name: fieldName, string: displayFieldInfo.attrs.string || fieldInfo.string, - value: self.event.record[fieldName], + value: self.event.extendedProps.record[fieldName], type: fieldInfo.type, }; if (field.type === 'selection') { @@ -199,7 +199,7 @@ var CalendarPopover = Widget.extend(WidgetAdapterMixin, StandaloneFieldManagerMi ev.preventDefault(); this.trigger_up('edit_event', { id: this.event.id, - title: this.event.record.display_name, + title: this.event.extendedProps.record.display_name, }); }, /** diff --git a/addons/web/static/src/js/views/calendar/calendar_renderer.js b/addons/web/static/src/js/views/calendar/calendar_renderer.js index 7f281e474c9..0ad1c4c0c6b 100644 --- a/addons/web/static/src/js/views/calendar/calendar_renderer.js +++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js @@ -16,9 +16,9 @@ var _t = core._t; var qweb = core.qweb; var scales = { - day: 'agendaDay', - week: 'agendaWeek', - month: 'month' + day: 'timeGridDay', + week: 'timeGridWeek', + month: 'dayGridMonth' }; var SidebarFilterM2O = relational_fields.FieldMany2One.extend({ @@ -78,6 +78,7 @@ var SidebarFilter = Widget.extend(FieldManagerMixin, { { mode: 'edit', attrs: { + string: _t(self.fields[self.fieldName].string), placeholder: "+ " + _.str.sprintf(_t("Add %s"), self.title), can_create: false }, @@ -192,6 +193,7 @@ return AbstractRenderer.extend({ this.hideDate = params.hideDate; this.hideTime = params.hideTime; this.canDelete = params.canDelete; + this._isInDOM = false; }, /** * @override @@ -209,22 +211,29 @@ return AbstractRenderer.extend({ * @override */ on_attach_callback: function () { + this._super(...arguments); + this._isInDOM = true; if (config.device.isMobile) { this.$el.height($(window).height() - this.$el.offset().top); } - var scrollTop = this.$calendar.find('.fc-scroller').scrollTop(); - if (scrollTop) { - this.$calendar.fullCalendar('reinitView'); - } else { - this.$calendar.fullCalendar('render'); - } + // BUG Test ???? + // this.$el.height($(window).height() - this.$el.offset().top); + this.calendar.render(); + this._renderCalendar(); + }, + /** + * Called when the field is detached from the DOM. + */ + on_detach_callback: function () { + this._super(...arguments); + this._isInDOM = false; }, /** * @override */ destroy: function () { - if (this.$calendar) { - this.$calendar.fullCalendar('destroy'); + if (this.calendar) { + this.calendar.destroy(); } if (this.$small_calendar) { this.$small_calendar.datepicker('destroy'); @@ -296,9 +305,9 @@ return AbstractRenderer.extend({ * @override */ getLocalState: function () { - var $fcScroller = this.$calendar.find('.fc-scroller'); + var fcScroller = this.calendarElement.querySelector('.fc-scroller'); return { - scrollPosition: $fcScroller.scrollTop(), + scrollPosition: fcScroller.scrollTop, }; }, /** @@ -306,8 +315,8 @@ return AbstractRenderer.extend({ */ setLocalState: function (localState) { if (localState.scrollPosition) { - var $fcScroller = this.$calendar.find('.fc-scroller'); - $fcScroller.scrollTop(localState.scrollPosition); + var fcScroller = this.calendarElement.querySelector('.fc-scroller'); + fcScroller.scrollTop = localState.scrollPosition; } }, @@ -325,14 +334,15 @@ return AbstractRenderer.extend({ var self = this; var touchStartX; var touchEndX; - this.$calendar.on('touchstart', function (event) { - touchStartX = event.originalEvent.touches[0].pageX; + this.calendarElement.addEventListener('touchstart', function (event) { + self.isSwipeEnabled = true; + touchStartX = event.touches[0].pageX; }); - this.$calendar.on('touchend', function (event) { - touchEndX = event.originalEvent.changedTouches[0].pageX; + this.calendarElement.addEventListener('touchend', function (event) { if (!self.isSwipeEnabled) { return; } + touchEndX = event.changedTouches[0].pageX; if (touchStartX - touchEndX > 100) { self.trigger_up('next'); } else if (touchStartX - touchEndX < -100) { @@ -341,14 +351,43 @@ return AbstractRenderer.extend({ }); }, /** + * Convert the new format of Event from FullCalendar V4 to a Event FullCalendar V3 + * @param fc4Event + * @return {Object} FullCalendar V3 Object Event + * @private + */ + _convertEventToFC3Event: function (fc4Event) { + var event = fc4Event; + if (!moment.isMoment(fc4Event.start)) { + event = { + id: fc4Event.id, + title: fc4Event.title, + start: moment(fc4Event.start).utcOffset(0, true), + end: fc4Event.end && moment(fc4Event.end).utcOffset(0, true), + allDay: fc4Event.allDay, + color: fc4Event.color, + }; + if (fc4Event.extendedProps) { + event = Object.assign({}, event, { + r_start: fc4Event.extendedProps.r_start && moment(fc4Event.extendedProps.r_start).utcOffset(0, true), + r_end: fc4Event.extendedProps.r_end && moment(fc4Event.extendedProps.r_end).utcOffset(0, true), + reset_allday: fc4Event.extendedProps.reset_allday, + record: fc4Event.extendedProps.record, + attendees: fc4Event.extendedProps.attendees, + }); + } + } + return event; + }, + /** * @param {any} event * @returns {string} the html for the rendered event */ _eventRender: function (event) { var qweb_context = { event: event, - record: event.record, - color: this.getColor(event.color_index), + record: event.extendedProps.record, + color: this.getColor(event.extendedProps.color_index), }; this.qweb_context = qweb_context; if (_.isEmpty(qweb_context.record)) { @@ -379,43 +418,50 @@ return AbstractRenderer.extend({ _initCalendar: function () { var self = this; - this.$calendar = this.$(".o_calendar_widget"); - - // This seems like a workaround but apparently passing the locale - // in the options is not enough. We should initialize it beforehand + this.calendarElement = this.$(".o_calendar_widget")[0]; var locale = moment.locale(); - $.fullCalendar.locale(locale); - //Documentation here : http://arshaw.com/fullcalendar/docs/ var fc_options = $.extend({}, this.state.fc_options, { - eventDrop: function (event) { + plugins: [ + 'moment', + 'interaction', + 'dayGrid', + 'timeGrid' + ], + eventDrop: function (eventDropInfo) { + var event = self._convertEventToFC3Event(eventDropInfo.event); self.trigger_up('dropRecord', event); }, - eventResize: function (event) { + eventResize: function (eventResizeInfo) { + self.isSwipeEnabled = false; self._unselectEvent(); + var event = self._convertEventToFC3Event(eventResizeInfo.event); self.trigger_up('updateRecord', event); }, - eventClick: function (eventData, ev) { + eventClick: function (eventClickInfo) { + var eventData = eventClickInfo.event; self._unselectEvent(); - self.$calendar.find(_.str.sprintf('[data-event-id=%s]', eventData.id)).addClass('o_cw_custom_highlight'); - self._renderEventPopover(eventData, $(ev.currentTarget)); + $(self.calendarElement).find(_.str.sprintf('[data-event-id=%s]', eventData.id)).addClass('o_cw_custom_highlight'); + self._renderEventPopover(eventData, $(eventClickInfo.el)); }, - select: function (startDate, endDate) { - self.isSwipeEnabled = false; + select: function (selectionInfo) { // Clicking on the view, dispose any visible popover. Otherwise create a new event. if (self.$('.o_cw_popover').length) { self._unselectEvent(); } else { - var data = {start: startDate, end: endDate}; + var data = {start: selectionInfo.start, end: selectionInfo.end}; if (self.state.context.default_name) { data.title = self.state.context.default_name; } - self.trigger_up('openCreate', data); + self.trigger_up('openCreate', self._convertEventToFC3Event(data)); } - self.$calendar.fullCalendar('unselect'); + self.calendar.unselect(); }, - eventRender: function (event, element, view) { + eventRender: function (info) { self.isSwipeEnabled = false; + var event = info.event; + var element = $(info.el); + var view = info.view; var $render = $(self._eventRender(event)); element.find('.fc-content').html($render.html()); element.addClass($render.attr('class')); @@ -426,17 +472,17 @@ return AbstractRenderer.extend({ element.find('.fc-content').after($('<div/>', {class: 'fc-bg'})); } - if (view.name === 'month' && event.record) { - var start = event.r_start || event.start; - var end = event.r_end || event.end; + if (view.type === 'dayGridMonth' && event.extendedProps.record) { + var start = event.extendedProps.r_start || event.start; + var end = event.extendedProps.r_end || event.end; // Detect if the event occurs in just one day // note: add & remove 1 min to avoid issues with 00:00 - var isSameDayEvent = start.clone().add(1, 'minute').isSame(end.clone().subtract(1, 'minute'), 'day'); - if (!event.record.allday && isSameDayEvent) { + var isSameDayEvent = moment(start).clone().add(1, 'minute').isSame(moment(end).clone().subtract(1, 'minute'), 'day'); + if (!event.extendedProps.record.allday && isSameDayEvent) { // For month view: do not show background for non allday, single day events element.addClass('o_cw_nobg'); - if (event.showTime && !self.hideTime) { - const displayTime = start.format(self._getDbTimeFormat()); + if (event.extendedProps.showTime && !self.hideTime) { + const displayTime = moment(start).clone().format(self._getDbTimeFormat()); element.find('.fc-content .fc-time').text(displayTime); } } @@ -447,32 +493,36 @@ return AbstractRenderer.extend({ self.trigger_up('edit_event', {id: event.id}); }); }, - eventAfterAllRender: function () { - self.isSwipeEnabled = true; + eventPositioned: function (){ + self.isSwipeEnabled = false; }, - viewRender: function (view) { - // compute mode from view.name which is either 'month', 'agendaWeek' or 'agendaDay' - var mode = view.name === 'month' ? 'month' : (view.name === 'agendaWeek' ? 'week' : 'day'); + datesRender: function (info) { + // compute mode from view.type which is either 'dayGridMonth', 'timeGridWeek' or 'timeGridDay' + var mode = info.view.type === 'dayGridMonth' ? 'month' : (info.view.type === 'timeGridWeek' ? 'week' : 'day'); self.trigger_up('viewUpdated', { mode: mode, - title: view.title, + title: info.view.title, }); }, // Add/Remove a class on hover to style multiple days events. // The css ":hover" selector can't be used because these events // are rendered using multiple elements. - eventMouseover: function (eventData) { - self.$calendar.find(_.str.sprintf('[data-event-id=%s]', eventData.id)).addClass('o_cw_custom_hover'); + eventMouseEnter: function (mouseEnterInfo) { + $(self.calendarElement).find(_.str.sprintf('[data-event-id=%s]', mouseEnterInfo.event.id)).addClass('o_cw_custom_hover'); }, - eventMouseout: function (eventData) { - self.$calendar.find(_.str.sprintf('[data-event-id=%s]', eventData.id)).removeClass('o_cw_custom_hover'); + eventMouseLeave: function (mouseLeaveInfo) { + if (!mouseLeaveInfo.event.id) { + return; + } + $(self.calendarElement).find(_.str.sprintf('[data-event-id=%s]', mouseLeaveInfo.event.id)).removeClass('o_cw_custom_hover'); }, - eventDragStart: function (eventData) { - self.$calendar.find(_.str.sprintf('[data-event-id=%s]', eventData.id)).addClass('o_cw_custom_hover'); + eventDragStart: function (mouseDragInfo) { + $(self.calendarElement).find(_.str.sprintf('[data-event-id=%s]', mouseDragInfo.event.id)).addClass('o_cw_custom_hover'); self._unselectEvent(); }, - eventResizeStart: function (eventData) { - self.$calendar.find(_.str.sprintf('[data-event-id=%s]', eventData.id)).addClass('o_cw_custom_hover'); + eventResizeStart: function (mouseResizeInfo) { + self.isSwipeEnabled = false; + $(self.calendarElement).find(_.str.sprintf('[data-event-id=%s]', mouseResizeInfo.event.id)).addClass('o_cw_custom_hover'); self._unselectEvent(); }, eventLimitClick: function () { @@ -483,23 +533,23 @@ return AbstractRenderer.extend({ self._render(); }, views: { - day: { - columnFormat: 'LL' + timeGridDay: { + columnHeaderFormat: 'LL' }, - week: { - columnFormat: 'ddd D' + timeGridWeek: { + columnHeaderFormat: 'ddd D' }, - month: { - columnFormat: config.device.isMobile ? 'ddd' : 'dddd' + dayGridMonth: { + columnHeaderFormat: config.device.isMobile ? 'ddd' : 'dddd' } }, height: 'parent', unselectAuto: false, - isRTL: _t.database.parameters.direction === "rtl", - locale: locale, // reset locale when fullcalendar has already been instanciated before now + dir: _t.database.parameters.direction, + locale: locale, }); - this.$calendar.fullCalendar(fc_options); + this.calendar = new FullCalendar.Calendar(this.calendarElement, fcOptions); }, /** * Initialize the mini calendar in the sidebar @@ -551,22 +601,10 @@ return AbstractRenderer.extend({ * @returns {Promise} */ _render: function () { - var $calendar = this.$calendar; - var $fc_view = $calendar.find('.fc-view'); - var scrollPosition = $fc_view.scrollLeft(); - - $fc_view.scrollLeft(0); - $calendar.fullCalendar('unselect'); - - if (scales[this.state.scale] !== $calendar.data('fullCalendar').getView().type) { - $calendar.fullCalendar('changeView', scales[this.state.scale]); - } - - if (this.target_date !== this.state.target_date.toString()) { - $calendar.fullCalendar('gotoDate', moment(this.state.target_date)); - this.target_date = this.state.target_date.toString(); + this.$('.o_calendar_view')[0].prepend(this.calendarElement); + if (this._isInDOM) { + this._renderCalendar(); } - this.$small_calendar.datepicker("setDate", this.state.highlight_date.toDate()) .find('.o_selected_range') .removeClass('o_color o_selected_range'); @@ -581,23 +619,41 @@ return AbstractRenderer.extend({ $a.not('.ui-state-active').addClass('o_color'); }); - $fc_view.scrollLeft(scrollPosition); - - this._unselectEvent(); var filterProm = this._renderFilters(); - this._renderEvents(); - this.$calendar.prependTo(this.$('.o_calendar_view')); return Promise.all([filterProm, this._super.apply(this, arguments)]); }, /** + * Render the specific code for the FullCalendar when it's in the DOM + * + * @private + */ + _renderCalendar() { + this.calendar.unselect(); + + if (scales[this.state.scale] !== this.calendar.view.type) { + this.calendar.changeView(scales[this.state.scale]); + } + + if (this.target_date !== this.state.target_date.toString()) { + this.calendar.gotoDate(moment(this.state.target_date).toDate()); + this.target_date = this.state.target_date.toString(); + } + + this._unselectEvent(); + this._renderEvents(); + // this._scrollToScrollTime(); + }, + /** * Render all events * * @private */ _renderEvents: function () { - this.$calendar.fullCalendar('removeEvents'); - this.$calendar.fullCalendar('addEventSource', this.state.data); + this.calendar.getEvents().forEach(function(event) { + event.remove(); + }); + this.calendar.addEventSource(this.state.data); }, /** * Render all filters @@ -612,7 +668,7 @@ return AbstractRenderer.extend({ filter.destroy(); }); if (this.state.fullWidth) { - return; + return Promise.resolve(); } return this._renderFiltersOneByOne(); }, @@ -699,12 +755,12 @@ return AbstractRenderer.extend({ canDelete: this.canDelete, }; - var start = moment(eventData.r_start || eventData.start); - var end = moment(eventData.r_end || eventData.end); + var start = moment((eventData.extendedProps && eventData.extendedProps.r_start) || eventData.start); + var end = moment((eventData.extendedProps && eventData.extendedProps.r_end) || eventData.end); var isSameDayEvent = start.clone().add(1, 'minute').isSame(end.clone().subtract(1, 'minute'), 'day'); // Do not display timing if the event occur across multiple days. Otherwise use user's timing preferences - if (!this.hideTime && !eventData.record.allday && isSameDayEvent) { + if (!this.hideTime && !eventData.extendedProps.record.allday && isSameDayEvent) { var dbTimeFormat = this._getDbTimeFormat(); context.eventTime.time = start.clone().format(dbTimeFormat) + ' - ' + end.clone().format(dbTimeFormat); @@ -729,9 +785,9 @@ return AbstractRenderer.extend({ context.eventDate.date = isSameDayEvent ? start.clone().format('dddd, LL') : start.clone().format('LL') + ' - ' + end.clone().format('LL'); } - if (eventData.record.allday && isSameDayEvent) { + if (eventData.extendedProps.record.allday && isSameDayEvent) { context.eventDate.duration = _t("All day"); - } else if (eventData.record.allday && !isSameDayEvent) { + } else if (eventData.extendedProps.record.allday && !isSameDayEvent) { var daysLocaleData = moment.localeData(); var days = moment.duration(end.diff(start)).days(); context.eventDate.duration = daysLocaleData.relativeTime(days, true, 'dd'); @@ -756,8 +812,8 @@ return AbstractRenderer.extend({ }, trigger: 'manual', html: true, - title: eventData.record.display_name, - template: qweb.render('CalendarView.event.popover.placeholder', {color: this.getColor(eventData.color_index)}), + title: eventData.extendedProps.record.display_name, + template: qweb.render('CalendarView.event.popover.placeholder', {color: this.getColor(eventData.extendedProps.color_index)}), container: eventData.allDay ? '.fc-view' : '.fc-scroller', } }, @@ -781,6 +837,14 @@ return AbstractRenderer.extend({ }).popover('show'); }); }, + /** + * Scroll to the time set in the FullCalendar parameter + * @private + */ + _scrollToScrollTime: function () { + var scrollTime = this.calendar.getOption('scrollTime'); + this.calendar.scrollToTime(scrollTime); + }, //-------------------------------------------------------------------------- // Handlers @@ -812,7 +876,7 @@ return AbstractRenderer.extend({ */ _onDeleteEvent: function (event) { this._unselectEvent(); - this.trigger_up('deleteRecord', {id: event.data.id}); + this.trigger_up('deleteRecord', {id: parseInt(event.data.id, 10)}); }, }); diff --git a/addons/web/static/src/js/views/calendar/calendar_view.js b/addons/web/static/src/js/views/calendar/calendar_view.js index dfdb41a54e0..881012952b7 100644 --- a/addons/web/static/src/js/views/calendar/calendar_view.js +++ b/addons/web/static/src/js/views/calendar/calendar_view.js @@ -22,8 +22,20 @@ var fieldsToGather = [ var CalendarView = AbstractView.extend({ display_name: _lt('Calendar'), icon: 'fa-calendar', - jsLibs: ['/web/static/lib/fullcalendar/js/fullcalendar.js'], - cssLibs: ['/web/static/lib/fullcalendar/css/fullcalendar.css'], + jsLibs: [ + '/web/static/lib/fullcalendar/core/main.js', + '/web/static/lib/fullcalendar/interaction/main.js', + '/web/static/lib/fullcalendar/moment/main.js', + '/web/static/lib/fullcalendar/daygrid/main.js', + '/web/static/lib/fullcalendar/timegrid/main.js', + '/web/static/lib/fullcalendar/list/main.js' + ], + cssLibs: [ + '/web/static/lib/fullcalendar/core/main.css', + '/web/static/lib/fullcalendar/daygrid/main.css', + '/web/static/lib/fullcalendar/timegrid/main.css', + '/web/static/lib/fullcalendar/list/main.css' + ], config: _.extend({}, AbstractView.prototype.config, { Model: CalendarModel, Controller: CalendarController, diff --git a/addons/web/static/src/scss/web_calendar.scss b/addons/web/static/src/scss/web_calendar.scss index 97c2ef44f5c..cb98cc47129 100644 --- a/addons/web/static/src/scss/web_calendar.scss +++ b/addons/web/static/src/scss/web_calendar.scss @@ -90,7 +90,7 @@ $o-cw-filter-avatar-size: 20px; background: gray('400'); } - .fc-agenda-view .fc-day-grid .fc-row .fc-content-skeleton { + .fc-timeGrid-view .fc-day-grid .fc-row .fc-content-skeleton { padding: .5em; } @@ -107,7 +107,7 @@ $o-cw-filter-avatar-size: 20px; // ==================================== // ====== Both Day and Week agenda - .fc-agendaDay-view, .fc-agendaWeek-view { + .fc-timeGridDay-view, .fc-timeGridWeek-view { .fc-axis { padding-left: $o-horizontal-padding; } @@ -167,7 +167,7 @@ $o-cw-filter-avatar-size: 20px; box-shadow: inset 0 1px 0 gray('400'); } - .fc-day.fc-widget-content.fc-today.fc-state-highlight { + .fc-day.fc-widget-content.fc-today { border-left-color: rgba($o-cw-color-today, 0.3); border-right-color: rgba($o-cw-color-today, 0.3); background: rgba($o-cw-color-today, 0.05 ); @@ -177,7 +177,7 @@ $o-cw-filter-avatar-size: 20px; // Prevent events with similar color to visually overlap each other box-shadow: 0 0 0 1px white; - &.fc-v-event { + &.fc-event:not(.fc-h-event) { border-width: 3px 0 0; &.fc-not-start { @@ -197,7 +197,7 @@ $o-cw-filter-avatar-size: 20px; } // ====== Day only - .fc-agendaDay-view .fc-event { + .fc-timeGridDay-view .fc-event { padding: 10px; font-size: 14px; @@ -208,13 +208,13 @@ $o-cw-filter-avatar-size: 20px; } // ====== Week only - .fc-agendaWeek-view { + .fc-timeGridWeek-view { .fc-now-indicator { left: $o-horizontal-padding; } // Expand tiny events on hover/select - .fc-v-event.o_cw_custom_highlight, .fc-v-event.o_cw_custom_hover { + .fc-event:not(.fc-h-event).o_cw_custom_highlight, .fc-event:not(.fc-h-event).o_cw_custom_hover { transition: margin .1s .3s, left .1s .3s, right .1s .3s; margin: 0!important; right: 1px!important; @@ -223,7 +223,7 @@ $o-cw-filter-avatar-size: 20px; } // ====== Month only - .fc-month-view { + .fc-dayGridMonth-view { padding-left: $o-horizontal-padding; .fc-widget-header { @@ -319,11 +319,11 @@ $o-cw-filter-avatar-size: 20px; } // ====== RTL layout(s) &.fc-rtl { - .fc-agendaWeek-view .fc-event { + .fc-timeGrid-view .fc-event { border-width: 0 3px 0 0; } - .fc-month-view .fc-event { + .fc-dayGridMonth-view .fc-event { border-width: 0 3px 0 0; &.fc-not-start { diff --git a/addons/web/static/tests/helpers/test_utils_create.js b/addons/web/static/tests/helpers/test_utils_create.js index 8130e74ba03..494a0aa2436 100644 --- a/addons/web/static/tests/helpers/test_utils_create.js +++ b/addons/web/static/tests/helpers/test_utils_create.js @@ -106,7 +106,12 @@ odoo.define('web.test_utils_create', function (require) { return calendar; } const viewElements = [...document.getElementById('qunit-fixture').children]; + // prepend reset the scrollTop to zero so we restore it manually + let fcScroller = document.querySelector('.fc-scroller'); + const scrollPosition = fcScroller.scrollTop; viewElements.forEach(el => document.body.prepend(el)); + fcScroller = document.querySelector('.fc-scroller'); + fcScroller.scrollTop = scrollPosition; const destroy = calendar.destroy; calendar.destroy = () => { diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js index 5865d4be80b..1a375cbf5f5 100644 --- a/addons/web/static/tests/views/calendar_tests.js +++ b/addons/web/static/tests/views/calendar_tests.js @@ -351,7 +351,7 @@ QUnit.module('Views', { }, }); - assert.ok(calendar.$('.fc-month-view').length, "should display in month mode"); + assert.ok(calendar.$('.fc-dayGridMonth-view').length, "should display in month mode"); // click on an existing event to open the formViewDialog @@ -654,7 +654,7 @@ QUnit.module('Views', { assert.strictEqual($newevent.find('.o_event_title').text(), "new event", "should display the new event with title"); - assert.deepEqual($newevent.data('fcSeg').event.record, + assert.deepEqual($newevent[0].fcSeg.eventRange.def.extendedProps.record, { display_name: "new event", start: fieldUtils.parse.datetime("2016-12-13 06:00:00", this.data.event.fields.start, {isUTC: true}), @@ -972,7 +972,7 @@ QUnit.module('Views', { assert.strictEqual($newevent.find('.o_event_title').text(), "new event", "should display the new event with title"); - assert.deepEqual($newevent.data('fcSeg').event.record, + assert.deepEqual($newevent[0].fcSeg.eventRange.def.extendedProps.record, { display_name: "new event", start: fieldUtils.parse.datetime("2016-12-13 06:00:00", this.data.event.fields.start, {isUTC: true}), @@ -1083,7 +1083,7 @@ QUnit.module('Views', { assert.strictEqual($newevent.find('.o_event_title').text(), "new event", "should display the new event with title"); - assert.deepEqual($newevent.data('fcSeg').event.record, + assert.deepEqual($newevent[0].fcSeg.eventRange.def.extendedProps.record, { display_name: "new event", start: fieldUtils.parse.datetime("2016-12-13 06:00:00", this.data.event.fields.start, {isUTC: true}), @@ -1256,7 +1256,7 @@ QUnit.module('Views', { assert.strictEqual($newevent.find('.o_event_title').text(), "new event", "should display the new event with title"); - assert.deepEqual($newevent.data('fcSeg').event.record, + assert.deepEqual($newevent[0].fcSeg.eventRange.def.extendedProps.record, { display_name: "new event", start: fieldUtils.parse.datetime("2016-12-13 06:00:00", this.data.event.fields.start, {isUTC: true}), @@ -1373,7 +1373,7 @@ QUnit.module('Views', { assert.hasAttrValue($newevent.parent(), 'colspan', "2", "should appear over two days."); - assert.deepEqual($newevent.data('fcSeg').event.record, + assert.deepEqual($newevent[0].fcSeg.eventRange.def.extendedProps.record, { display_name: "new event", start: fieldUtils.parse.datetime("2016-12-14 00:00:00", this.data.event.fields.start, {isUTC: true}), @@ -1552,7 +1552,7 @@ QUnit.module('Views', { assert.hasAttrValue($newevent.parent(), 'colspan', "2", "should appear over two days."); - assert.deepEqual($newevent.data('fcSeg').event.record, { + assert.deepEqual($newevent[0].fcSeg.eventRange.def.extendedProps.record, { display_name: "new event", start: fieldUtils.parse.datetime("2016-12-14 05:00:00", this.data.event.fields.start, {isUTC: true}), stop: fieldUtils.parse.datetime("2016-12-15 17:00:00", this.data.event.fields.stop, {isUTC: true}), @@ -1588,25 +1588,25 @@ QUnit.module('Views', { }, }); - assert.containsOnce(calendar, '.fc-agendaWeek-view', "should be in week mode"); + assert.containsOnce(calendar, '.fc-timeGridWeek-view', "should be in week mode"); assert.containsN(calendar, '.fc-event', 9, "should display 9 events on the week (4 event + 5 days event)"); await testUtils.dom.click(calendar.$('.o_calendar_mini a:contains(19)')); // Clicking on a day in another week should switch to the other week view - assert.containsOnce(calendar, '.fc-agendaWeek-view', "should be in week mode"); + assert.containsOnce(calendar, '.fc-timeGridWeek-view', "should be in week mode"); assert.containsN(calendar, '.fc-event', 4, "should display 4 events on the week (1 event + 3 days event)"); // Clicking on a day in the same week should switch to that particular day view await testUtils.dom.click(calendar.$('.o_calendar_mini a:contains(18)')); - assert.containsOnce(calendar, '.fc-agendaDay-view', "should be in day mode"); + assert.containsOnce(calendar, '.fc-timeGridDay-view', "should be in day mode"); assert.containsN(calendar, '.fc-event', 2, "should display 2 events on the day"); // Clicking on the same day should toggle between day, month and week views await testUtils.dom.click(calendar.$('.o_calendar_mini a:contains(18)')); - assert.containsOnce(calendar, '.fc-month-view', "should be in month mode"); + assert.containsOnce(calendar, '.fc-dayGridMonth-view', "should be in month mode"); assert.containsN(calendar, '.fc-event', 7, "should display 7 events on the month (event 5 is on multiple weeks and generates to .fc-event)"); await testUtils.dom.click(calendar.$('.o_calendar_mini a:contains(18)')); - assert.containsOnce(calendar, '.fc-agendaWeek-view', "should be in week mode"); + assert.containsOnce(calendar, '.fc-timeGridWeek-view', "should be in week mode"); assert.containsN(calendar, '.fc-event', 4, "should display 4 events on the week (1 event + 3 days event)"); await testUtils.dom.click(calendar.$('.o_calendar_mini a:contains(18)')); - assert.containsOnce(calendar, '.fc-agendaDay-view', "should be in day mode"); + assert.containsOnce(calendar, '.fc-timeGridDay-view', "should be in day mode"); assert.containsN(calendar, '.fc-event', 2, "should display 2 events on the day"); calendar.destroy(); @@ -2459,7 +2459,7 @@ QUnit.module('Views', { }); assert.containsOnce(calendar, '.fc-day-grid .fc-event-container', "should be one event in the all day row"); - assert.strictEqual(calendar.model.data.data[0].r_start.date(), 14, + assert.strictEqual(moment(calendar.model.data.data[0].r_start).date(), 14, "the date should be 14"); calendar.destroy(); }); diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml index 20bf8b43205..e28cc6d28dd 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -607,8 +607,17 @@ <script type="text/javascript" src="/web/static/tests/helpers/qunit_asserts.js"></script> <!-- add lazy-loaded libs to make tests synchronous --> - <link rel="stylesheet" href="/web/static/lib/fullcalendar/css/fullcalendar.css"/> - <script type="text/javascript" src="/web/static/lib/fullcalendar/js/fullcalendar.js"></script> + <link rel="stylesheet" href="/web/static/lib/fullcalendar/core/main.css"/> + <link rel="stylesheet" href="/web/static/lib/fullcalendar/daygrid/main.css"/> + <link rel="stylesheet" href="/web/static/lib/fullcalendar/timegrid/main.css"/> + <link rel="stylesheet" href="/web/static/lib/fullcalendar/list/main.css"/> + <script type="text/javascript" src="/web/static/lib/fullcalendar/core/main.js"></script> + <script type="text/javascript" src="/web/static/lib/fullcalendar/moment/main.js"></script> + <script type="text/javascript" src="/web/static/lib/fullcalendar/interaction/main.js"></script> + <script type="text/javascript" src="/web/static/lib/fullcalendar/daygrid/main.js"></script> + <script type="text/javascript" src="/web/static/lib/fullcalendar/timegrid/main.js"></script> + <script type="text/javascript" src="/web/static/lib/fullcalendar/list/main.js"></script> + <script type="text/javascript" src="/web/static/lib/ace/ace.js"></script> <script type="text/javascript" src="/web/static/lib/ace/javascript_highlight_rules.js"></script> <script type="text/javascript" src="/web/static/lib/ace/mode-python.js"></script> |
