UNPKG

@daykeep/calendar-core

Version:

A full display calendar for multiple Vue.js frameworks

579 lines (557 loc) 20.9 kB
import dashHas from 'lodash.has' import DateTime from 'luxon/src/datetime' import Interval from 'luxon/src/interval' const defaultParsed = { byAllDayStartDate: {}, byAllDayObject: {}, byStartDate: {}, byId: {} } const gridBlockSize = 5 // the number here is how many minutes for each block to use when calculating overlaps // const debug = require('debug')('calendar:CalendarEventMixin') export default { computed: {}, methods: { formatToSqlDate: function (dateObject) { return this.makeDT(dateObject).toISODate() }, getEventById: function (eventId) { return this.parsed.byId[eventId] }, dateGetEvents: function (thisDate, skipSlotIndicators) { let hasAllDayEvents = this.hasAllDayEvents(thisDate) let hasEvents = this.hasEvents(thisDate) let returnArray = [] let sqlDate = this.makeDT(thisDate).toISODate() if (hasAllDayEvents) { let transferFields = ['daysFromStart', 'durationDays', 'hasNext', 'hasPrev', 'slot'] // build temp object with slot IDs let slotObject = {} let maxSlot = 0 for (let thisEvent of this.parsed.byAllDayObject[sqlDate]) { slotObject[thisEvent.slot] = thisEvent if (thisEvent.slot > maxSlot) { maxSlot = thisEvent.slot } } // now we have it sorted but have to fill in any gaps for (let counter = 0; counter <= maxSlot; counter++) { let tempObject = {} if (dashHas(slotObject, counter)) { // this element exists tempObject = this.getEventById(slotObject[counter].id) for (let thisField of transferFields) { tempObject[thisField] = slotObject[counter][thisField] } } else { // this is an empty slot tempObject = { slot: counter, start: { isAllDay: true, isEmptySlot: true } } } if (skipSlotIndicators && tempObject.slot) { // bypass this - we don't want slot indicators } else { returnArray.push(tempObject) } } } if (hasEvents) { for (let thisEvent of this.parsed.byStartDate[sqlDate]) { returnArray.push(this.getEventById(thisEvent)) } } return returnArray }, hasAnyEvents: function (thisDateObject) { return ( this.hasEvents(thisDateObject) || this.hasAllDayEvents(thisDateObject) ) }, hasAllDayEvents: function (thisDateObject) { return dashHas( this.parsed.byAllDayObject, this.formatToSqlDate(thisDateObject) ) }, hasEvents: function (thisDateObject) { return dashHas( this.parsed.byStartDate, this.formatToSqlDate(thisDateObject) ) }, clearParsed: function () { this.parsed = {} this.parsed = { byAllDayStartDate: {}, byAllDayObject: {}, byStartDate: {}, byId: {}, byMultiDay: {}, byNextDay: {}, byContinuedMultiDay: {}, byContinuedNextDay: {} } return true }, moveToDisplayZone: function (dateObject) { return this.makeDT(dateObject, this.calendarTimezone) }, parseEventList: function () { this.clearParsed() for (let thisEvent of this.eventArray) { this.parsed.byId[thisEvent.id] = thisEvent if (dashHas(thisEvent.start, 'date')) { thisEvent.start['dateObject'] = this.moveToDisplayZone( DateTime.fromISO(thisEvent.start.date).startOf('day') ) thisEvent.end['dateObject'] = this.moveToDisplayZone( DateTime.fromISO(thisEvent.end.date).endOf('day') ) thisEvent.start['isAllDay'] = true thisEvent['durationDays'] = Math.ceil( thisEvent.end.dateObject .diff(thisEvent.start.dateObject) .as('days') ) } else { // start date thisEvent.start['dateObject'] = DateTime.fromISO(thisEvent.start.dateTime) if (dashHas(thisEvent.start, 'timeZone')) { // convert to local timezone thisEvent.start.dateObject = thisEvent.start.dateObject .setZone(thisEvent.start.timeZone, { keepLocalTime: true }) .toLocal() delete thisEvent.start.timeZone thisEvent.start.dateTime = thisEvent.start.dateObject.toISO() // fix time zone } thisEvent.start.dateObject = this.moveToDisplayZone( thisEvent.start.dateObject ) // end date thisEvent.end['dateObject'] = DateTime.fromISO(thisEvent.end.dateTime) if (dashHas(thisEvent.end, 'timeZone')) { // convert to local timezone thisEvent.end.dateObject = thisEvent.end.dateObject .setZone(thisEvent.end.timeZone, { keepLocalTime: true }) .toLocal() delete thisEvent.end.timeZone thisEvent.end.dateTime = thisEvent.end.dateObject.toISO() // fix time zone } thisEvent.end.dateObject = this.moveToDisplayZone( thisEvent.end.dateObject ) } // put in duration for multiday events with an associated time if ( !thisEvent.start['isAllDay'] && thisEvent.start.dateObject.toISODate() !== thisEvent.end.dateObject.toISODate() ) { thisEvent['durationDays'] = Math.ceil( thisEvent.end.dateObject .diff(thisEvent.start.dateObject) .as('days') ) if (thisEvent['durationDays'] > 2) { thisEvent['timeSpansMultipleDays'] = true } else { thisEvent['timeSpansOvernight'] = true } } let thisStartDate = thisEvent.start.dateObject.toISODate() // get all-day events if ( thisEvent.start.isAllDay || Math.floor(thisEvent.end.dateObject.diff(thisEvent.start.dateObject).as('days')) > 1 ) { for (let dayAdd = 0; dayAdd < thisEvent.durationDays; dayAdd++) { let innerStartDate = thisEvent.start.dateObject .plus({ days: dayAdd }) .toISODate() this.addToParsedList('byAllDayStartDate', innerStartDate, thisEvent.id) // newer all-day events routine this.addToParsedList( 'byAllDayObject', innerStartDate, { id: thisEvent.id, hasPrev: (dayAdd > 0), hasNext: (dayAdd < (thisEvent.durationDays - 1)), hasPreviousDay: (dayAdd > 0), hasNextDay: (dayAdd < (thisEvent.durationDays - 1)), durationDays: thisEvent.durationDays, startDate: thisEvent.start.dateObject, daysFromStart: dayAdd } ) } } // get events with a start and end time else { thisEvent.durationMinutes = this.parseGetDurationMinutes(thisEvent) this.addToParsedList('byStartDate', thisStartDate, thisEvent.id) if (thisEvent.start.dateObject.toISODate() !== thisEvent.end.dateObject.toISODate()) { // this is a date where the time is set and spans across more than one day const diffDays = Math.floor(thisEvent.end.dateObject.diff(thisEvent.start.dateObject).as('days')) if (diffDays > 1) { // this event spans multiple days this.addToParsedList('byMultiDay', thisStartDate, thisEvent.id) this.addToParsedList('byAllDayObject', thisStartDate, thisEvent.id) this.addToParsedList('byAllDayStartDate', thisStartDate, thisEvent.id) let multiDate = thisEvent.start.dateObject while (multiDate.toISODate() !== thisEvent.end.dateObject.toISODate()) { multiDate = multiDate.plus({ days: 1 }) this.addToParsedList('byContinuedMultiDay', multiDate.toISODate(), thisEvent.id) this.addToParsedList('byAllDayObject', thisStartDate, thisEvent.id) } } else { // this event crosses into the next day this.addToParsedList('byNextDay', thisStartDate, thisEvent.id) this.addToParsedList('byContinuedNextDay', thisEvent.end.dateObject.toISODate(), thisEvent.id) this.addToParsedList('byStartDate', thisEvent.end.dateObject.toISODate(), thisEvent.id) } } } } // sort all day events for (let thisDate in this.parsed.byAllDayObject) { this.parsed.byAllDayObject[thisDate].sort(this.sortPairOfAllDayObjects) } this.buildAllDaySlotArray() for (let thisDate in this.parsed.byStartDate) { this.parsed.byStartDate[thisDate] = this.sortDateEvents(this.parsed.byStartDate[thisDate]) this.parseDateEvents(this.parsed.byStartDate[thisDate]) } }, addToParsedList: function (listName, thisDate, whatToPush) { if (!dashHas(this.parsed[listName], thisDate)) { this.parsed[listName][thisDate] = [] } this.parsed[listName][thisDate].push(whatToPush) }, eventIsContinuedFromPreviousDay (id, thisDayObject) { const isoDate = this.makeDT(thisDayObject).toISODate() return ( dashHas(this.parsed['byContinuedNextDay'], isoDate) && this.parsed['byContinuedNextDay'][isoDate].includes(id) ) }, buildAllDaySlotArray: function () { let slotAssignments = {} let dateArray = Object.keys(this.parsed.byAllDayObject).sort() for (let thisDate of dateArray) { if (!dashHas(slotAssignments, thisDate)) { slotAssignments[thisDate] = {} } // go through each element on that date for (let thisAllDayObject of this.parsed.byAllDayObject[thisDate]) { if (!dashHas(thisAllDayObject, 'slot')) { let thisEventId = thisAllDayObject.id // find the first empty slot in the first day let slotToUse = 0 let slotFound = false while (!slotFound) { if (dashHas(slotAssignments[thisDate], slotToUse)) { slotToUse++ } else { slotFound = true } } // now fill that slot for each successive day for (let dayAdd = 0; dayAdd < thisAllDayObject.durationDays; dayAdd++) { let innerStartDate = DateTime.fromISO(thisDate + 'T00:00:00') .plus({ days: dayAdd }) .toISODate() if (!dashHas(slotAssignments, innerStartDate)) { slotAssignments[innerStartDate] = {} } slotAssignments[innerStartDate][slotToUse] = thisEventId // go through each element on that date for (let thisDateElementIndex in this.parsed.byAllDayObject[innerStartDate]) { let thisDateElement = this.parsed.byAllDayObject[innerStartDate][thisDateElementIndex] if (thisDateElement.id === thisEventId) { this.parsed.byAllDayObject[innerStartDate][thisDateElementIndex]['slot'] = slotToUse break } } } } } } }, sortPairOfAllDayObjects: function (eventA, eventB) { if (eventA.daysFromStart < eventB.daysFromStart) return 1 if (eventA.daysFromStart > eventB.daysFromStart) return -1 // okay, so daysFromStart are equal, now look at duration if (eventA.durationDays > eventB.durationDays) return 1 if (eventA.durationDays < eventB.durationDays) return -1 // daysFromStart are equal, so just take the first one return 0 }, sortPairOfDateEvents: function (eventA, eventB) { // return date.getDateDiff( // date.addToDate(eventA.start.dateObject, { milliseconds: eventA.durationMinutes }), // date.addToDate(eventB.start.dateObject, { milliseconds: eventB.durationMinutes }) // ) return eventB.start.dateObject .plus({ milliseconds: eventA.durationMinutes }) .diff( eventB.start.dateObject.plus({ milliseconds: eventA.durationMinutes }) ) .as('days') }, sortDateEvents: function (eventArray) { let tempArray = [] for (let eventId of eventArray) { tempArray.push(this.parsed.byId[eventId]) } tempArray.sort(this.sortPairOfDateEvents) let returnArray = [] for (let thisEvent of tempArray) { returnArray.push(thisEvent.id) } return returnArray }, parseDateEvents: function (eventArray) { let columnArray = [[]] let gridTimeMap = new Map() for (let eventId of eventArray) { let thisEvent = this.parsed.byId[eventId] let gridTimes = this.getGridTimeSlots(thisEvent) for (let gridCounter = gridTimes.start; gridCounter <= gridTimes.end; gridCounter++) { if (gridTimeMap.has(gridCounter)) { gridTimeMap.set(gridCounter, gridTimeMap.get(gridCounter) + 1) } else { gridTimeMap.set(gridCounter, 1) } } let foundAColumn = false for (let columnIndex in columnArray) { if (this.hasSlotForEvent(thisEvent, columnArray[columnIndex])) { columnArray[columnIndex].push(thisEvent) foundAColumn = true break } } if (!foundAColumn) { columnArray.push([thisEvent]) } } // let numberOfColumns = columnArray.length for (let columnIndex in columnArray) { for (let thisEvent of columnArray[columnIndex]) { // thisEvent.numberOfOverlaps = numberOfColumns - 1 thisEvent.numberOfOverlaps = this.getMaxOfGrid(thisEvent, gridTimeMap) - 1 thisEvent.overlapIteration = parseInt(columnIndex) + 1 } } // make column count corrections for overlapping events that overlap with other events. Confusing. for (let eventId of eventArray) { let thisEvent = this.parsed.byId[eventId] thisEvent.numberOfOverlaps = this.getMaxOverlapsForEvent(thisEvent, eventArray) } }, eventsOverlap: function (event1, event2) { // const interval1 = this.getIntervalFromEvent(event1) // const interval2 = this.getIntervalFromEvent(event2) // return interval1.overlaps(interval2) return this.getIntervalFromEvent(event1).overlaps(this.getIntervalFromEvent(event2)) }, getIntervalFromEvent: function (thisEvent) { return Interval.fromDateTimes( thisEvent.start.dateObject, thisEvent.end.dateObject ) }, getMaxOverlapsForEvent: function (testEvent, eventArray) { let maxOverlaps = testEvent.numberOfOverlaps for (let eventId of eventArray) { const thisEvent = this.parsed.byId[eventId] if (this.eventsOverlap(testEvent, thisEvent)) { if (thisEvent.numberOfOverlaps > testEvent.numberOfOverlaps) { maxOverlaps = thisEvent.numberOfOverlaps } } } return maxOverlaps }, hasSlotForEvent: function (checkEvent, existingEvents = []) { let slotAvailable = true for (let thisEvent of existingEvents) { if ( // case 1: top of checkEvent overlaps bottom of thisEvent checkEvent.start.dateObject >= thisEvent.start.dateObject && checkEvent.start.dateObject < thisEvent.end.dateObject ) { slotAvailable = false break } else if ( // case 2: bottom of checkEvent overlaps top of thisEvent checkEvent.end.dateObject > thisEvent.start.dateObject && checkEvent.end.dateObject <= thisEvent.end.dateObject ) { slotAvailable = false break } else if ( // case 3: checkEvent falls inside of thisEvent checkEvent.start.dateObject >= thisEvent.start.dateObject && checkEvent.end.dateObject <= thisEvent.end.dateObject ) { slotAvailable = false break } else if ( // case 4: checkEvent encompasses all of thisEvent checkEvent.start.dateObject <= thisEvent.start.dateObject && checkEvent.end.dateObject >= thisEvent.end.dateObject ) { slotAvailable = false break } } return slotAvailable }, getGridTimeSlots: function (thisEvent) { return { start: this.getGridTime(thisEvent.start.dateObject, false), end: this.getGridTime(thisEvent.end.dateObject, true) - 1 } }, getGridTime: function (dateObject, roundUp = false) { dateObject = this.makeDT(dateObject) // just in case const gridCalc = ((dateObject.hour * 60) + dateObject.minute) / gridBlockSize if (roundUp) { return Math.ceil(gridCalc) } else { return Math.floor(gridCalc) } }, getMaxOfGrid: function (thisEvent, gridTimeMap) { // TODO: there's probably a fancier Collections way to do this let max = 0 const gridTimes = this.getGridTimeSlots(thisEvent) for (let gridCounter = gridTimes.start; gridCounter <= gridTimes.end; gridCounter++) { if (gridTimeMap.has(gridCounter) && gridTimeMap.get(gridCounter) > max) { max = gridTimeMap.get(gridCounter) } } return max }, parseGetDurationMinutes: function (eventObj) { if (eventObj.start.isAllDay) { return 24 * 60 } else { return eventObj.end.dateObject.diff( eventObj.start.dateObject, 'minutes' ) } }, getPassedInParsedEvents: function () { this.parsed = defaultParsed if ( this.parsedEvents !== undefined && this.parsedEvents.byId !== undefined && Object.keys(this.parsedEvents).length > 0 ) { this.parsed = this.parsedEvents return true } else { return false } }, getPassedInEventArray: function () { this.parsed = defaultParsed if (this.eventArray !== undefined && this.eventArray.length > 0) { this.parseEventList() return true } else { return false } }, getDefaultParsed: function () { return defaultParsed }, isParsedEventsEmpty: function () { return !( this.parsedEvents !== undefined && this.parsedEvents.byId !== undefined && Object.keys(this.parsedEvents).length > 0 ) }, isEventArrayEmpty: function () { return !(this.eventArray !== undefined && this.eventArray.length > 0) }, handlePassedInEvents: function () { if (!this.isParsedEventsEmpty()) { this.getPassedInParsedEvents() } else if (!this.isEventArrayEmpty()) { this.getPassedInEventArray() } }, handleEventUpdate: function (eventObject) { if (dashHas(this._props, 'fullComponentRef') && this._props.fullComponentRef) { // this component has a calendar parent, so don't move forward return } let thisEventId = eventObject.id // update eventArray for (let thisEventIndex in this.eventArray) { if (this.eventArray[thisEventIndex].id === thisEventId) { this.eventArray[thisEventIndex] = eventObject this.parseEventList() } } }, formatTimeRange: function (startTime, endTime) { let returnString = '' // start time returnString += this.simplifyTimeFormat( this.makeDT(startTime).toLocaleString(DateTime.TIME_SIMPLE), (this.formatDate(startTime, 'a') === this.formatDate(endTime, 'a')) ) returnString += ' - ' // end time returnString += this.simplifyTimeFormat( this.makeDT(endTime).toLocaleString(DateTime.TIME_SIMPLE), false ) return returnString }, formatTime: function (startTime) { let returnString = this.makeDT(startTime).toLocaleString(DateTime.TIME_SIMPLE) // simplify if AM / PM present if (returnString.includes('M')) { returnString = returnString.replace(':00', '') // remove minutes if = ':00' .replace(' AM', 'am') .replace(' PM', 'pm') } return returnString }, getEventDuration: function (startTime, endTime) { return Math.floor( this.makeDT(endTime).diff(this.makeDT(startTime)).as('minutes') ) } }, mounted () {} }