UNPKG

@nextcloud/calendar-availability-vue

Version:

Weekly calendar availability component for Nextcloud apps

377 lines (376 loc) 12.6 kB
import './assets/index-CRf0xAEX.css'; import { NcButton, NcDateTimePickerNative } from "@nextcloud/vue"; import IconDelete from "vue-material-design-icons/Delete.vue"; import IconAdd from "vue-material-design-icons/Plus.vue"; import { getFirstDay } from "@nextcloud/l10n"; import { resolveComponent, createElementBlock, openBlock, Fragment, renderList, createElementVNode, createBlock, toDisplayString, createCommentVNode, createVNode, withCtx } from "vue"; import { getZoneString } from "icalzone"; import ICAL from "ical.js"; import { v4 } from "uuid"; import { getLoggerBuilder } from "@nextcloud/logger"; const _export_sfc = (sfc, props) => { const target = sfc.__vccOpts || sfc; for (const [key, val] of props) { target[key] = val; } return target; }; const _sfc_main = { name: "CalendarAvailability", components: { NcDateTimePickerNative, NcButton, IconAdd, IconDelete }, props: { slots: { type: Object, required: true }, loading: { type: Boolean, default: false }, l10nTo: { type: String, required: true }, l10nDeleteSlot: { type: String, required: true }, l10nEmptyDay: { type: String, required: true }, l10nAddSlot: { type: String, required: true }, l10nWeekDayListLabel: { type: String, default: "Weekdays" }, l10nMonday: { type: String, required: true }, l10nTuesday: { type: String, required: true }, l10nWednesday: { type: String, required: true }, l10nThursday: { type: String, required: true }, l10nFriday: { type: String, required: true }, l10nSaturday: { type: String, required: true }, l10nSunday: { type: String, required: true }, l10nStartPickerLabel: { type: Function, default: (dayName) => `Pick a start time for ${dayName}` }, l10nEndPickerLabel: { type: Function, default: (dayName) => `Pick a end time for ${dayName}` } }, data() { return { internalSlots: this.slotsToInternalData(this.slots) }; }, watch: { slots() { this.internalSlots = this.slotsToInternalData(this.slots); } }, methods: { timeStampSlotsToDateObjectSlots(slots) { return slots.map((slot) => ({ start: new Date(slot.start * 1e3), end: new Date(slot.end * 1e3) })); }, slotsToInternalData() { const moToSa = [ { id: "MO", displayName: this.l10nMonday, slots: this.timeStampSlotsToDateObjectSlots(this.slots.MO) }, { id: "TU", displayName: this.l10nTuesday, slots: this.timeStampSlotsToDateObjectSlots(this.slots.TU) }, { id: "WE", displayName: this.l10nWednesday, slots: this.timeStampSlotsToDateObjectSlots(this.slots.WE) }, { id: "TH", displayName: this.l10nThursday, slots: this.timeStampSlotsToDateObjectSlots(this.slots.TH) }, { id: "FR", displayName: this.l10nFriday, slots: this.timeStampSlotsToDateObjectSlots(this.slots.FR) }, { id: "SA", displayName: this.l10nSaturday, slots: this.timeStampSlotsToDateObjectSlots(this.slots.SA) } ]; const sunday = { id: "SU", displayName: this.l10nSunday, slots: this.timeStampSlotsToDateObjectSlots(this.slots.SU) }; return getFirstDay() === 1 ? [...moToSa, sunday] : [sunday, ...moToSa]; }, internalDataToSlots() { const converted = {}; this.internalSlots.forEach(({ id, slots }) => { converted[id] = slots.map((slot) => ({ start: Math.round(slot.start.getTime() / 1e3), end: Math.round(slot.end.getTime() / 1e3) })); }); return converted; }, addSlot(day) { const start = /* @__PURE__ */ new Date(); start.setHours(9, 0, 0, 0); const end = /* @__PURE__ */ new Date(); end.setHours(17, 0, 0, 0); day.slots.push({ start, end }); this.onChangeSlots(); }, removeSlot(day, idx) { day.slots.splice(idx, 1); this.onChangeSlots(); }, onChangeSlots() { this.$emit("update:slots", this.internalDataToSlots()); } } }; const _hoisted_1 = ["aria-label"]; const _hoisted_2 = { class: "label-weekday" }; const _hoisted_3 = ["id"]; const _hoisted_4 = { class: "availability-slot-group" }; const _hoisted_5 = { class: "to-text" }; const _hoisted_6 = { key: 0, class: "empty-content" }; function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) { const _component_NcDateTimePickerNative = resolveComponent("NcDateTimePickerNative"); const _component_IconDelete = resolveComponent("IconDelete"); const _component_NcButton = resolveComponent("NcButton"); const _component_IconAdd = resolveComponent("IconAdd"); return openBlock(), createElementBlock("ul", { class: "week-day-container", "aria-label": $props.l10nWeekDayListLabel }, [ (openBlock(true), createElementBlock(Fragment, null, renderList($data.internalSlots, (day) => { return openBlock(), createElementBlock("li", { key: `day-label-${day.id}`, class: "day-container" }, [ createElementVNode("div", _hoisted_2, [ createElementVNode("span", { id: day.displayName + "-label" }, toDisplayString(day.displayName), 9, _hoisted_3) ]), (openBlock(), createElementBlock("div", { key: `day-slots-${day.id}`, class: "availability-slots" }, [ createElementVNode("div", _hoisted_4, [ (openBlock(true), createElementBlock(Fragment, null, renderList(day.slots, (slot, idx) => { return openBlock(), createElementBlock("div", { key: `slot-${day.id}-${idx}`, class: "availability-slot" }, [ createVNode(_component_NcDateTimePickerNative, { id: `start-${day.id}-${idx}`, modelValue: slot.start, "onUpdate:modelValue": ($event) => slot.start = $event, type: "time", label: $props.l10nStartPickerLabel?.(day.displayName), "hide-label": true, class: "start-date", onChange: $options.onChangeSlots }, null, 8, ["id", "modelValue", "onUpdate:modelValue", "label", "onChange"]), createElementVNode("span", _hoisted_5, toDisplayString($props.l10nTo), 1), createVNode(_component_NcDateTimePickerNative, { id: `end-${day.id}-${idx}`, modelValue: slot.end, "onUpdate:modelValue": ($event) => slot.end = $event, type: "time", label: $props.l10nEndPickerLabel?.(day.displayName), "hide-label": true, class: "end-date", onChange: $options.onChangeSlots }, null, 8, ["id", "modelValue", "onUpdate:modelValue", "label", "onChange"]), (openBlock(), createBlock(_component_NcButton, { key: `slot-${day.id}-${idx}-btn`, type: "tertiary", class: "button", "aria-label": $props.l10nDeleteSlot, title: $props.l10nDeleteSlot, onClick: ($event) => $options.removeSlot(day, idx) }, { icon: withCtx(() => [ createVNode(_component_IconDelete, { size: 20 }) ]), _: 2 }, 1032, ["aria-label", "title", "onClick"])) ]); }), 128)) ]), day.slots.length === 0 ? (openBlock(), createElementBlock("span", _hoisted_6, toDisplayString($props.l10nEmptyDay), 1)) : createCommentVNode("", true) ])), (openBlock(), createBlock(_component_NcButton, { key: `add-slot-${day.id}`, disabled: $props.loading, class: "add-another button", title: $props.l10nAddSlot, "aria-label": $props.l10nAddSlot, onClick: ($event) => $options.addSlot(day) }, { icon: withCtx(() => [ createVNode(_component_IconAdd, { size: 20 }) ]), _: 2 }, 1032, ["disabled", "title", "aria-label", "onClick"])) ]); }), 128)) ], 8, _hoisted_1); } const CalendarAvailability = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render], ["__scopeId", "data-v-450e2481"]]); const logger = getLoggerBuilder().detectUser().setApp("@nextcloud/calendar-availability-vue").build(); function getEmptySlots() { return { MO: [], TU: [], WE: [], TH: [], FR: [], SA: [], SU: [] }; } function vavailabilityToSlots(vavailability) { const parsedIcal = ICAL.parse(vavailability); const vcalendarComp = new ICAL.Component(parsedIcal); const vavailabilityComp = vcalendarComp.getFirstSubcomponent("vavailability"); let timezoneId; const timezoneComp = vcalendarComp.getFirstSubcomponent("vtimezone"); if (timezoneComp) { timezoneId = timezoneComp.getFirstProperty("tzid").getFirstValue(); } const availableComps = vavailabilityComp.getAllSubcomponents("available"); const slots = getEmptySlots(); availableComps.forEach((availableComp) => { const startIcalDate = availableComp.getFirstProperty("dtstart").getFirstValue(); const endIcalDate = availableComp.getFirstProperty("dtend").getFirstValue(); const rrule = availableComp.getFirstProperty("rrule"); const start = /* @__PURE__ */ new Date(); start.setHours(startIcalDate.hour, startIcalDate.minute, 0, 0); const end = /* @__PURE__ */ new Date(); end.setHours(endIcalDate.hour, endIcalDate.minute, 0, 0); if (rrule.getFirstValue().freq !== "WEEKLY") { logger.warn("rrule not supported", { rrule: rrule.toICALString() }); return; } rrule.getFirstValue().getComponent("BYDAY").forEach((day) => { slots[day].push({ start: start.getTime() / 1e3, end: end.getTime() / 1e3 }); }); }); return { slots, timezoneId }; } function slotsToVavailability(slots, timezoneId) { const vcalendarComp = new ICAL.Component("vcalendar"); vcalendarComp.addPropertyWithValue("prodid", "Nextcloud DAV app"); const predefinedTimezoneIcal = getZoneString(timezoneId); if (predefinedTimezoneIcal) { const timezoneComp = new ICAL.Component(ICAL.parse(predefinedTimezoneIcal)); vcalendarComp.addSubcomponent(timezoneComp); } else { const timezoneComp = new ICAL.Component("vtimezone"); timezoneComp.addPropertyWithValue("tzid", timezoneId); vcalendarComp.addSubcomponent(timezoneComp); } const vavailabilityComp = new ICAL.Component("vavailability"); const deduplicated = slots.reduce((acc, slot) => { const start = new Date(slot.start * 1e3); const end = new Date(slot.end * 1e3); const key = [ start.getHours(), start.getMinutes(), end.getHours(), end.getMinutes() ].join("-"); return { ...acc, [key]: [...acc[key] ?? [], slot] }; }, {}); Object.keys(deduplicated).map((key) => { const slots2 = deduplicated[key]; const start = slots2[0].start; const end = slots2[0].end; const days = slots2.map((slot) => slot.day).filter((day, index, self) => self.indexOf(day) === index); const availableComp = new ICAL.Component("available"); const startTimeProp = availableComp.addPropertyWithValue("dtstart", ICAL.Time.fromJSDate(new Date(start * 1e3), false)); startTimeProp.setParameter("tzid", timezoneId); const endTimeProp = availableComp.addPropertyWithValue("dtend", ICAL.Time.fromJSDate(new Date(end * 1e3), false)); endTimeProp.setParameter("tzid", timezoneId); availableComp.addPropertyWithValue("uid", v4()); availableComp.addPropertyWithValue("rrule", { freq: "WEEKLY", byday: days }); return availableComp; }).map(vavailabilityComp.addSubcomponent.bind(vavailabilityComp)); vcalendarComp.addSubcomponent(vavailabilityComp); return vcalendarComp.toString(); } export { CalendarAvailability, getEmptySlots, slotsToVavailability, vavailabilityToSlots }; //# sourceMappingURL=index.mjs.map