mirror of
https://bitbucket.org/studiotem/jandl-taskmanager-app.git
synced 2026-10-22 06:49:35 +00:00
vue-cal
This commit is contained in:
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()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user