UNPKG

@sbh321/qcalendar

Version:

A forked version of Jeff Galbraith's fork of Quasar UI QCalendar

651 lines (592 loc) 17.1 kB
// interval composables import { computed } from 'vue' import { addToDate, createDayList, createIntervalList, createNativeLocaleFormatter, copyTimestamp, getDateTime, getDayTimeIdentifier, getStartOfWeek, getEndOfWeek, parsed, parseTime, updateMinutes, updateRelative, validateNumber } from '../utils/Timestamp.js' import { animVerticalScrollTo, animHorizontalScrollTo } from '../utils/scroll.js' export const useIntervalProps = { view: { type: String, validator: v => [ 'day', 'week', 'month', 'month-interval' ].includes(v), default: 'day' }, shortIntervalLabel: Boolean, intervalHeight: { type: [ Number, String ], default: 40, validator: validateNumber }, intervalMinutes: { type: [ Number, String ], default: 60, validator: validateNumber }, intervalStart: { type: [ Number, String ], default: 0, validator: validateNumber }, intervalCount: { type: [ Number, String ], default: 24, validator: validateNumber }, intervalStyle: { type: Function, default: null }, intervalClass: { type: Function, default: null }, weekdayStyle: { type: Function, default: null }, weekdayClass: { type: Function, default: null }, showIntervalLabel: { type: Function, default: null }, hour24Format: Boolean, timeClicksClamped: Boolean, dateHeader: { type: String, default: 'stacked', validator: v => [ 'stacked', 'inline', 'inverted' ].includes(v) } } export const useSchedulerProps = { view: { type: String, validator: v => [ 'day', 'week', 'month', 'month-interval' ].includes(v), default: 'day' }, modelResources: { type: Array // required: true }, resourceKey: { type: [ String, Number ], default: 'id' }, resourceLabel: { type: [ String, Number ], default: 'label' }, resourceHeight: { type: [ Number, String ], default: 0, validator: validateNumber }, resourceMinHeight: { type: [ Number, String ], default: 70, validator: validateNumber }, resourceStyle: { type: Function, default: null }, resourceClass: { type: Function, default: null }, weekdayStyle: { type: Function, default: null }, weekdayClass: { type: Function, default: null }, dayStyle: { type: Function, default: null }, dayClass: { type: Function, default: null }, dateHeader: { type: String, default: 'stacked', validator: v => [ 'stacked', 'inline', 'inverted' ].includes(v) } } export const useAgendaProps = { view: { type: String, validator: v => [ 'day', 'week', 'month', 'month-interval' ].includes(v), default: 'day' }, leftColumnOptions: { type: Array }, rightColumnOptions: { type: Array }, columnOptionsId: { type: String }, columnOptionsLabel: { type: String }, weekdayStyle: { type: Function, default: null }, weekdayClass: { type: Function, default: null }, dayStyle: { type: Function, default: null }, dayClass: { type: Function, default: null }, dateHeader: { type: String, default: 'stacked', validator: v => [ 'stacked', 'inline', 'inverted' ].includes(v) }, dayHeight: { type: [ Number, String ], default: 0, validator: validateNumber }, dayMinHeight: { type: [ Number, String ], default: 40, validator: validateNumber } } export const useResourceProps = { modelResources: { type: Array // required: true }, resourceKey: { type: [ String, Number ], default: 'id' }, resourceLabel: { type: [ String, Number ], default: 'label' }, resourceHeight: { type: [ Number, String ], default: 0, validator: validateNumber }, resourceMinHeight: { type: [ Number, String ], default: 70, validator: validateNumber }, resourceStyle: { type: Function, default: null }, resourceClass: { type: Function, default: null }, cellWidth: { type: [ Number, String ], default: 100 }, intervalHeaderHeight: { type: [ Number, String ], default: 20, validator: validateNumber }, noSticky: Boolean } export default function (props, { weekdaySkips, times, scrollArea, parsedStart, parsedEnd, maxDays, size, headerColumnRef }) { const parsedIntervalStart = computed(() => parseInt(props.intervalStart, 10)) const parsedIntervalMinutes = computed(() => parseInt(props.intervalMinutes, 10)) const parsedIntervalCount = computed(() => parseInt(props.intervalCount, 10)) const parsedIntervalHeight = computed(() => parseFloat(props.intervalHeight)) const parsedCellWidth = computed(() => { let width = 0 if (props.cellWidth) { width = props.cellWidth } else if (size.width > 0 && headerColumnRef.value) { width = headerColumnRef.value.offsetWidth / (props.columnCount > 1 ? props.columnCount : maxDays.value) } return width }) const parsedStartMinute = computed(() => parsedIntervalStart.value * parsedIntervalMinutes.value) const bodyHeight = computed(() => parsedIntervalCount.value * parsedIntervalHeight.value) const bodyWidth = computed(() => parsedIntervalCount.value * parsedCellWidth.value) const parsedWeekStart = computed(() => startOfWeek(parsedStart.value)) const parsedWeekEnd = computed(() => endOfWeek(parsedEnd.value)) /** * Returns the days of the specified week */ const days = computed(() => { return createDayList( parsedStart.value, parsedEnd.value, times.today, weekdaySkips.value, props.disabledBefore, props.disabledAfter, props.disabledWeekdays, props.disabledDays, maxDays.value ) }) /** * Returns an interval list for each day */ const intervals = computed(() => { return days.value.map(day => createIntervalList( day, parsedIntervalStart.value, parsedIntervalMinutes.value, parsedIntervalCount.value, times.now )) }) function startOfWeek (timestamp) { return getStartOfWeek(timestamp, props.weekdays, times.today) } function endOfWeek (timestamp) { return getEndOfWeek(timestamp, props.weekdays, times.today) } /** * Returns true if Timestamp is within passed Array of Timestamps * @param {Array.<Timestamp>} arr * @param {Timestamp} timestamp */ function arrayHasDateTime (arr, timestamp) { return arr && arr.length > 0 && arr.includes(getDateTime(timestamp)) } /** * Takes an array of 2 Timestamps and validates the passed Timestamp (second param) * @param {Array.<Timestamp>} arr * @param {Timestamp} timestamp * @returns {Object.<{firstDay: Boolean, betweenDays: Boolean, lastDay: Boolean}>} */ function checkIntervals (arr, timestamp) { const days = { firstDay: false, betweenDays: false, lastDay: false } // array must have two dates ('YYYY-MM-DD HH:MM') in it if (arr && arr.length === 2) { const current = getDayTimeIdentifier(timestamp) const first = getDayTimeIdentifier(parsed(arr[ 0 ])) const last = getDayTimeIdentifier(parsed(arr[ 1 ])) days.firstDay = first === current days.lastDay = last === current days.betweenDays = first < current && last > current } return days } function getIntervalClasses (interval, selectedDays = [], startEndDays = []) { const isSelected = arrayHasDateTime(selectedDays, interval) const { firstDay, lastDay, betweenDays } = checkIntervals(startEndDays, interval) return { 'q-selected': isSelected, 'q-range-first': firstDay === true, 'q-range': betweenDays === true, 'q-range-last': lastDay === true, 'q-disabled-interval disabled': interval.disabled === true } } function getResourceClasses (interval, selectedDays = [], startEndDays = []) { return [] } /** * Returns a function that uses the locale property * The function takes a timestamp and a boolean (to indicate short format) * and returns a formatted hour value from the browser */ const intervalFormatter = computed(() => { const longOptions = { timeZone: 'UTC', hour12: !props.hour24Format, hour: '2-digit', minute: '2-digit' } const shortOptions = { timeZone: 'UTC', hour12: !props.hour24Format, hour: 'numeric', minute: '2-digit' } const shortHourOptions = { timeZone: 'UTC', hour12: !props.hour24Format, hour: 'numeric' } return createNativeLocaleFormatter( props.locale, (tms, short) => (short ? (tms.minute === 0 ? shortHourOptions : shortOptions) : longOptions) ) }) /** * Returns a function that uses the locale property * The function takes a timestamp and a boolean (to indicate short format) * and returns a fully formatted timestamp string from the browser * that can be read with screen readers. * Note: This value also contains the time. */ const ariaDateTimeFormatter = computed(() => { const longOptions = { timeZone: 'UTC', dateStyle: 'full', timeStyle: 'short' } return createNativeLocaleFormatter( props.locale, (_tms) => longOptions ) }) function showIntervalLabelDefault (interval) { const first = intervals.value[ 0 ][ 0 ] const isFirst = first.hour === interval.hour && first.minute === interval.minute return !isFirst && interval.minute === 0 } function showResourceLabelDefault (resource) { } function styleDefault (interval) { return undefined } /** * Returns a Timestamp based on mouse click position on the calendar * Also handles touch events * This function is used for vertical intervals * @param {MouseEvent} e Browser MouseEvent * @param {Timestamp} day Timestamp associated with event * @param {Boolean} clamp Whether to clamp values to nearest interval * @param {Timestamp*} now Optional Timestamp for now date/time */ function getTimestampAtEventInterval (e, day, clamp = false, now = undefined) { let timestamp = copyTimestamp(day) const bounds = (e.currentTarget).getBoundingClientRect() const touchEvent = e const mouseEvent = e const touches = touchEvent.changedTouches || touchEvent.touches const clientY = touches && touches[ 0 ] ? touches[ 0 ].clientY : mouseEvent.clientY const addIntervals = (clientY - bounds.top) / parsedIntervalHeight.value const addMinutes = Math.floor((clamp ? Math.floor(addIntervals) : addIntervals) * parsedIntervalMinutes.value) if (addMinutes !== 0) { timestamp = addToDate(timestamp, { minute: addMinutes }) } if (now) { updateRelative(timestamp, now, true) } return timestamp } /** * Returns a Timestamp based on mouse click position on the calendar * Also handles touch events * This function is used for vertical intervals * @param {MouseEvent} e Browser MouseEvent * @param {Timestamp} day Timestamp associated with event * @param {Boolean} clamp Whether to clamp values to nearest interval * @param {Timestamp*} now Optional Timestamp for now date/time */ function getTimestampAtEvent (e, day, clamp = false, now = undefined) { let timestamp = copyTimestamp(day) const bounds = (e.currentTarget).getBoundingClientRect() const touchEvent = e const mouseEvent = e const touches = touchEvent.changedTouches || touchEvent.touches const clientY = touches && touches[ 0 ] ? touches[ 0 ].clientY : mouseEvent.clientY const addIntervals = (clientY - bounds.top) / parsedIntervalHeight.value const addMinutes = Math.floor((clamp ? Math.floor(addIntervals) : addIntervals) * parsedIntervalMinutes.value) if (addMinutes !== 0) { timestamp = addToDate(timestamp, { minute: addMinutes }) } if (now) { updateRelative(timestamp, now, true) } return timestamp } /** * Returns a Timestamp based on mouse click position on the calendar * Also handles touch events * This function is used for horizontal intervals * @param {MouseEvent} e Browser MouseEvent * @param {Timestamp} day Timestamp associated with event * @param {Boolean} clamp Whether to clamp values to nearest interval * @param {Timestamp*} now Optional Timestamp for now date/time */ function getTimestampAtEventX (e, day, clamp = false, now = undefined) { let timestamp = copyTimestamp(day) const bounds = (e.currentTarget).getBoundingClientRect() const touchEvent = e const mouseEvent = e const touches = touchEvent.changedTouches || touchEvent.touches const clientX = touches && touches[ 0 ] ? touches[ 0 ].clientX : mouseEvent.clientX const addIntervals = (clientX - bounds.left) / parsedCellWidth.value const addMinutes = Math.floor((clamp ? Math.floor(addIntervals) : addIntervals) * parsedIntervalMinutes.value) if (addMinutes !== 0) { timestamp = addToDate(timestamp, { minute: addMinutes }) } if (now) { updateRelative(timestamp, now, true) } return timestamp } /** * Returns the scope for the associated Timestamp * This function is used for vertical intervals * @param {Timestamp} timestamp * @param {Number} columnIndex */ function getScopeForSlot (timestamp, columnIndex) { const scope = { timestamp } scope.timeStartPos = timeStartPos scope.timeDurationHeight = timeDurationHeight if (columnIndex !== undefined) { scope.columnIndex = columnIndex } return scope } /** * Returns the scope for the associated Timestamp * This function is used for horizontal intervals * @param {Timestamp} timestamp * @param {Number*} index */ function getScopeForSlotX (timestamp, index) { const scope = { timestamp: copyTimestamp(timestamp) } scope.timeStartPosX = timeStartPosX scope.timeDurationWidth = timeDurationWidth if (index !== undefined) { scope.index = index } return scope } /** * Forces the browser to scroll to the specified time * This function is used for vertical intervals * @param {String} time in format HH:MM * @param {Number*} duration in milliseconds */ function scrollToTime (time, duration = 0) { const y = timeStartPos(time) if (y === false || !scrollArea.value) { return false } animVerticalScrollTo (scrollArea.value, y, duration) return true } /** * Forces the browser to scroll to the specified time * This function is used for horizontal intervals * @param {String} time in format HH:MM * @param {Number*} duration in milliseconds */ function scrollToTimeX (time, duration = 0) { const x = timeStartPosX(time) if (x === false || !scrollArea.value) { return false } animHorizontalScrollTo (scrollArea.value, x, duration) return true } function timeDurationHeight (minutes) { return minutes / parsedIntervalMinutes.value * parsedIntervalHeight.value } function timeDurationWidth (minutes) { return minutes / parsedIntervalMinutes.value * parsedCellWidth.value } function heightToMinutes (height) { return parseInt(height, 10) * parsedIntervalMinutes.value / parsedIntervalHeight.value } function widthToMinutes (width) { return parseInt(width, 10) * parsedIntervalMinutes.value / parsedCellWidth.value } function timeStartPos (time, clamp = true) { const minutes = parseTime(time) if (minutes === false) return false const min = parsedStartMinute.value const gap = parsedIntervalCount.value * parsedIntervalMinutes.value const delta = (minutes - min) / gap let y = delta * bodyHeight.value if (clamp) { if (y < 0) { y = 0 } if (y > bodyHeight.value) { y = bodyHeight.value } } return y } function timeStartPosX (time, clamp = true) { const minutes = parseTime(time) if (minutes === false) return false const min = parsedStartMinute.value const gap = parsedIntervalCount.value * parsedIntervalMinutes.value const delta = (minutes - min) / gap let x = delta * bodyWidth.value if (clamp) { if (x < 0) { x = 0 } if (x > bodyWidth.value) { x = bodyWidth.value } } return x } return { parsedIntervalStart, parsedIntervalMinutes, parsedIntervalCount, parsedIntervalHeight, parsedCellWidth, parsedStartMinute, bodyHeight, bodyWidth, parsedWeekStart, parsedWeekEnd, days, intervals, intervalFormatter, ariaDateTimeFormatter, arrayHasDateTime, checkIntervals, getIntervalClasses, getResourceClasses, showIntervalLabelDefault, showResourceLabelDefault, styleDefault, getTimestampAtEventInterval, getTimestampAtEvent, getTimestampAtEventX, getScopeForSlot, getScopeForSlotX, scrollToTime, scrollToTimeX, timeDurationHeight, timeDurationWidth, heightToMinutes, widthToMinutes, timeStartPos, timeStartPosX } }