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),
options: {
rootMargin: '-' + topMargin + 'px 0px 0px 0px',
threshold: [0, 1],
},
}"
:focus-on-mount="initDate && initDate.toMillis() === date.toMillis()"
:value="currentDate"
@input="gotoDate"
@prev="gotoPrev"
@next="gotoNext"

Julian
committed
</template>
<template #loading>

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,
},
/**
* Number of consecutive to load at once
* This number of days is initially loaded and loaded
* incrementally while scrolling.
*/
dayIncrement: {
type: Number,
required: false,
default: 7,
},
/**
* 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();
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: this.dayIncrement }).toISODate();
this.dateEnd = date.plus({ days: this.dayIncrement }).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");
// => [{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) {
this.lastQuery.fetchMore({
variables: {
dateStart: from,
dateEnd: to,
},
// Transform the previous result with new data
updateQuery: (previousResult, { 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.hashUpdater = false;
});
fixScrollPos(height, top) {
this.$nextTick(() => {
if (height < document.documentElement.scrollHeight) {
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) {
this.ready = false;
this.fetchMore(
date.minus({ days: this.dayIncrement }).toISODate(),
date.minus({ days: 1 }).toISODate(),
() => {
this.fixScrollPos(
document.documentElement.scrollHeight,
document.documentElement.scrollTop,
);
},
);
once = false;
} else if (once && this.ready && last) {
this.ready = false;
this.fetchMore(
date.plus({ days: 1 }).toISODate(),
date.plus({ days: this.dayIncrement }).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);
},
const present = this.$refs.days.find(
(day) => day.date.toISODate() === date,
);
// 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))
) {
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>