This commit is contained in:
romankko2 committed 2022-11-28 12:40:23 +01:00
1 parent f6d0764969
commit 944ea92cef
56 files changed
+5535

No files matched your search

@@ -0,0 +1,94 @@
/**
* Cell Utils.
*
* Waiting for VS Code to support JavaScript private fields.
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Class_fields
* Meantime keep `_` for private.
*/
const minutesInADay = 24 * 60
export default class CellUtils {
_vuecal = null
constructor (vuecal) {
this._vuecal = vuecal
}
/**
* 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.
*/
selectCell = (force = false, date, split) => {
// Cell-click event returns a date and time at cursor position.
this._vuecal.$emit('cell-click', split ? { date, split } : date)
// Switch to narrower view.
if (this._vuecal.clickToNavigate || force) this._vuecal.switchToNarrowerView()
// Handle double click manually for touch devices.
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.
*/
keyPressEnterCell = (date, split) => {
// Cell-key-press-enter event returns a date and time at cursor position.
this._vuecal.$emit('cell-keypress-enter', split ? { date, split } : date)
// Switch to narrower view.
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} }
*/
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} } }
*/
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), 0), cursorCoords }
}
}
@@ -0,0 +1,376 @@
/**
* Date Utils & prototypes.
*
* Waiting for VS Code to support JavaScript private fields.
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Class_fields
* Meantime keep `_` for private.
*/
let now, todayDate, todayF, self
let _dateObject = {}
let _timeObject = {}
export default class DateUtils {
texts = {}
constructor (texts, noPrototypes = false) {
self = this // For use in Date prototypes.
this._texts = texts
// Add prototypes ASAP - only once.
if (!noPrototypes && Date && !Date.prototype.addDays) this._initDatePrototypes()
// @todo: This would be nicer, but how to set Date.noPrototypes ASAP only if user wants?
// if (Date.noPrototypes) delete Date.noPrototypes
// else this._initDatePrototypes()
}
_initDatePrototypes () {
/* eslint-disable no-extend-native */
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) }
/* eslint-enable no-extend-native */
}
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 !== (new Date()).getDate()) {
now = 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) / 86400000) + 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())) || 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
// Regexp way is less performant: https://jsperf.com/string-to-date-regexp-vs-new-date
// const [, y, m, d, h = 0, min = 0] = date.match(/(\d{4})-(\d{2})-(\d{2})(?: (\d{2}):(\d{2}))?/)
// return new Date(y, parseInt(m) - 1, d, h, min)
if (date.length === 10) date += ' 00:00'
return new Date(date.replace(/-/g, '/')) // replace '-' with '/' for Safari.
}
/**
* 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).
*/
dateToMinutes = date => date.getHours() * 60 + date.getMinutes()
/**
* 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) {
// replace '-' with '/' for Safari.
if (typeof start === 'string') start = start.replace(/-/g, '/')
if (typeof end === 'string') end = end.replace(/-/g, '/')
// Set start & end at midnight then compare the delta. Don't modify the original dates.
start = (new Date(start)).setHours(0, 0, 0, 0)
// Set end at midnight plus 1 sec, so Math.ceil will round it up to a full day.
end = (new Date(end)).setHours(0, 0, 1, 0)
// Remove the potential daylight saving delta.
const timezoneDiffMs = (new Date(end).getTimezoneOffset() - new Date(start).getTimezoneOffset()) * 60 * 1000
return Math.ceil((end - start - timezoneDiffMs) / (24 * 3600 * 1000))
}
/**
* 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 * 1000
}
// ====================================================================
// 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' // Allows passing null for default format.
if (format === 'YYYY-MM-DD') return this.formatDateLite(date)
// Reinit the date and time object on each function call.
_dateObject = {}
_timeObject = {}
// Each keyword is a function to load the dateObject or timeObject on demand: no wasted resource.
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 !== undefined ? 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 = {} // Reinit the time object on each function call.
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 !== undefined ? result : contents
})
// Round 23:59:59 to 24:00. For 12-hour format there is nothing to replace: as both are 12am.
// Also don't return `24:00` straight away as the user format may be different.
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() // Day of the week.
const dayNumber = (day - 1 + 7) % 7 // Day of the week. 0 to 6 with 6 = Sunday.
// Some of this props are functions, to only calculate on demand.
_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
}
// ====================================================================
}
@@ -0,0 +1,426 @@
/**
* Events Utils.
*
* Waiting for VS Code to support JavaScript private fields.
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Class_fields
* Meantime keep `_` for private.
*/
const defaultEventDuration = 2 // In hours.
const minutesInADay = 24 * 60 // Don't do the maths every time.
let ud
// Event overlaps: only for the current view, recreated on view change.
let _cellOverlaps, _comparisonArray
// This is an approximation, it will not work with DLS time.
// const approxDayMilliseconds = minutesInADay * 60 * 1000
// This is an approximate minimum we can get in a year. Purposely stay below 365 but close.
// const minYearMilliseconds = 364 * approxDayMilliseconds // Don't do the maths every time.
export default class EventUtils {
_vuecal = null
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: ''
}
constructor (vuecal, dateUtils) {
this._vuecal = vuecal
ud = dateUtils
}
/**
* 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)
// Automatically add the required endTimeMinutes when passing an end.
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 the onEventCreate() function is given as a parameter to vue-cal:
// 1. give it access to the created event & the deleteAnEvent() function.
// 2. Prevent creation of the event if this function returns false.
if (typeof this._vuecal.onEventCreate === 'function') {
if (!this._vuecal.onEventCreate(event, () => this.deleteAnEvent(event))) return
}
// Check if event is a multiple day event and update days count.
if (event.startDateF !== event.endDateF) {
event.daysCount = ud.countDays(event.start, event.end)
}
// Add event to the mutableEvents array.
this._vuecal.mutableEvents.push(event)
// Add the new event to the current view.
// The event may have been edited on the fly to become a multiple-day event,
// the method addEventsToView makes sure the segments are created.
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 event was previously single-day, event.segments = null,
// so first create the first segment (first day).
if (!e.segments) {
e.segments = {}
e.segments[ud.formatDateLite(e.start)] = {
start: e.start,
startTimeMinutes: e.startTimeMinutes,
endTimeMinutes: minutesInADay,
isFirstDay: true,
isLastDay: false
}
}
// Modify the last segment - which will not stay the last one after this function.
const previousSegment = e.segments[ud.formatDateLite(e.end)]
// previousSegment might not exist when dragging too fast, prevent errors.
if (previousSegment) {
previousSegment.isLastDay = false
previousSegment.endTimeMinutes = minutesInADay
}
else {
// @todo: when moving fast might lose the previousSegment.
// Trying to update it would then result in an error, but do nothing would create a visual bug.
}
// Create the new last segment.
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)
// Remove the last segment.
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 no more segments, reset the segments attribute to null.
if (!segmentsCount) e.segments = null
// previousSegment might not exist when dragging too fast, prevent errors.
else if (previousSegment) {
// Modify the new last segment.
previousSegment.isLastDay = true
previousSegment.endTimeMinutes = e.endTimeMinutes
}
else {
// @todo: when moving fast might lose the previousSegment.
// Trying to update it would then result in an error, but do nothing would create a visual bug.
}
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
// @todo: I don't think we still need that:
// Removing 1 sec when ending at 00:00, so that we don't create a segment for nothing on last day.
if (!e.end.getHours() && !e.end.getMinutes()) eventEnd -= 1000
e.segments = {}
// The goal is to create 1 segment per day in the event, but only within the current view.
if (!e.repeat) { // Simple case first.
timestamp = Math.max(viewStartTimestamp, eventStart)
end = Math.min(viewEndTimestamp, eventEnd)
}
else {
// Start at the beginning of the range, and end at soonest between `repeat.until` if any or range end.
// This range will most likely be too large (e.g. whole week) and we need to narrow it
// down in the while loop below.
// We must not create unused segments, it would break the render or result in weird behaviors.
timestamp = viewStartTimestamp
end = Math.min(
viewEndTimestamp,
e.repeat.until ? ud.stringToDate(e.repeat.until).getTime() : viewEndTimestamp
)
}
while (timestamp <= end) {
let createSegment = false
// Be careful not to simply add 24 hours!
// In case of DLS, that would cause the event to never end and browser to hang.
// So use `addDays(1)` instead.
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 the current day in loop is a known date of the repeated event (in `e.occurrences`),
// then we found the first day of the event repetition, now update the `eventStart` and
// the end of the loop at current day + event days count.
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)
// We want to find any potential other repetition of event in same range.
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,
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)
// Delete the event globally.
this._vuecal.mutableEvents = this._vuecal.mutableEvents.filter(e => e._eid !== event._eid)
// Delete the event from the current view.
// checkCellOverlappingEvents() will be re-run automatically from the cell computed events.
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 = {}
// Can't filter background events before calling this function otherwise
// when an event is changed to background it would not update its previous overlaps.
cellEvents.forEach(e => {
// For performance, never compare the current event in the next loops.
// The array is smaller and smaller as we loop.
_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
// Add to the overlaps array if overlapping.
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)] // Dedupe, most performant way.
_cellOverlaps[e2._eid].overlaps.push(e._eid)
_cellOverlaps[e2._eid].overlaps = [...new Set(_cellOverlaps[e2._eid].overlaps)] // Dedupe, most performant way.
_cellOverlaps[e2._eid].position++
}
// Remove from the overlaps array if not overlapping or if 1 of the 2 events is background or all-day long.
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--
}
})
})
// Overlaps streak is the longest horizontal set of simultaneous events.
// This is determining the width of events in a streak.
// e.g. 3 overlapping events in a cell:
// ___ ___
// | 1 | |_2_| 1 overlaps 2 & 3; 2 & 3 don't overlap;
// | | ___ => streak = 2; each width = 50% not 33%.
// |___| |_3_|
let longestStreak = 0
for (const id in _cellOverlaps) {
const item = _cellOverlaps[id]
// Calculate the position of each event in current streak (determines the CSS left property).
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)] // Dedupe, most performant way.
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) {
// Check if all-day or timeless event (if date but no time there won't be a `:` in event.start).
if (event.allDay || !this._vuecal.time) {
// Get the date and discard the time if any, then check it's within the date range.
const startTimestamp = new Date(event.start).setHours(0, 0, 0, 0)
const endTimestamp = new Date(event.end).setHours(23, 59, 0, 0)
return (endTimestamp >= new Date(start).setHours(0, 0, 0, 0) &&
startTimestamp <= 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()
}
}