@nextcloud/calendar-availability-vue
Version:
Weekly calendar availability component for Nextcloud apps
377 lines (376 loc) • 12.6 kB
JavaScript
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