473 lines
15 KiB
JavaScript
473 lines
15 KiB
JavaScript
import { createElementVNode as _createElementVNode, createTextVNode as _createTextVNode, mergeProps as _mergeProps, withDirectives as _withDirectives } from "vue";
|
|
// Styles
|
|
import "./calendarWithEvents.css";
|
|
|
|
// Composables
|
|
import { useCalendarBase } from "./calendarBase.js"; // Directives
|
|
import vRipple from "../../../directives/ripple/index.js"; // Utilities
|
|
import { computed, ref } from 'vue';
|
|
import { CalendarEventOverlapModes } from "../modes/index.js";
|
|
import { isEventHiddenOn, isEventOn, isEventOnDay, isEventOverlapping, isEventStart, parseEvent } from "../util/events.js";
|
|
import { diffMinutes, getDayIdentifier } from "../util/timestamp.js";
|
|
import { getPrefixedEventHandlers, propsFactory } from "../../../util/index.js"; // Types
|
|
// Constants
|
|
const WIDTH_FULL = 100;
|
|
const WIDTH_START = 95;
|
|
// const MINUTES_IN_DAY = 1440
|
|
|
|
// Prevent import from being erased
|
|
void vRipple;
|
|
export const makeCalendarWithEventsProps = propsFactory({
|
|
events: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
eventStart: {
|
|
type: String,
|
|
default: 'start'
|
|
},
|
|
eventEnd: {
|
|
type: String,
|
|
default: 'end'
|
|
},
|
|
eventTimed: {
|
|
type: [String, Function],
|
|
default: 'timed'
|
|
},
|
|
eventCategory: {
|
|
type: [String, Function],
|
|
default: 'category'
|
|
},
|
|
eventHeight: {
|
|
type: Number,
|
|
default: 20
|
|
},
|
|
eventColor: {
|
|
type: [String, Function],
|
|
default: 'primary'
|
|
},
|
|
eventTextColor: {
|
|
type: [String, Function]
|
|
},
|
|
eventName: {
|
|
type: [String, Function],
|
|
default: 'name'
|
|
},
|
|
eventOverlapThreshold: {
|
|
type: [String, Number],
|
|
default: 60
|
|
},
|
|
eventOverlapMode: {
|
|
type: [String, Function],
|
|
default: 'stack',
|
|
validate: mode => mode in CalendarEventOverlapModes || typeof mode === 'function'
|
|
},
|
|
eventMore: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
eventMoreText: {
|
|
type: String,
|
|
default: '$vuetify.calendar.moreEvents'
|
|
},
|
|
eventRipple: {
|
|
type: [Boolean, Object],
|
|
default: null
|
|
},
|
|
eventMarginBottom: {
|
|
type: Number,
|
|
default: 1
|
|
}
|
|
}, 'VCalendar-events');
|
|
export function useCalendarWithEvents(props, slots, attrs) {
|
|
const base = useCalendarBase(props);
|
|
const noEvents = computed(() => {
|
|
return !Array.isArray(props.events) || props.events.length === 0;
|
|
});
|
|
const categoryMode = computed(() => {
|
|
return props.type === 'category';
|
|
});
|
|
const eventTimedFunction = computed(() => {
|
|
return typeof props.eventTimed === 'function' ? props.eventTimed : event => !!event[props.eventTimed];
|
|
});
|
|
const eventCategoryFunction = computed(() => {
|
|
return typeof props.eventCategory === 'function' ? props.eventCategory : event => event[props.eventCategory];
|
|
});
|
|
const parsedEvents = computed(() => {
|
|
if (!props.events) return [];
|
|
return props.events.map((event, index) => parseEvent(event, index, props.eventStart || '', props.eventEnd || '', eventTimedFunction.value(event), categoryMode.value ? eventCategoryFunction.value(event) : false));
|
|
});
|
|
const parsedEventOverlapThreshold = computed(() => {
|
|
return parseInt(String(props.eventOverlapThreshold || 0));
|
|
});
|
|
const eventTextColorFunction = computed(() => {
|
|
return typeof props.eventTextColor === 'function' ? props.eventTextColor : () => props.eventTextColor;
|
|
});
|
|
const eventNameFunction = computed(() => {
|
|
return typeof props.eventName === 'function' ? props.eventName : (event, timedEvent) => event.input[props.eventName] || '';
|
|
});
|
|
const eventModeFunction = computed(() => {
|
|
return typeof props.eventOverlapMode === 'function' ? props.eventOverlapMode : CalendarEventOverlapModes[props.eventOverlapMode];
|
|
});
|
|
const eventWeekdays = computed(() => {
|
|
return base.effectiveWeekdays.value;
|
|
});
|
|
function eventColorFunction(e) {
|
|
return typeof props.eventColor === 'function' ? props.eventColor(e) : e.color || props.eventColor;
|
|
}
|
|
const eventsRef = ref([]);
|
|
function updateEventVisibility() {
|
|
if (noEvents.value || !props.eventMore) {
|
|
return;
|
|
}
|
|
const eventHeight = props.eventHeight || 0;
|
|
const eventsMap = getEventsMap();
|
|
for (const date in eventsMap) {
|
|
const {
|
|
parent,
|
|
events,
|
|
more
|
|
} = eventsMap[date];
|
|
if (!more) {
|
|
break;
|
|
}
|
|
const parentBounds = parent.getBoundingClientRect();
|
|
const last = events.length - 1;
|
|
const eventsSorted = events.map(event => ({
|
|
event,
|
|
bottom: event.getBoundingClientRect().bottom
|
|
})).sort((a, b) => a.bottom - b.bottom);
|
|
let hidden = 0;
|
|
for (let i = 0; i <= last; i++) {
|
|
const bottom = eventsSorted[i].bottom;
|
|
const hide = i === last ? bottom > parentBounds.bottom : bottom + eventHeight > parentBounds.bottom;
|
|
if (hide) {
|
|
eventsSorted[i].event.style.display = 'none';
|
|
hidden++;
|
|
}
|
|
}
|
|
|
|
// TODO: avoid direct DOM manipulation
|
|
if (hidden) {
|
|
more.style.display = '';
|
|
more.innerHTML = base.locale.t(props.eventMoreText, hidden);
|
|
} else {
|
|
more.style.display = 'none';
|
|
}
|
|
}
|
|
}
|
|
function getEventsMap() {
|
|
const eventsMap = {};
|
|
const elements = eventsRef.value;
|
|
if (!elements || !elements.length) {
|
|
return eventsMap;
|
|
}
|
|
elements.forEach(el => {
|
|
const date = el.getAttribute('data-date');
|
|
if (el.parentElement && date) {
|
|
if (!(date in eventsMap)) {
|
|
eventsMap[date] = {
|
|
parent: el.parentElement,
|
|
more: null,
|
|
events: []
|
|
};
|
|
}
|
|
if (el.getAttribute('data-more')) {
|
|
eventsMap[date].more = el;
|
|
} else {
|
|
eventsMap[date].events.push(el);
|
|
el.style.display = '';
|
|
}
|
|
}
|
|
});
|
|
return eventsMap;
|
|
}
|
|
function genDayEvent({
|
|
event
|
|
}, day) {
|
|
const eventHeight = props.eventHeight || 0;
|
|
const eventMarginBottom = props.eventMarginBottom || 0;
|
|
const dayIdentifier = getDayIdentifier(day);
|
|
const week = day.week;
|
|
const start = dayIdentifier === event.startIdentifier;
|
|
let end = dayIdentifier === event.endIdentifier;
|
|
let width = WIDTH_START;
|
|
if (!categoryMode.value) {
|
|
for (let i = day.index + 1; i < week.length; i++) {
|
|
const weekdayIdentifier = getDayIdentifier(week[i]);
|
|
if (event.endIdentifier >= weekdayIdentifier) {
|
|
width += WIDTH_FULL;
|
|
end = end || weekdayIdentifier === event.endIdentifier;
|
|
} else {
|
|
end = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
const scope = {
|
|
eventParsed: event,
|
|
day,
|
|
start,
|
|
end,
|
|
timed: false
|
|
};
|
|
return genEvent(event, scope, false, {
|
|
class: ['v-event', {
|
|
'v-event-start': start,
|
|
'v-event-end': end
|
|
}],
|
|
style: {
|
|
height: `${eventHeight}px`,
|
|
width: `${width}%`,
|
|
marginBottom: `${eventMarginBottom}px`
|
|
},
|
|
'data-date': day.date
|
|
});
|
|
}
|
|
function genTimedEvent({
|
|
event,
|
|
left,
|
|
width
|
|
}, day) {
|
|
const startDelta = day.timeDelta(event.start, day);
|
|
const endDelta = day.timeDelta(event.end, day);
|
|
if (endDelta === false || startDelta === false || endDelta < 0 || startDelta >= 1 || isEventHiddenOn(event, day)) {
|
|
return false;
|
|
}
|
|
const dayIdentifier = getDayIdentifier(day);
|
|
const start = event.startIdentifier >= dayIdentifier;
|
|
const end = event.endIdentifier > dayIdentifier;
|
|
const top = day.timeToY(event.start, day);
|
|
const bottom = day.timeToY(event.end, day);
|
|
const height = Math.max(props.eventHeight || 0, bottom - top);
|
|
const scope = {
|
|
eventParsed: event,
|
|
day,
|
|
start,
|
|
end,
|
|
timed: true
|
|
};
|
|
return genEvent(event, scope, true, {
|
|
class: 'v-event-timed',
|
|
style: {
|
|
top: `${top}px`,
|
|
height: `${height}px`,
|
|
left: `${left}%`,
|
|
width: `${width}%`
|
|
}
|
|
});
|
|
}
|
|
function genEvent(event, scopeInput, timedEvent, data) {
|
|
const slot = slots.event;
|
|
const text = eventTextColorFunction.value(event.input);
|
|
const background = eventColorFunction(event.input);
|
|
const overlapsNoon = event.start.hour < 12 && event.end.hour >= 12;
|
|
const singline = diffMinutes(event.start, event.end) <= parsedEventOverlapThreshold.value;
|
|
const formatTime = (withTime, ampm) => {
|
|
const formatter = base.getFormatter({
|
|
timeZone: 'UTC',
|
|
hour: 'numeric',
|
|
minute: withTime.minute > 0 ? 'numeric' : undefined
|
|
});
|
|
return formatter(withTime, true);
|
|
};
|
|
const timeSummary = () => formatTime(event.start, overlapsNoon) + ' - ' + formatTime(event.end, true);
|
|
const eventSummary = () => {
|
|
const name = eventNameFunction.value(event, timedEvent);
|
|
if (event.start.hasTime) {
|
|
if (timedEvent) {
|
|
const time = timeSummary();
|
|
const delimiter = singline ? ', ' : _createElementVNode("br", null, null);
|
|
return _createElementVNode("span", {
|
|
"class": "v-event-summary"
|
|
}, [_createElementVNode("strong", null, [name]), delimiter, time]);
|
|
} else {
|
|
const time = formatTime(event.start, true);
|
|
return _createElementVNode("span", {
|
|
"class": "v-event-summary"
|
|
}, [_createElementVNode("strong", null, [time]), _createTextVNode(" "), name]);
|
|
}
|
|
}
|
|
return _createElementVNode("span", {
|
|
"class": "v-event-summary"
|
|
}, [name]);
|
|
};
|
|
const scope = {
|
|
...scopeInput,
|
|
event: event.input,
|
|
outside: scopeInput.day.outside,
|
|
singline,
|
|
overlapsNoon,
|
|
formatTime,
|
|
timeSummary,
|
|
eventSummary
|
|
};
|
|
const events = getPrefixedEventHandlers(attrs, ':event', nativeEvent => ({
|
|
...scope,
|
|
nativeEvent
|
|
}));
|
|
return _withDirectives(_createElementVNode("div", _mergeProps(base.getColorProps({
|
|
text,
|
|
background
|
|
}), events, data, {
|
|
"ref_for": true,
|
|
"ref": eventsRef
|
|
}), [slot?.(scope) ?? genName(eventSummary)]), [[vRipple, props.eventRipple ?? true]]);
|
|
}
|
|
function genName(eventSummary) {
|
|
return _createElementVNode("div", {
|
|
"class": "pl-1"
|
|
}, [eventSummary()]);
|
|
}
|
|
function genPlaceholder(day) {
|
|
const height = (props.eventHeight || 0) + (props.eventMarginBottom || 0);
|
|
return _createElementVNode("div", {
|
|
"style": {
|
|
height: `${height}px`
|
|
},
|
|
"data-date": day.date,
|
|
"ref_for": true,
|
|
"ref": eventsRef
|
|
}, null);
|
|
}
|
|
function genMore(day) {
|
|
const eventHeight = props.eventHeight || 0;
|
|
const eventMarginBottom = props.eventMarginBottom || 0;
|
|
const events = getPrefixedEventHandlers(attrs, ':more', nativeEvent => ({
|
|
nativeEvent,
|
|
...day
|
|
}));
|
|
return _withDirectives(_createElementVNode("div", _mergeProps({
|
|
"class": ['v-event-more pl-1', {
|
|
'v-outside': day.outside
|
|
}],
|
|
"data-date": day.date,
|
|
"data-more": "1",
|
|
"style": {
|
|
display: 'none',
|
|
height: `${eventHeight}px`,
|
|
marginBottom: `${eventMarginBottom}px`
|
|
},
|
|
"ref_for": true,
|
|
"ref": eventsRef
|
|
}, events), null), [[vRipple, props.eventRipple ?? true]]);
|
|
}
|
|
function getVisibleEvents() {
|
|
const days = base.days.value;
|
|
const start = getDayIdentifier(days[0]);
|
|
const end = getDayIdentifier(days[days.length - 1]);
|
|
return parsedEvents.value.filter(event => isEventOverlapping(event, start, end));
|
|
}
|
|
function isEventForCategory(event, category) {
|
|
return !categoryMode.value || typeof category === 'object' && category.categoryName && category.categoryName === event.category || typeof event.category === 'string' && category === event.category || typeof event.category !== 'string' && category === null;
|
|
}
|
|
function getEventsForDay(day) {
|
|
const identifier = getDayIdentifier(day);
|
|
const firstWeekday = eventWeekdays.value[0];
|
|
return parsedEvents.value.filter(event => isEventStart(event, day, identifier, firstWeekday));
|
|
}
|
|
function getEventsForDayAll(day) {
|
|
const identifier = getDayIdentifier(day);
|
|
const firstWeekday = eventWeekdays.value[0];
|
|
return parsedEvents.value.filter(event => event.allDay && (categoryMode.value ? isEventOn(event, identifier) : isEventStart(event, day, identifier, firstWeekday)) && isEventForCategory(event, day.category));
|
|
}
|
|
function getEventsForDayTimed(day) {
|
|
return parsedEvents.value.filter(event => !event.allDay && isEventOnDay(event, day, day.intervalRange) && isEventForCategory(event, day.category));
|
|
}
|
|
function getScopedSlots() {
|
|
if (noEvents.value) {
|
|
return {
|
|
...slots
|
|
};
|
|
}
|
|
const mode = eventModeFunction.value(parsedEvents.value, eventWeekdays.value[0], parsedEventOverlapThreshold.value);
|
|
const isNode = input => !!input;
|
|
const getSlotChildren = (day, getter, mapper, timed) => {
|
|
const events = getter(day);
|
|
const visuals = mode(day, events, timed, categoryMode.value);
|
|
if (timed) {
|
|
return visuals.map(visual => mapper(visual, day)).filter(isNode);
|
|
}
|
|
const children = [];
|
|
visuals.forEach((visual, index) => {
|
|
while (children.length < visual.column) {
|
|
children.push(genPlaceholder(day));
|
|
}
|
|
const mapped = mapper(visual, day);
|
|
if (mapped) {
|
|
children.push(mapped);
|
|
}
|
|
});
|
|
return children;
|
|
};
|
|
return {
|
|
...slots,
|
|
day: day => {
|
|
let children = getSlotChildren(day, getEventsForDay, genDayEvent, false);
|
|
if (children && children.length > 0 && props.eventMore) {
|
|
children.push(genMore(day));
|
|
}
|
|
if (slots.day) {
|
|
const slot = slots.day(day);
|
|
if (slot) {
|
|
children = children ? children.concat(slot) : slot;
|
|
}
|
|
}
|
|
return children;
|
|
},
|
|
'day-header': day => {
|
|
let children = getSlotChildren(day, getEventsForDayAll, genDayEvent, false);
|
|
if (slots['day-header']) {
|
|
const slot = slots['day-header'](day);
|
|
if (slot) {
|
|
children = children ? children.concat(slot) : slot;
|
|
}
|
|
}
|
|
return children;
|
|
},
|
|
'day-body': day => {
|
|
const events = getSlotChildren(day, getEventsForDayTimed, genTimedEvent, true);
|
|
let children = [_createElementVNode("div", {
|
|
"class": "v-event-timed-container"
|
|
}, [events])];
|
|
if (slots['day-body']) {
|
|
const slot = slots['day-body'](day);
|
|
if (slot) {
|
|
children = children.concat(slot);
|
|
}
|
|
}
|
|
return children;
|
|
}
|
|
};
|
|
}
|
|
return {
|
|
...base,
|
|
noEvents,
|
|
parsedEvents,
|
|
parsedEventOverlapThreshold,
|
|
eventTimedFunction,
|
|
eventCategoryFunction,
|
|
eventTextColorFunction,
|
|
eventNameFunction,
|
|
eventModeFunction,
|
|
eventWeekdays,
|
|
categoryMode,
|
|
eventColorFunction,
|
|
eventsRef,
|
|
updateEventVisibility,
|
|
getEventsMap,
|
|
genDayEvent,
|
|
genTimedEvent,
|
|
genEvent,
|
|
genName,
|
|
genPlaceholder,
|
|
genMore,
|
|
getVisibleEvents,
|
|
isEventForCategory,
|
|
getEventsForDay,
|
|
getEventsForDayAll,
|
|
getEventsForDayTimed,
|
|
getScopedSlots
|
|
};
|
|
}
|
|
//# sourceMappingURL=calendarWithEvents.js.map
|