<template> <c-r-u-d-iterator i18n-key="alsijil.coursebook" :gql-query="gqlQuery" :gql-additional-query-args="gqlQueryArgs" :enable-create="false" :enable-edit="false" :elevated="false" @items="docsByDay = groupDocsByDay($event)" @lastQuery="lastQuery = $event" ref="iterator" disable-pagination hide-default-footer use-deep-search > <template #additionalActions="{ attrs, on }"> <coursebook-filters v-model="filters" /> </template> <template #default> <v-list-item v-for="day in listDocsByDay(docsByDay)" two-line :key="'day-' + day[0]" :id="'documentation_' + day[0].toISODate()" > <v-list-item-content> <v-subheader class="text-h6">{{ $d(day[0], "dateWithWeekday") }}</v-subheader> <v-list max-width="100%" class="pt-0 mt-n1"> <v-list-item v-for="doc in day.slice(1)" :key="'documentation-' + (doc.oldId || doc.id)" > <documentation-modal :documentation="doc" :affected-query="lastQuery" /> </v-list-item> </v-list> </v-list-item-content> </v-list-item> <date-select-footer :value="$route.hash.substring(1)" /> </template> <template #loading> <CoursebookLoader /> </template> <template #no-data> <CoursebookEmptyMessage icon="mdi-book-off-outline"> {{ $t("alsijil.coursebook.no_data") }} </CoursebookEmptyMessage> </template> <template #no-results> <CoursebookEmptyMessage icon="mdi-book-alert-outline"> {{ $t("alsijil.coursebook.no_results", { search: $refs.iterator.search }) }} </CoursebookEmptyMessage> </template> </c-r-u-d-iterator> </template> <script> import CRUDIterator from "aleksis.core/components/generic/CRUDIterator.vue"; import DateSelectFooter from "aleksis.core/components/generic/DateSelectFooter.vue"; import DocumentationModal from "./documentation/DocumentationModal.vue"; import { DateTime, Interval } from "luxon"; import { documentationsForCoursebook } from "./coursebook.graphql"; import CoursebookFilters from "./CoursebookFilters.vue"; import CoursebookLoader from "./CoursebookLoader.vue"; import CoursebookEmptyMessage from "./CoursebookEmptyMessage.vue"; export default { name: "Coursebook", components: { CoursebookEmptyMessage, CoursebookFilters, CoursebookLoader, CRUDIterator, DateSelectFooter, DocumentationModal, }, props: { filterType: { type: String, required: true, }, objId: { type: [Number, String], required: false, default: null, }, objType: { type: String, required: false, default: null, }, }, data() { return { gqlQuery: documentationsForCoursebook, knownDates: {}, docsByDay: {}, lastQuery: null, // Placeholder values while query isn't completed yet groups: [], courses: [], incomplete: false, }; }, computed: { // Assertion: Should only fire on page load or selection change. // Resets date range. gqlQueryArgs() { console.log('computing gqlQueryArgs'); const dateRange = this.resetDate(); return { own: this.filterType === "all" ? false : true, objId: this.objId ? Number(this.objId) : undefined, objType: this.objType?.toUpperCase(), dateStart: dateRange[0].toISODate(), dateEnd: dateRange[1].toISODate(), incomplete: !!this.incomplete, }; }, filters: { get() { return { objType: this.objType, objId: this.objId, filterType: this.filterType, incomplete: this.incomplete, }; }, set(selectedFilters) { if (Object.hasOwn(selectedFilters, "incomplete")) { this.incomplete = selectedFilters.incomplete; } else if ( Object.hasOwn(selectedFilters, "filterType") || Object.hasOwn(selectedFilters, "objId") || Object.hasOwn(selectedFilters, "objType") ) { this.$router.push({ name: "alsijil.coursebook", params: { filterType: selectedFilters.filterType ? selectedFilters.filterType : this.filterType, objType: selectedFilters.objType, objId: selectedFilters.objId, }, hash: this.$route.hash, }); } }, }, }, methods: { resetDate() { // Assure current date console.log('Resetting date range', this.$route.hash); if (!this.$route.hash) { console.log('Set default date'); this.$router.replace({ hash: DateTime.now().toISODate() }) } // Resetting known dates to dateRange around current date this.knownDates = {}; const dateRange = this.dateRange(DateTime.fromISO(this.$route.hash.substring(1))) dateRange.forEach((ts) => this.knownDates[ts] = true); const lastIdx = dateRange.length - 1; // Returning a dateRange each around first & last date for the initial query return [this.dateRange(dateRange[0])[0], this.dateRange(dateRange[lastIdx])[lastIdx]]; }, // => {dt: [dt doc ...] ...} groupDocsByDay(docs) { return docs.reduce((byDay, doc) => { // This works with dummy. Does actual doc have dateStart instead? const day = DateTime.fromISO(doc.datetimeStart).startOf("day"); byDay[day] ??= [day]; byDay[day].push(doc); return byDay; }, {}); }, // => [[dt doc ...] ...] listDocsByDay(docsByDay) { return Object.keys(docsByDay) .sort() .map((key) => docsByDay[key]); }, debounce(fn, delay) { let timer; return () => { console.log('debounce'); clearTimeout(timer); timer = setTimeout(fn, delay); } }, // Adapted from // https://github.com/vuejs/vuepress/blob/38e98634af117f83b6a32c8ff42488d91b66f663/packages/%40vuepress/plugin-active-header-links/clientRootMixin.js setCurrentDay() { const days = Array.from(document.querySelectorAll("[id^='documentation_']")); const scrollTop = Math.max( window.pageYOffset, document.documentElement.scrollTop, document.body.scrollTop ); for (let i = 0; i < days.length; i++) { const day = days[i]; const nextDay =days[i + 1]; if ((scrollTop >= day.offsetTop + 10 || i == 0) && (!nextDay || scrollTop < nextDay.offsetTop - 10)) { const date = day.id.split("_")[1]; if (date !== this.$route.hash.substring(1)) { this.gotoDate(date); } return } } }, /** * @param {"prev"|"next"} direction */ handleDateMove(direction) { const dateStartParsed = DateTime.fromISO(this.dateStart); const dateEndParsed = DateTime.fromISO(this.dateEnd); const dateParsed = DateTime.fromISO(this.date); const newDate = direction === "prev" ? dateParsed.minus({ days: 1 }) : dateParsed.plus({ days: 1 }); /* TODO: Everything below this line is also needed for when a date is selected via the calendar. → probably move this into a different function and create a second event listener for the input event. */ // Load 3 days into the future/past if (dateStartParsed >= newDate) { this.dateStart = newDate.minus({ days: 3 }).toISODate(); } if (dateEndParsed <= newDate) { this.dateEnd = newDate.plus({ days: 3 }).toISODate(); } this.$router.push({ name: "alsijil.coursebook", params: { filterType: this.filterType, objType: this.objType, objId: this.objId, }, }); // Define the function to find the nearest ID const ids = Array.from( document.querySelectorAll("[id^='documentation_']"), ).map((el) => el.id); // TODO: This should only be done after loading the new data const nearestId = this.findNearestId(newDate, direction, ids); this.$vuetify.goTo("#" + nearestId); }, findNearestId(targetDate, direction, ids) { const sortedIds = ids .map((id) => DateTime.fromISO(id.split("_")[1])) .sort((a, b) => a - b); if (direction === "prev") { sortedIds.reverse(); } const nearestId = sortedIds.find((id) => direction === "next" ? id >= targetDate : id <= targetDate, ) || sortedIds[sortedIds.length - 1]; return "documentation_" + nearestId.toISODate(); }, dateRange(date) { return Interval .fromDateTimes(date.minus({ days: 3 }), date.plus({ days: 4 })) .splitBy({ days: 1 }) .map((ts) => ts.start); }, // docsByDay: {dt: [dt doc ...] ...} assureDate(date) { if (!this.knownDates[date]) { // find missing & fetch missing range // date +- 5 days ? const dateRange = Interval .fromDateTimes(date.minus({ days: 3 }), date.plus({ days: 4 })) .splitBy({ days: 1 }) .map((ts) => ts.start); console.log('assureDate', dateRange.map((ts) => ts.toISODate())); // look up in docsByDay console.log('missing', dateRange.map((ts) => this.docsByDay[ts] )); console.log('missing', dateRange.filter((ts) => !this.docsByDay[ts] )); // dateRange.forEach((ts) => { this.docsByDay[ts.toISODate()] = 42 }); console.log('docsByDay', this.docsByDay); console.log('2024-03-29', this.docsByDay[DateTime.fromISO('2024-03-29')]); console.log('2024-03-29', dateRange[3], this.docsByDay[dateRange[3]]); // sort missing and ask for first to last // integrate into docsByDay } }, gotoDate(date, scroll) { // show this.$router.replace({ hash: date }) console.log('hash', this.$route.hash); // assure this.assureDate(DateTime.fromISO(date)); // scroll }, }, mounted() { window.addEventListener('scroll', this.debounce(this.setCurrentDay, 300)); }, }; </script> <style> .max-width { max-width: 25rem; } </style>