vue-cal
Version:
A Vue JS full calendar, no dependency, no BS. :metal:
1,180 lines (1,179 loc) • 154 kB
JavaScript
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) {