@nextcloud/calendar-availability-vue
Version:
Weekly calendar availability component for Nextcloud apps
321 lines (320 loc) • 11.4 kB
JavaScript
import './assets/index-UKC4AV1t.css';
import NcDateTimePickerNative from "@nextcloud/vue/dist/Components/NcDateTimePickerNative.js";
import NcButton from "@nextcloud/vue/dist/Components/NcButton.js";
import IconDelete from "vue-material-design-icons/Delete.vue";
import IconAdd from "vue-material-design-icons/Plus.vue";
import { getFirstDay } from "@nextcloud/l10n";
import { getZoneString } from "icalzone";
import ICAL from "ical.js";
import { v4 } from "uuid";
import { getLoggerBuilder } from "@nextcloud/logger";
function normalizeComponent(scriptExports, render2, staticRenderFns, functionalTemplate, injectStyles, scopeId, moduleIdentifier, shadowMode) {
var options = typeof scriptExports === "function" ? scriptExports.options : scriptExports;
if (render2) {
options.render = render2;
options.staticRenderFns = staticRenderFns;
options._compiled = true;
}
{
options._scopeId = "data-v-" + scopeId;
}
return {
exports: scriptExports,
options
};
}
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 ".concat(dayName)
},
l10nEndPickerLabel: {
type: Function,
default: (dayName) => "Pick a end time for ".concat(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());
}
}
};
var _sfc_render = function render() {
var _vm = this, _c = _vm._self._c;
return _c("ul", { staticClass: "week-day-container", attrs: { "aria-label": _vm.l10nWeekDayListLabel } }, [_vm._l(_vm.internalSlots, function(day) {
return [_c("li", { key: "day-label-".concat(day.id), staticClass: "day-container" }, [_c("div", { staticClass: "label-weekday" }, [_c("span", { attrs: { "id": day.displayName + "-label" } }, [_vm._v(_vm._s(day.displayName))])]), _c("div", { key: "day-slots-".concat(day.id), staticClass: "availability-slots" }, [_c("div", { staticClass: "availability-slot-group" }, [_vm._l(day.slots, function(slot, idx) {
var _a, _b;
return [_c("div", { key: "slot-".concat(day.id, "-").concat(idx), staticClass: "availability-slot" }, [_c("NcDateTimePickerNative", { staticClass: "start-date", attrs: { "id": "start-".concat(day.id, "-").concat(idx), "type": "time", "label": (_a = _vm.l10nStartPickerLabel) == null ? void 0 : _a.call(_vm, day.displayName), "hide-label": true }, on: { "change": _vm.onChangeSlots }, model: { value: slot.start, callback: function($$v) {
_vm.$set(slot, "start", $$v);
}, expression: "slot.start" } }), _c("span", { staticClass: "to-text" }, [_vm._v(" " + _vm._s(_vm.l10nTo) + " ")]), _c("NcDateTimePickerNative", { staticClass: "end-date", attrs: { "id": "end-".concat(day.id, "-").concat(idx), "type": "time", "label": (_b = _vm.l10nEndPickerLabel) == null ? void 0 : _b.call(_vm, day.displayName), "hide-label": true }, on: { "change": _vm.onChangeSlots }, model: { value: slot.end, callback: function($$v) {
_vm.$set(slot, "end", $$v);
}, expression: "slot.end" } }), _c("NcButton", { key: "slot-".concat(day.id, "-").concat(idx, "-btn"), staticClass: "button", attrs: { "type": "tertiary", "aria-label": _vm.l10nDeleteSlot, "title": _vm.l10nDeleteSlot }, on: { "click": function($event) {
return _vm.removeSlot(day, idx);
} }, scopedSlots: _vm._u([{ key: "icon", fn: function() {
return [_c("IconDelete", { attrs: { "size": 20 } })];
}, proxy: true }], null, true) })], 1)];
})], 2), day.slots.length === 0 ? _c("span", { staticClass: "empty-content" }, [_vm._v(" " + _vm._s(_vm.l10nEmptyDay) + " ")]) : _vm._e()]), _c("NcButton", { key: "add-slot-".concat(day.id), staticClass: "add-another button", attrs: { "disabled": _vm.loading, "title": _vm.l10nAddSlot, "aria-label": _vm.l10nAddSlot }, on: { "click": function($event) {
return _vm.addSlot(day);
} }, scopedSlots: _vm._u([{ key: "icon", fn: function() {
return [_c("IconAdd", { attrs: { "size": 20 } })];
}, proxy: true }], null, true) })], 1)];
})], 2);
};
var _sfc_staticRenderFns = [];
var __component__ = /* @__PURE__ */ normalizeComponent(
_sfc_main,
_sfc_render,
_sfc_staticRenderFns,
false,
null,
"c15ebf8b"
);
const CalendarAvailability = __component__.exports;
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) => {
var _a;
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]: [...(_a = acc[key]) != null ? _a : [], 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
};