UNPKG

vue-cal

Version:

A Vue JS full calendar, no dependency, no BS. :metal:

1,180 lines (1,179 loc) 154 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); /** * vue-cal v4.10.2 * (c) 2025 Antoni Andre <antoniandre.web@gmail.com> * @license MIT */ import { openBlock, createElementBlock, Fragment, renderList, normalizeClass, normalizeStyle, createVNode, Transition, withCtx, createElementVNode, renderSlot, toDisplayString, createCommentVNode, createTextVNode, resolveComponent, createBlock, resolveDynamicComponent, createSlots, withKeys, withModifiers, TransitionGroup, normalizeProps, mergeProps } from "vue"; const __variableDynamicImportRuntimeHelper = (glob, path, segs) => { const v = glob[path]; if (v) { return typeof v === "function" ? v() : Promise.resolve(v); } return new Promise((_, reject) => { (typeof queueMicrotask === "function" ? queueMicrotask : setTimeout)( reject.bind( null, new Error( "Unknown variable dynamic import: " + path + (path.split("/").length !== segs ? ". Note that variables only represent file names one level deep." : "") ) ) ); }); }; let now, todayDate, todayF, self; let _dateObject = {}; let _timeObject = {}; class DateUtils { constructor(texts, noPrototypes = false) { __publicField(this, "texts", {}); /** * Simply takes a Date and returns the associated time in minutes (sum of hours + minutes). * * @param {Date} date the JavaScript Date to extract minutes from. * @return {Number} the number of minutes (total of hours plus minutes). */ __publicField(this, "dateToMinutes", (date) => date.getHours() * 60 + date.getMinutes()); self = this; this._texts = texts; if (!noPrototypes && Date && !Date.prototype.addDays) this._initDatePrototypes(); } _initDatePrototypes() { Date.prototype.addDays = function(days) { return self.addDays(this, days); }; Date.prototype.subtractDays = function(days) { return self.subtractDays(this, days); }; Date.prototype.addHours = function(hours) { return self.addHours(this, hours); }; Date.prototype.subtractHours = function(hours) { return self.subtractHours(this, hours); }; Date.prototype.addMinutes = function(minutes) { return self.addMinutes(this, minutes); }; Date.prototype.subtractMinutes = function(minutes) { return self.subtractMinutes(this, minutes); }; Date.prototype.getWeek = function() { return self.getWeek(this); }; Date.prototype.isToday = function() { return self.isToday(this); }; Date.prototype.isLeapYear = function() { return self.isLeapYear(this); }; Date.prototype.format = function(format = "YYYY-MM-DD") { return self.formatDate(this, format); }; Date.prototype.formatTime = function(format = "HH:mm") { return self.formatTime(this, format); }; } removePrototypes() { delete Date.prototype.addDays; delete Date.prototype.subtractDays; delete Date.prototype.addHours; delete Date.prototype.subtractHours; delete Date.prototype.addMinutes; delete Date.prototype.subtractMinutes; delete Date.prototype.getWeek; delete Date.prototype.isToday; delete Date.prototype.isLeapYear; delete Date.prototype.format; delete Date.prototype.formatTime; } updateTexts(texts) { this._texts = texts; } // Cache Today's date (to a maximum) for better isToday() performances. Formatted without leading 0. // We still need to update Today's date when Today changes without page refresh. _todayFormatted() { if (todayDate !== (/* @__PURE__ */ new Date()).getDate()) { now = /* @__PURE__ */ new Date(); todayDate = now.getDate(); todayF = `${now.getFullYear()}-${now.getMonth()}-${now.getDate()}`; } return todayF; } // UTILITIES. // ==================================================================== addDays(date, days) { const d = new Date(date.valueOf()); d.setDate(d.getDate() + days); return d; } subtractDays(date, days) { const d = new Date(date.valueOf()); d.setDate(d.getDate() - days); return d; } addHours(date, hours) { const d = new Date(date.valueOf()); d.setHours(d.getHours() + hours); return d; } subtractHours(date, hours) { const d = new Date(date.valueOf()); d.setHours(d.getHours() - hours); return d; } addMinutes(date, minutes) { const d = new Date(date.valueOf()); d.setMinutes(d.getMinutes() + minutes); return d; } subtractMinutes(date, minutes) { const d = new Date(date.valueOf()); d.setMinutes(d.getMinutes() - minutes); return d; } getWeek(date) { const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); const dayNum = d.getUTCDay() || 7; d.setUTCDate(d.getUTCDate() + 4 - dayNum); const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); return Math.ceil(((d - yearStart) / 864e5 + 1) / 7); } isToday(date) { return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}` === this._todayFormatted(); } isLeapYear(date) { const year = date.getFullYear(); return !(year % 400) || year % 100 && !(year % 4); } // Returns today if it's FirstDayOfWeek (Monday or Sunday) or previous FirstDayOfWeek otherwise. getPreviousFirstDayOfWeek(date = null, weekStartsOnSunday) { const prevFirstDayOfWeek = date && new Date(date.valueOf()) || /* @__PURE__ */ new Date(); const dayModifier = weekStartsOnSunday ? 7 : 6; prevFirstDayOfWeek.setDate(prevFirstDayOfWeek.getDate() - (prevFirstDayOfWeek.getDay() + dayModifier) % 7); return prevFirstDayOfWeek; } /** * Converts a string to a Javascript Date object. If a Date object is passed, return it as is. * * @param {String | Date} date the string to convert to Date. * @return {Date} the equivalent Javascript Date object. */ stringToDate(date) { if (date instanceof Date) return date; if (date.length === 10) date += " 00:00"; return new Date(date.replace(/-/g, "/")); } /** * Count the number of days this date range spans onto. * E.g. countDays(2019-11-02 18:00, 2019-11-03 02:00) = 2 * * @param {String | Date} start the start date * @param {String | Date} end the end date * @return {Integer} The number of days this date range involves */ countDays(start, end) { if (typeof start === "string") start = start.replace(/-/g, "/"); if (typeof end === "string") end = end.replace(/-/g, "/"); start = new Date(start).setHours(0, 0, 0, 0); end = new Date(end).setHours(0, 0, 1, 0); const timezoneDiffMs = (new Date(end).getTimezoneOffset() - new Date(start).getTimezoneOffset()) * 60 * 1e3; return Math.ceil((end - start - timezoneDiffMs) / (24 * 3600 * 1e3)); } /** * Take 2 dates and check if within the same time step (useful in overlapping events). * * @return {Boolean} `true` if their time is included in the same time step, * this means these 2 dates are very close. */ datesInSameTimeStep(date1, date2, timeStep) { return Math.abs(date1.getTime() - date2.getTime()) <= timeStep * 60 * 1e3; } // ==================================================================== // FORMATTERS. // ==================================================================== /** * Formats a date/time to the given format and returns the formatted string. * * @param {Date} date a JavaScript Date object to format. * @param {String} format the wanted format. * @param {Object} texts Optional: the localized texts object to override the vue-cal one in this._texts. * This becomes useful when showing multiple instances with different languages, * like in the documentation page. * @return {String} the formatted date. */ formatDate(date, format = "YYYY-MM-DD", texts = null) { if (!texts) texts = this._texts; if (!format) format = "YYYY-MM-DD"; if (format === "YYYY-MM-DD") return this.formatDateLite(date); _dateObject = {}; _timeObject = {}; const dateObj = { YYYY: () => this._hydrateDateObject(date, texts).YYYY, YY: () => this._hydrateDateObject(date, texts).YY(), M: () => this._hydrateDateObject(date, texts).M, MM: () => this._hydrateDateObject(date, texts).MM(), MMM: () => this._hydrateDateObject(date, texts).MMM(), MMMM: () => this._hydrateDateObject(date, texts).MMMM(), MMMMG: () => this._hydrateDateObject(date, texts).MMMMG(), D: () => this._hydrateDateObject(date, texts).D, DD: () => this._hydrateDateObject(date, texts).DD(), S: () => this._hydrateDateObject(date, texts).S(), d: () => this._hydrateDateObject(date, texts).d, dd: () => this._hydrateDateObject(date, texts).dd(), ddd: () => this._hydrateDateObject(date, texts).ddd(), dddd: () => this._hydrateDateObject(date, texts).dddd(), HH: () => this._hydrateTimeObject(date, texts).HH, H: () => this._hydrateTimeObject(date, texts).H, hh: () => this._hydrateTimeObject(date, texts).hh, h: () => this._hydrateTimeObject(date, texts).h, am: () => this._hydrateTimeObject(date, texts).am, AM: () => this._hydrateTimeObject(date, texts).AM, mm: () => this._hydrateTimeObject(date, texts).mm, m: () => this._hydrateTimeObject(date, texts).m }; return format.replace(/(\{[a-zA-Z]+\}|[a-zA-Z]+)/g, (m, contents) => { const result = dateObj[contents.replace(/\{|\}/g, "")]; return result !== void 0 ? result() : contents; }); } // More performant function to convert a Date to `YYYY-MM-DD` formatted string only. formatDateLite(date) { const m = date.getMonth() + 1; const d = date.getDate(); return `${date.getFullYear()}-${m < 10 ? "0" : ""}${m}-${d < 10 ? "0" : ""}${d}`; } /** * Formats a time (from Date or number of mins) to the given format and returns the formatted string. * * @param {Date | Number} date a JavaScript Date object or a time in minutes. * @param {String} format the wanted format. * @param {Object} texts Optional: the localized texts object to override the vue-cal one in this._texts. * This becomes useful when showing multiple instances with different languages, * like in the documentation page. * @param {Boolean} round if time is 23:59:59, rounds up to 24:00 for formatting only. * @return {String} the formatted time. */ formatTime(date, format = "HH:mm", texts = null, round = false) { let shouldRound = false; if (round) { const [h, m, s] = [date.getHours(), date.getMinutes(), date.getSeconds()]; if (h + m + s === 23 + 59 + 59) shouldRound = true; } if (date instanceof Date && format === "HH:mm") return shouldRound ? "24:00" : this.formatTimeLite(date); _timeObject = {}; if (!texts) texts = this._texts; const timeObj = this._hydrateTimeObject(date, texts); const formatted = format.replace(/(\{[a-zA-Z]+\}|[a-zA-Z]+)/g, (m, contents) => { const result = timeObj[contents.replace(/\{|\}/g, "")]; return result !== void 0 ? result : contents; }); return shouldRound ? formatted.replace("23:59", "24:00") : formatted; } /** * Formats a time to 'HH:mm' from a Date and returns the formatted string. * * @param {Date} date a JavaScript Date object to format. * @return {String} the formatted time. */ formatTimeLite(date) { const h = date.getHours(); const m = date.getMinutes(); return `${(h < 10 ? "0" : "") + h}:${(m < 10 ? "0" : "") + m}`; } _nth(d) { if (d > 3 && d < 21) return "th"; switch (d % 10) { case 1: return "st"; case 2: return "nd"; case 3: return "rd"; default: return "th"; } } _hydrateDateObject(date, texts) { if (_dateObject.D) return _dateObject; const YYYY = date.getFullYear(); const M = date.getMonth() + 1; const D = date.getDate(); const day = date.getDay(); const dayNumber = (day - 1 + 7) % 7; _dateObject = { // Year. YYYY, // 2019. YY: () => YYYY.toString().substring(2), // 19. // Month. M, // 1 to 12. MM: () => (M < 10 ? "0" : "") + M, // 01 to 12. MMM: () => texts.months[M - 1].substring(0, 3), // Jan to Dec. MMMM: () => texts.months[M - 1], // January to December. MMMMG: () => (texts.monthsGenitive || texts.months)[M - 1], // January to December in genitive form (Greek...) // Day. D, // 1 to 31. DD: () => (D < 10 ? "0" : "") + D, // 01 to 31. S: () => this._nth(D), // st, nd, rd, th. // Day of the week. d: dayNumber + 1, // 1 to 7 with 7 = Sunday. dd: () => texts.weekDays[dayNumber][0], // M to S. ddd: () => texts.weekDays[dayNumber].substr(0, 3), // Mon to Sun. dddd: () => texts.weekDays[dayNumber] // Monday to Sunday. }; return _dateObject; } _hydrateTimeObject(date, texts) { if (_timeObject.am) return _timeObject; let H, m; if (date instanceof Date) { H = date.getHours(); m = date.getMinutes(); } else { H = Math.floor(date / 60); m = Math.floor(date % 60); } const h = H % 12 ? H % 12 : 12; const am = (texts || { am: "am", pm: "pm" })[H === 24 || H < 12 ? "am" : "pm"]; _timeObject = { H, h, HH: (H < 10 ? "0" : "") + H, hh: (h < 10 ? "0" : "") + h, am, AM: am.toUpperCase(), m, mm: (m < 10 ? "0" : "") + m }; return _timeObject; } // ==================================================================== } const minutesInADay$2 = 24 * 60; class CellUtils { constructor(vuecal) { __publicField(this, "_vuecal", null); /** * Select a cell and go to narrower view on double click or single click according to vuecalProps option. * * @param {Boolean} force Force switching to narrower view. * @param {Date} date The selected cell date at the exact time where it was clicked (through cursor coords). * @param {Integer} split The selected cell split if any. */ __publicField(this, "selectCell", (force = false, date, split) => { this._vuecal.$emit("cell-click", split ? { date, split } : date); if (this._vuecal.clickToNavigate || force) this._vuecal.switchToNarrowerView(); else if (this._vuecal.dblclickToNavigate && "ontouchstart" in window) { this._vuecal.domEvents.dblTapACell.taps++; setTimeout(() => this._vuecal.domEvents.dblTapACell.taps = 0, this._vuecal.domEvents.dblTapACell.timeout); if (this._vuecal.domEvents.dblTapACell.taps >= 2) { this._vuecal.domEvents.dblTapACell.taps = 0; this._vuecal.switchToNarrowerView(); this._vuecal.$emit("cell-dblclick", split ? { date, split } : date); } } }); /** * Select a cell and go to narrower view on enter. * * @param {Boolean} force Force switching to narrower view. * @param {Date} date The selected cell date at the exact time where it was clicked (through cursor coords). * @param {Integer} split The selected cell split if any. */ __publicField(this, "keyPressEnterCell", (date, split) => { this._vuecal.$emit("cell-keypress-enter", split ? { date, split } : date); this._vuecal.switchToNarrowerView(); }); /** * Get the coordinates of the mouse cursor from the cells wrapper referential (`ref="cells"`). * * @todo Cache bounding box & update it on resize. * @param {Object} e the native DOM event object. * @return {Object} containing { x: {Number}, y: {Number} } */ __publicField(this, "getPosition", (e) => { const { left, top } = this._vuecal.cellsEl.getBoundingClientRect(); const { clientX, clientY } = "ontouchstart" in window && e.touches ? e.touches[0] : e; return { x: clientX - left, y: clientY - top }; }); /** * Get the number of minutes from the top to the mouse cursor. * Returns a constrained time between 0 and 24 * 60. * * @param {Object} e the native DOM event object. * @return {Object} containing { minutes: {Number}, cursorCoords: { x: {Number}, y: {Number} } } */ __publicField(this, "minutesAtCursor", (e) => { let minutes = 0; let cursorCoords = { x: 0, y: 0 }; const { timeStep, timeCellHeight, timeFrom } = this._vuecal.$props; if (typeof e === "number") minutes = e; else if (typeof e === "object") { cursorCoords = this.getPosition(e); minutes = Math.round(cursorCoords.y * timeStep / parseInt(timeCellHeight) + timeFrom); } return { minutes: Math.max(Math.min(minutes, minutesInADay$2), 0), cursorCoords }; }); this._vuecal = vuecal; } } const defaultEventDuration = 2; const minutesInADay$1 = 24 * 60; let ud; let _cellOverlaps, _comparisonArray; class EventUtils { constructor(vuecal, dateUtils2) { __publicField(this, "_vuecal", null); __publicField(this, "eventDefaults", { _eid: null, start: "", // Externally given formatted date & time or Date object. startTimeMinutes: 0, end: "", // Externally given formatted date & time or Date object. endTimeMinutes: 0, title: "", content: "", background: false, allDay: false, segments: null, repeat: null, daysCount: 1, deletable: true, deleting: false, titleEditable: true, resizable: true, resizing: false, draggable: true, dragging: false, draggingStatic: false, // Controls the CSS class of the static clone while dragging. focused: false, class: "" }); this._vuecal = vuecal; ud = dateUtils2; } /** * Create an event at the given date and time, and allow overriding * event attributes through the eventOptions object. * * @param {Date | String} dateTime The date and time of the new event start. * @param {Number} duration the event duration in minutes. * @param {Object} eventOptions some options to override the `eventDefaults` - optional. */ createAnEvent(dateTime, duration, eventOptions) { if (typeof dateTime === "string") dateTime = ud.stringToDate(dateTime); if (!(dateTime instanceof Date)) return false; const startTimeMinutes = ud.dateToMinutes(dateTime); duration = duration * 1 || defaultEventDuration * 60; const endTimeMinutes = startTimeMinutes + duration; const end = ud.addMinutes(new Date(dateTime), duration); if (eventOptions.end) { if (typeof eventOptions.end === "string") eventOptions.end = ud.stringToDate(eventOptions.end); eventOptions.endTimeMinutes = ud.dateToMinutes(eventOptions.end); } const event = { ...this.eventDefaults, _eid: `${this._vuecal._.uid}_${this._vuecal.eventIdIncrement++}`, start: dateTime, startTimeMinutes, end, endTimeMinutes, segments: null, ...eventOptions }; if (typeof this._vuecal.onEventCreate === "function") { if (!this._vuecal.onEventCreate(event, () => this.deleteAnEvent(event))) return; } if (event.startDateF !== event.endDateF) { event.daysCount = ud.countDays(event.start, event.end); } this._vuecal.mutableEvents.push(event); this._vuecal.addEventsToView([event]); this._vuecal.emitWithEvent("event-create", event); this._vuecal.$emit("event-change", { event: this._vuecal.cleanupEvent(event), originalEvent: null }); return event; } /** * Add an event segment (= day) to a multiple-day event. * * @param {Object} e the multiple-day event to add segment in. */ addEventSegment(e) { if (!e.segments) { e.segments = {}; e.segments[ud.formatDateLite(e.start)] = { start: e.start, startTimeMinutes: e.startTimeMinutes, endTimeMinutes: minutesInADay$1, isFirstDay: true, isLastDay: false }; } const previousSegment = e.segments[ud.formatDateLite(e.end)]; if (previousSegment) { previousSegment.isLastDay = false; previousSegment.endTimeMinutes = minutesInADay$1; } const start = ud.addDays(e.end, 1); const formattedDate = ud.formatDateLite(start); start.setHours(0, 0, 0, 0); e.segments[formattedDate] = { start, startTimeMinutes: 0, endTimeMinutes: e.endTimeMinutes, isFirstDay: false, isLastDay: true }; e.end = ud.addMinutes(start, e.endTimeMinutes); e.daysCount = Object.keys(e.segments).length; return formattedDate; } /** * Remove an event segment (= day) from a multiple-day event. * * @param {Object} e the multiple-day event to remove segments from. */ removeEventSegment(e) { let segmentsCount = Object.keys(e.segments).length; if (segmentsCount <= 1) return ud.formatDateLite(e.end); delete e.segments[ud.formatDateLite(e.end)]; segmentsCount--; const end = ud.subtractDays(e.end, 1); const formattedDate = ud.formatDateLite(end); const previousSegment = e.segments[formattedDate]; if (!segmentsCount) e.segments = null; else if (previousSegment) { previousSegment.isLastDay = true; previousSegment.endTimeMinutes = e.endTimeMinutes; } else ; e.daysCount = segmentsCount || 1; e.end = end; return formattedDate; } /** * Create 1 segment per day of the given event, but only within the current view. * (It won't create segments for all the days in view that are not in the event!) * * An event segment is a piece of event per day that contains more day-specific data. * * @param {Object} e the multiple-day event to create segments for. * @param {Date} viewStartDate the starting date of the view. * @param {Date} viewEndDate the ending date of the view. */ createEventSegments(e, viewStartDate, viewEndDate) { const viewStartTimestamp = viewStartDate.getTime(); const viewEndTimestamp = viewEndDate.getTime(); let eventStart = e.start.getTime(); let eventEnd = e.end.getTime(); let repeatedEventStartFound = false; let timestamp, end, eventStartAtMidnight; if (!e.end.getHours() && !e.end.getMinutes()) eventEnd -= 1e3; e.segments = {}; if (!e.repeat) { timestamp = Math.max(viewStartTimestamp, eventStart); end = Math.min(viewEndTimestamp, eventEnd); } else { timestamp = viewStartTimestamp; end = Math.min( viewEndTimestamp, e.repeat.until ? ud.stringToDate(e.repeat.until).getTime() : viewEndTimestamp ); } while (timestamp <= end) { let createSegment = false; const nextMidnight = ud.addDays(new Date(timestamp), 1).setHours(0, 0, 0, 0); let isFirstDay, isLastDay, start, formattedDate; if (e.repeat) { const tmpDate = new Date(timestamp); const tmpDateFormatted = ud.formatDateLite(tmpDate); if (repeatedEventStartFound || e.occurrences && e.occurrences[tmpDateFormatted]) { if (!repeatedEventStartFound) { eventStart = e.occurrences[tmpDateFormatted].start; eventStartAtMidnight = new Date(eventStart).setHours(0, 0, 0, 0); eventEnd = e.occurrences[tmpDateFormatted].end; } repeatedEventStartFound = true; createSegment = true; } isFirstDay = timestamp === eventStartAtMidnight; isLastDay = tmpDateFormatted === ud.formatDateLite(new Date(eventEnd)); start = new Date(isFirstDay ? eventStart : timestamp); formattedDate = ud.formatDateLite(start); if (isLastDay) repeatedEventStartFound = false; } else { createSegment = true; isFirstDay = timestamp === eventStart; isLastDay = end === eventEnd && nextMidnight > end; start = isFirstDay ? e.start : new Date(timestamp); formattedDate = ud.formatDateLite(isFirstDay ? e.start : start); } if (createSegment) { e.segments[formattedDate] = { start, startTimeMinutes: isFirstDay ? e.startTimeMinutes : 0, endTimeMinutes: isLastDay ? e.endTimeMinutes : minutesInADay$1, isFirstDay, isLastDay }; } timestamp = nextMidnight; } return e; } /** * Delete an event. * * @param {Object} event the calendar event to delete. */ deleteAnEvent(event) { this._vuecal.emitWithEvent("event-delete", event); this._vuecal.mutableEvents = this._vuecal.mutableEvents.filter((e) => e._eid !== event._eid); this._vuecal.view.events = this._vuecal.view.events.filter((e) => e._eid !== event._eid); } // EVENT OVERLAPS. // =================================================================== // Will recalculate all the overlaps of the current cell OR split. // cellEvents will contain only the current split events if in a split. checkCellOverlappingEvents(cellEvents, options) { _comparisonArray = cellEvents.slice(0); _cellOverlaps = {}; cellEvents.forEach((e) => { _comparisonArray.shift(); if (!_cellOverlaps[e._eid]) _cellOverlaps[e._eid] = { overlaps: [], start: e.start, position: 0 }; _cellOverlaps[e._eid].position = 0; _comparisonArray.forEach((e2) => { if (!_cellOverlaps[e2._eid]) _cellOverlaps[e2._eid] = { overlaps: [], start: e2.start, position: 0 }; const eventIsInRange = this.eventInRange(e2, e.start, e.end); const eventsInSameTimeStep = options.overlapsPerTimeStep ? ud.datesInSameTimeStep(e.start, e2.start, options.timeStep) : 1; if (!e.background && !e.allDay && !e2.background && !e2.allDay && eventIsInRange && eventsInSameTimeStep) { _cellOverlaps[e._eid].overlaps.push(e2._eid); _cellOverlaps[e._eid].overlaps = [...new Set(_cellOverlaps[e._eid].overlaps)]; _cellOverlaps[e2._eid].overlaps.push(e._eid); _cellOverlaps[e2._eid].overlaps = [...new Set(_cellOverlaps[e2._eid].overlaps)]; _cellOverlaps[e2._eid].position++; } else { let pos1, pos2; if ((pos1 = (_cellOverlaps[e._eid] || { overlaps: [] }).overlaps.indexOf(e2._eid)) > -1) _cellOverlaps[e._eid].overlaps.splice(pos1, 1); if ((pos2 = (_cellOverlaps[e2._eid] || { overlaps: [] }).overlaps.indexOf(e._eid)) > -1) _cellOverlaps[e2._eid].overlaps.splice(pos2, 1); _cellOverlaps[e2._eid].position--; } }); }); let longestStreak = 0; for (const id in _cellOverlaps) { const item = _cellOverlaps[id]; const overlapsRow = item.overlaps.map((id2) => ({ id: id2, start: _cellOverlaps[id2].start })); overlapsRow.push({ id, start: item.start }); overlapsRow.sort((a, b) => a.start < b.start ? -1 : a.start > b.start ? 1 : a.id > b.id ? -1 : 1); item.position = overlapsRow.findIndex((e) => e.id === id); longestStreak = Math.max(this.getOverlapsStreak(item, _cellOverlaps), longestStreak); } return [_cellOverlaps, longestStreak]; } /** * Overlaps streak is the longest horizontal set of simultaneous events. * This is determining the width of each events in this streak. * E.g. 3 overlapping events [1, 2, 3]; 1 overlaps 2 & 3; 2 & 3 don't overlap; * => streak = 2; each width = 50% not 33%. * * @param {Object} event The current event we are checking among all the events of the current cell. * @param {Object} cellOverlaps An indexed array of all the events overlaps for the current cell. * @return {Number} The number of simultaneous event for this event. */ getOverlapsStreak(event, cellOverlaps = {}) { let streak = event.overlaps.length + 1; let removeFromStreak = []; event.overlaps.forEach((id) => { if (!removeFromStreak.includes(id)) { const overlapsWithoutSelf = event.overlaps.filter((id2) => id2 !== id); overlapsWithoutSelf.forEach((id3) => { if (!cellOverlaps[id3].overlaps.includes(id)) removeFromStreak.push(id3); }); } }); removeFromStreak = [...new Set(removeFromStreak)]; streak -= removeFromStreak.length; return streak; } /** * Tells whether an event is in a given date range, even partially. * * @param {Object} event The event to test. * @param {Date} start The start of range date object. * @param {Date} end The end of range date object. * @return {Boolean} true if in range, even partially. */ eventInRange(event, start, end) { if (event.allDay || !this._vuecal.time) { const startTimestamp2 = new Date(event.start).setHours(0, 0, 0, 0); const endTimestamp2 = new Date(event.end).setHours(23, 59, 0, 0); return endTimestamp2 >= new Date(start).setHours(0, 0, 0, 0) && startTimestamp2 <= new Date(end).setHours(0, 0, 0, 0); } const startTimestamp = event.start.getTime(); const endTimestamp = event.end.getTime(); return startTimestamp < end.getTime() && endTimestamp > start.getTime(); } } const _hoisted_1$5 = { class: "vuecal__flex vuecal__weekdays-headings" }; const _hoisted_2$3 = ["onClick"]; const _hoisted_3$3 = { class: "vuecal__flex weekday-label", grow: "" }; const _hoisted_4$3 = { class: "full" }; const _hoisted_5$3 = { class: "small" }; const _hoisted_6$2 = { class: "xsmall" }; const _hoisted_7$2 = { key: 0 }; const _hoisted_8$2 = { key: 0, class: "vuecal__flex vuecal__split-days-headers", grow: "" }; function render$5(_ctx, _cache, $props, $setup, $data, $options) { return openBlock(), createElementBlock("div", _hoisted_1$5, [ (openBlock(true), createElementBlock(Fragment, null, renderList($options.headings, (heading, i) => { return openBlock(), createElementBlock(Fragment, { key: i }, [ !heading.hide ? (openBlock(), createElementBlock("div", { key: 0, class: normalizeClass(["vuecal__flex vuecal__heading", { today: heading.today, clickable: $options.cellHeadingsClickable }]), style: normalizeStyle($options.weekdayCellStyles), onClick: ($event) => $options.view.id === "week" && $options.selectCell(heading.date, $event), onDblclick: _cache[0] || (_cache[0] = ($event) => $options.view.id === "week" && $options.vuecal.dblclickToNavigate && $props.switchToNarrowerView()) }, [ createVNode(Transition, { name: `slide-fade--${$props.transitionDirection}`, appear: $options.vuecal.transitions }, { default: withCtx(() => [ (openBlock(), createElementBlock("div", { class: "vuecal__flex", column: "", key: $options.vuecal.transitions ? `${i}-${heading.dayOfMonth}` : false }, [ createElementVNode("div", _hoisted_3$3, [ renderSlot(_ctx.$slots, "weekday-heading", { heading: $options.cleanupHeading(heading), view: $options.view }, () => [ createElementVNode("span", _hoisted_4$3, toDisplayString(heading.full), 1), createElementVNode("span", _hoisted_5$3, toDisplayString(heading.small), 1), createElementVNode("span", _hoisted_6$2, toDisplayString(heading.xsmall), 1), heading.dayOfMonth ? (openBlock(), createElementBlock("span", _hoisted_7$2, " " + toDisplayString(heading.dayOfMonth), 1)) : createCommentVNode("", true) ]) ]), $options.vuecal.hasSplits && $options.vuecal.stickySplitLabels ? (openBlock(), createElementBlock("div", _hoisted_8$2, [ (openBlock(true), createElementBlock(Fragment, null, renderList($options.vuecal.daySplits, (split, i2) => { return openBlock(), createElementBlock("div", { class: normalizeClass(["day-split-header", split.class || false]), key: i2 }, [ renderSlot(_ctx.$slots, "split-label", { split, view: $options.view }, () => [ createTextVNode(toDisplayString(split.label), 1) ]) ], 2); }), 128)) ])) : createCommentVNode("", true) ])) ]), _: 2 }, 1032, ["name", "appear"]) ], 46, _hoisted_2$3)) : createCommentVNode("", true) ], 64); }), 128)) ]); } const _export_sfc = (sfc, props) => { const target = sfc.__vccOpts || sfc; for (const [key, val] of props) { target[key] = val; } return target; }; const _sfc_main$5 = { inject: ["vuecal", "utils", "view"], props: { transitionDirection: { type: String, default: "right" }, weekDays: { type: Array, default: () => [] }, switchToNarrowerView: { type: Function, default: () => { } } }, methods: { selectCell(date, DOMEvent) { if (date.getTime() !== this.view.selectedDate.getTime()) { this.view.selectedDate = date; } this.utils.cell.selectCell(false, date, DOMEvent); }, cleanupHeading: (heading) => ({ label: heading.full, date: heading.date, ...heading.today ? { today: heading.today } : {} }) }, computed: { headings() { if (!["month", "week"].includes(this.view.id)) return []; let todayFound = false; const headings = this.weekDays.map((cell, i) => { const date = this.utils.date.addDays(this.view.startDate, this.vuecal.startWeekOnSunday && this.vuecal.hideWeekends ? i - 1 : i); return { hide: cell.hide, full: cell.label, // If defined in i18n file, weekDaysShort overrides default truncation of // week days when does not fit on screen or with small/xsmall options. small: cell.short || cell.label.substr(0, 3), xsmall: cell.short || cell.label.substr(0, 1), // Only for week view. ...this.view.id === "week" ? { dayOfMonth: date.getDate(), date, today: !todayFound && this.utils.date.isToday(date) && !todayFound++ } : {} }; }); return headings; }, cellWidth() { return 100 / (7 - this.weekDays.reduce((total, day) => total + day.hide, 0)); }, weekdayCellStyles() { return { ...this.vuecal.hideWeekdays.length ? { width: `${this.cellWidth}%` } : {} }; }, cellHeadingsClickable() { return this.view.id === "week" && (this.vuecal.clickToNavigate || this.vuecal.dblclickToNavigate); } } }; const WeekdaysHeadings = /* @__PURE__ */ _export_sfc(_sfc_main$5, [["render", render$5]]); const _hoisted_1$4 = { class: "vuecal__header" }; const _hoisted_2$2 = { key: 0, class: "vuecal__flex vuecal__menu", role: "tablist", "aria-label": "Calendar views navigation" }; const _hoisted_3$2 = ["onDragenter", "onDragleave", "onClick", "aria-label"]; const _hoisted_4$2 = { key: 1, class: "vuecal__title-bar" }; const _hoisted_5$2 = ["aria-label"]; const _hoisted_6$1 = { class: "vuecal__flex vuecal__title", grow: "" }; const _hoisted_7$1 = ["aria-label"]; const _hoisted_8$1 = { key: 0, class: "vuecal__flex vuecal__split-days-headers" }; function render$4(_ctx, _cache, $props, $setup, $data, $options) { const _component_weekdays_headings = resolveComponent("weekdays-headings"); return openBlock(), createElementBlock("div", _hoisted_1$4, [ !$props.options.hideViewSelector ? (openBlock(), createElementBlock("div", _hoisted_2$2, [ (openBlock(true), createElementBlock(Fragment, null, renderList($props.viewProps.views, (v, id) => { return openBlock(), createElementBlock(Fragment, { key: id }, [ v.enabled ? (openBlock(), createElementBlock("button", { key: 0, class: normalizeClass(["vuecal__view-btn", { "vuecal__view-btn--active": $options.view.id === id, "vuecal__view-btn--highlighted": _ctx.highlightedControl === id }]), type: "button", onDragenter: ($event) => $props.editEvents.drag && $options.dnd && $options.dnd.viewSelectorDragEnter($event, id, _ctx.$data), onDragleave: ($event) => $props.editEvents.drag && $options.dnd && $options.dnd.viewSelectorDragLeave($event, id, _ctx.$data), onClick: ($event) => $options.switchView(id, null, true), "aria-label": `${v.label} view` }, toDisplayString(v.label), 43, _hoisted_3$2)) : createCommentVNode("", true) ], 64); }), 128)) ])) : createCommentVNode("", true), !$props.options.hideTitleBar ? (openBlock(), createElementBlock("div", _hoisted_4$2, [ createElementVNode("button", { class: normalizeClass(["vuecal__arrow vuecal__arrow--prev", { "vuecal__arrow--highlighted": _ctx.highlightedControl === "previous" }]), type: "button", onClick: _cache[0] || (_cache[0] = (...args) => $options.previous && $options.previous(...args)), onDragenter: _cache[1] || (_cache[1] = ($event) => $props.editEvents.drag && $options.dnd && $options.dnd.viewSelectorDragEnter($event, "previous", _ctx.$data)), onDragleave: _cache[2] || (_cache[2] = ($event) => $props.editEvents.drag && $options.dnd && $options.dnd.viewSelectorDragLeave($event, "previous", _ctx.$data)), "aria-label": `Previous ${$options.view.id}` }, [ renderSlot(_ctx.$slots, "arrow-prev") ], 42, _hoisted_5$2), createElementVNode("div", _hoisted_6$1, [ createVNode(Transition, { name: $props.options.transitions ? `slide-fade--${$options.transitionDirection}` : "" }, { default: withCtx(() => [ (openBlock(), createBlock(resolveDynamicComponent($options.broaderView ? "button" : "span"), { type: !!$options.broaderView && "button", key: `${$options.view.id}${$options.view.startDate.toString()}`, onClick: _cache[3] || (_cache[3] = ($event) => !!$options.broaderView && $options.switchToBroaderView()), "aria-label": !!$options.broaderView && `Go to ${$options.broaderView} view` }, { default: withCtx(() => [ renderSlot(_ctx.$slots, "title") ]), _: 3 }, 8, ["type", "aria-label"])) ]), _: 3 }, 8, ["name"]) ]), $props.options.todayButton ? (openBlock(), createElementBlock("button", { key: 0, class: normalizeClass(["vuecal__today-btn", { "vuecal__today-btn--highlighted": _ctx.highlightedControl === "today" }]), type: "button", onClick: _cache[4] || (_cache[4] = (...args) => $options.goToToday && $options.goToToday(...args)), onDragenter: _cache[5] || (_cache[5] = ($event) => $props.editEvents.drag && $options.dnd && $options.dnd.viewSelectorDragEnter($event, "today", _ctx.$data)), onDragleave: _cache[6] || (_cache[6] = ($event) => $props.editEvents.drag && $options.dnd && $options.dnd.viewSelectorDragLeave($event, "today", _ctx.$data)), "aria-label": "Today" }, [ renderSlot(_ctx.$slots, "today-button") ], 34)) : createCommentVNode("", true), createElementVNode("button", { class: normalizeClass(["vuecal__arrow vuecal__arrow--next", { "vuecal__arrow--highlighted": _ctx.highlightedControl === "next" }]), type: "button", onClick: _cache[7] || (_cache[7] = (...args) => $options.next && $options.next(...args)), onDragenter: _cache[8] || (_cache[8] = ($event) => $props.editEvents.drag && $options.dnd && $options.dnd.viewSelectorDragEnter($event, "next", _ctx.$data)), onDragleave: _cache[9] || (_cache[9] = ($event) => $props.editEvents.drag && $options.dnd && $options.dnd.viewSelectorDragLeave($event, "next", _ctx.$data)), "aria-label": `Next ${$options.view.id}` }, [ renderSlot(_ctx.$slots, "arrow-next") ], 42, _hoisted_7$1) ])) : createCommentVNode("", true), $props.viewProps.weekDaysInHeader ? (openBlock(), createBlock(_component_weekdays_headings, { key: 2, "week-days": $props.weekDays, "transition-direction": $options.transitionDirection, "switch-to-narrower-view": $props.switchToNarrowerView }, createSlots({ _: 2 }, [ _ctx.$slots["weekday-heading"] ? { name: "weekday-heading", fn: withCtx(({ heading, view }) => [ renderSlot(_ctx.$slots, "weekday-heading", { heading, view }) ]), key: "0" } : void 0, _ctx.$slots["split-label"] ? { name: "split-label", fn: withCtx(({ split }) => [ renderSlot(_ctx.$slots, "split-label", { split, view: $options.view }) ]), key: "1" } : void 0 ]), 1032, ["week-days", "transition-direction", "switch-to-narrower-view"])) : createCommentVNode("", true), createVNode(Transition, { name: `slide-fade--${$options.transitionDirection}` }, { default: withCtx(() => [ $options.showDaySplits ? (openBlock(), createElementBlock("div", _hoisted_8$1, [ (openBlock(true), createElementBlock(Fragment, null, renderList($props.daySplits, (split, i) => { return openBlock(), createElementBlock("div", { class: normalizeClass(["day-split-header", split.class || false]), key: i }, [ renderSlot(_ctx.$slots, "split-label", { split, view: $options.view.id }, () => [ createTextVNode(toDisplayString(split.label), 1) ]) ], 2); }), 128)) ])) : createCommentVNode("", true) ]), _: 3 }, 8, ["name"]) ]); } const _sfc_main$4 = { inject: ["vuecal", "previous", "next", "switchView", "updateSelectedDate", "modules", "view"], components: { WeekdaysHeadings }, props: { // Vuecal main component options (props). options: { type: Object, default: () => ({}) }, editEvents: { type: Object, required: true }, hasSplits: { type: [Boolean, Number], default: false }, daySplits: { type: Array, default: () => [] }, viewProps: { type: Object, default: () => ({}) }, weekDays: { type: Array, default: () => [] }, switchToNarrowerView: { type: Function, default: () => { } } }, data: () => ({ highlightedControl: null }), methods: { goToToday() { this.updateSelectedDate(new Date((/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0))); }, switchToBroaderView() { this.transitionDirection = "left"; if (this.broaderView) this.switchView(this.broaderView); } }, computed: { transitionDirection: { get() { return this.vuecal.transitionDirection; }, set(direction) { this.vuecal.transitionDirection = direction; } }, broaderView() { const { enabledViews } = this.vuecal; return enabledViews[enabledViews.indexOf(this.view.id) - 1]; }, showDaySplits() { return this.view.id === "day" && this.hasSplits && this.options.stickySplitLabels && !this.options.minSplitWidth; }, // Drag & drop module. dnd() { return this.modules.dnd; } } }; const Header = /* @__PURE__ */ _export_sfc(_sfc_main$4, [["render", render$4]]); const _hoisted_1$3 = ["draggable"]; function render$3(_ctx, _cache, $props, $setup, $data, $options) { return openBlock(), createElementBlock("div", { class: normalizeClass(["vuecal__event", $options.eventClasses]), style: normalizeStyle($options.eventStyles), tabindex: "0", onFocus: _cache[4] || (_cache[4] = (...args) => $options.focusEvent && $options.focusEvent(...args)), onKeypress: _cache[5] || (_cache[5] = withKeys(withModifiers((...args) => $options.onEnterKeypress && $options.onEnterKeypress(...args), ["stop"]), ["enter"])), onMouseenter: _cache[6] || (_cache[6] = (...args) => $options.onMouseEnter && $options.onMouseEnter(...args)), onMouseleave: _cache[7] || (_cache[7] = (...args) => $options.onMouseLeave && $options.onMouseLeave(...args)), onTouchstart: _cache[8] || (_cache[8] = withModifiers((...args) => $options.onTouchStart && $options.onTouchStart(...args), ["stop"])), onMousedown: _cache[9] || (_cache[9] = ($event) => $options.onMouseDown($event)), onMouseup: _cache[10] || (_cache[10] = (...args) => $options.onMouseUp && $options.onMouseUp(...args)), onTouchend: _cache[11] || (_cache[11] = (...args) => $options.onMouseUp && $options.onMouseUp(...args)), onTouchmove: _cache[12] || (_cache[12] = (...args) => $options.onTouchMove && $options.onTouchMove(...args)), onDblclick: _cache[13] || (_cache[13] = (...args) => $options.onDblClick && $options.onDblClick(...args)), draggable: $options.draggable, onDragstart: _cache[14] || (_cache[14] = ($event) => $options.draggable && $options.onDragStart($event)), onDragend: _cache[15] || (_cache[15] = ($event) => $options.draggable && $options.onDragEnd()) }, [ $options.vuecal.editEvents.delete && $props.event.deletable ? (openBlock(), createElementBlock("div", { key: 0, class: "vuecal__event-delete", onClick: _cache[0] || (_cache[0] = withModifiers((...args) => $options.deleteEvent && $options.deleteEvent(...args), ["stop"])), onTouchstart: _cache[1] || (_cache[1] = withModifiers((...args) => $options.touchDeleteEvent && $options.touchDeleteEvent(...args), ["stop"])) }, toDisplayString($options.vuecal.texts.deleteEvent), 33)) : createCommentVNode("", true), renderSlot(_ctx.$slots, "event", { event: $props.event, view: $options.view.id }), $options.resizable ? (openBlock(), createElementBlock("div", { key: 1, class: "vuecal__event-resize-handle", contenteditable: "false", onMousedown: _cache[2] || (_cache[2] = withModifiers((...args) => $options.onResizeHandleMouseDown && $options.onResizeHandleMouseDown(...args), ["stop", "prevent"])), onTouchstart: _cache[3] || (_cache[3] = withModifiers((...args) => $options.onResizeHandleMouseDown && $options.onResizeHandleMouseDown(...args), ["stop", "prevent"])) }, null, 32)) : createCommentVNode("", true) ], 46, _hoisted_1$3); } const _sfc_main$3 = { inject: ["vuecal", "utils", "modules", "view", "domEvents", "editEvents"], props: { cellFormattedDate: { type: String, default: "" }, event: { type: Object, default: () => ({}) }, cellEvents: { type: Array, default: () => [] }, overlaps: { type: Array, default: () => [] }, // If multiple simultaneous events, the events are placed from left to right from the // one starting first to the last. (See utils/event.js > checkCellOverlappingEvents) eventPosition: { type: Number, default: 0 }, overlapsStreak: { type: Number, default: 0 }, allDay: { type: Boolean, default: false } // Is the event displayed in the all-day bar. }, data: () => ({ // Event touch detection with 30px threshold. touch: { dragThreshold: 30, // px. startX: 0, startY: 0, // Detect if the event touch start + touch end was a drag or a tap. // If it was a drag, don't call the event-click handler. dragged: false } }), methods: { /** * On event mousedown. * Do not prevent propagation to trigger cell mousedown which highlights the cell if not highlighted. */ onMouseDown(e, touch = false) { if ("ontouchstart" in window && !touch) return false; const { clickHoldAnEvent, focusAnEvent, resizeAnEvent, dragAnEvent } = this.domEvents; if (focusAnEvent._eid === this.event._eid && clickHoldAnEvent._eid === this.event._eid) { return true; } this.focusEvent(); clickHoldAnEvent._eid = null; if (this.vuecal.editEvents.delete && this.event.deletable) { clickHoldAnEvent.timeoutId = setTimeout(() => { if (!resizeAnEvent._eid && !dragAnEvent._eid) { clickHoldAnEvent._eid = this.event._eid; this.event.deleting = true; } }, clickHoldAnEvent.timeout); } }, /** * The mouseup handler is global (whole document) and initialized in index.vue on mounted. * It handles the mouseup on cell, events, and everything. * All mouseup on event, should be put there to avoid conflicts with other cases. * This function is also called on touchend on the event. */ onMouseUp(e) { if (this.domEvents.focusAnEvent._eid === this.event._eid && !this.touch.dragged) { this.domEvents.focusAnEvent.mousedUp = true; } this.touch.dragged = false; }, onMouseEnter(e) { e.preventDefault(); this.vuecal.emitWithEvent("event-mouse-enter", this.event); }, onMouseLeave(e) { e.preventDefault(); this.vuecal.emitWithEvent("event-mouse-leave", this.event); }, // Detect if user taps on an event or drags it. If dragging, don't fire the event-click handler (if any). onTouchMove(e) { if (typeof this.vuecal.onEventClick !== "function") return; const { clientX, clientY } = e.touches[0]; const { startX, startY, dragThreshold } = this.touch; if (Math.abs(clientX - startX) > dragThreshold || Math.abs(clientY - startY) > dragThreshold) { this.touch.dragged = true; } }, onTouchStart(e) {