Newer
Older
i18n-key="alsijil.coursebook"
:gql-query="gqlQuery"
:gql-additional-query-args="gqlQueryArgs"
:enable-create="false"
:enable-edit="false"
<template #additionalActions="{ attrs, on }">
<coursebook-filters v-model="filters" />
v-for="{ date, docs, first, last } in groupDocsByDay(items)"
handler: intersectHandler(date, first, last),
rootMargin: '-' + topMargin + 'px 0px 0px 0px',
:focus-on-mount="initDate && (initDate.toMillis() === date.toMillis())"
:value="currentDate"
@input="gotoDate"
@prev="gotoPrev"
@next="gotoNext"
/>

Julian
committed
</template>
<template #loading>
:number-of-days="10"
:number-of-docs="5"
/>

Julian
committed
</template>
<template #no-data>
<CoursebookEmptyMessage icon="mdi-book-off-outline">
{{ $t("alsijil.coursebook.no_data") }}
</CoursebookEmptyMessage>

Julian
committed
</template>
<template #no-results>
<CoursebookEmptyMessage icon="mdi-book-alert-outline">
{{
$t("alsijil.coursebook.no_results", { search: $refs.iterator.search })
}}
</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 { 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: {
filterType: {
type: String,
required: true,
},
objId: {
objType: {
type: String,
required: false,
},
/**
* Margin from coursebook list to top of viewport in pixels
*/
topMargin: {
type: Number,
required: false,
default: 165,
},
gqlQuery: documentationsForCoursebook,
dateStart: "",
dateEnd: "",
// Placeholder values while query isn't completed yet
groups: [],
courses: [],
currentDate: "",
hashUpdater: false,
// Assertion: Should only fire on page load or selection change.
// Resets date range.
own: this.filterType === "all" ? false : true,
objId: this.objId ? Number(this.objId) : undefined,
objType: this.objType?.toUpperCase(),
dateStart: this.dateStart,
dateEnd: this.dateEnd,
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")
) {
params: {
filterType: selectedFilters.filterType
? selectedFilters.filterType
: this.filterType,
objType: selectedFilters.objType,
objId: selectedFilters.objId,
},
// computed should not have side effects
// but this was actually done before filters was refactored into
// its own component
this.resetDate();
// Assure current date
console.log('Resetting date range', this.$route.hash);
this.currentDate = toDate || this.$route.hash?.substring(1);
if (!this.currentDate) {
this.setDate(DateTime.now().toISODate());
const date = DateTime.fromISO(this.currentDate);
this.dateStart = date.minus({ days: 3 }).toISODate();
this.dateEnd = date.plus({ days: 4 }).toISODate();
groupDocsByDay(docs) {
const docsByDay = docs.reduce((byDay, doc) => {
// This works with dummy. Does actual doc have dateStart instead?
const day = DateTime.fromISO(doc.datetimeStart).startOf("day");
byDay[day] ??= {date: day, docs: []};
byDay[day].docs.push(doc);
// => [{date: dt, docs: doc ..., idx: idx, lastIdx: last-idx} ...]
// sorting is necessary since backend can send docs unordered
const day = docsByDay[key];
day.first = idx === 0;
const lastIdx = length - 1;
day.last = idx === lastIdx;
// docsByDay: {dt: [dt doc ...] ...}
fetchMore(from, to, then) {
console.log('fetching', from, to);
this.lastQuery.fetchMore({
variables: {
dateStart: from,
dateEnd: to,
},
// Transform the previous result with new data
updateQuery: (previousResult, { fetchMoreResult }) => {
console.log('previousResult', previousResult);
console.log('fetchMoreResult', fetchMoreResult);
return { items: previousResult.items.concat(fetchMoreResult.items) };
}
this.currentDate = date;
if (!this.hashUpdater) {
this.hashUpdater = window.requestIdleCallback(() => {
if (!(this.$route.hash.substring(1) === this.currentDate)) {
this.$router.replace({ hash: this.currentDate })
}
this.hashUpdater = false;
});
fixScrollPos(height, top) {
console.log('fix @', top, document.documentElement.scrollTop, height, document.documentElement.scrollHeight);
this.$nextTick(() => {
console.log('fix @', top, document.documentElement.scrollTop, height, document.documentElement.scrollHeight);
if (height < document.documentElement.scrollHeight) {
console.log('fixingTop');
document.documentElement.scrollTop = document.documentElement.scrollHeight - height + top;
this.ready = true;
} else {
// Update top, could have changed in the meantime.
this.fixScrollPos(height, document.documentElement.scrollTop);
}
});
},
intersectHandler(date, first, last) {
let once = true;
return (entries) => {
const entry = entries[0];
if (entry.isIntersecting) {
if ((entry.boundingClientRect.top <= this.topMargin) || first) {
console.log('@', date.toISODate());
this.setDate(date.toISODate());
}
if (once && this.ready && first) {
console.log('load up', date.toISODate());
this.ready = false;
this.fetchMore(date.minus({ days: 5 }).toISODate(),
date.minus({ days: 1 }).toISODate(),
() => {
this.fixScrollPos(document.documentElement.scrollHeight,
document.documentElement.scrollTop);
});
once = false;
} else if (once && this.ready && last) {
console.log('load down', date.toISODate());
this.ready = false;
this.fetchMore(date.plus({ days: 1 }).toISODate(),
date.plus({ days: 5 }).toISODate(),
() => { this.ready = true });
once = false;
}
}
};
},
// Improve me?
// The navigation logic could be a bit simpler if the current days
// where known as a sorted array (= result of groupDocsByDay) But
// then the list would need its own component and this gets rather
// complicated. Then the calendar could also show the present days
// / gray out the missing.
//
// Next two: arg date is ts object
findPrev(date) {
return this.$refs.days
.map((day) => day.date)
.sort()
.reverse()
.find((date2) => date2 < date);
},
findNext(date) {
return this.$refs.days
.map((day) => day.date)
.sort()
.find((date2) => date2 > date);
},
gotoDate(date) {
const present = this.$refs.days
.find((day) => day.date.toISODate() === date);
if (present) {
// React immediatly -> smoother navigation
// Also intersect handler does not always react to scrollIntoView
this.setDate(date);
} else if (!this.findPrev(DateTime.fromISO(date)) || !this.findNext(DateTime.fromISO(date))) {
console.log('resetting from cal to ', date);
this.resetDate(date);
const prev = this.findPrev(DateTime.fromISO(this.currentDate));
if (prev) {
this.gotoDate(prev.toISODate());
}
const next = this.findNext(DateTime.fromISO(this.currentDate));
if (next) {
this.gotoDate(next.toISODate());
}
created() {
this.resetDate();
},
<style>
.max-width {
max-width: 25rem;
}
</style>