diff --git a/application/libraries/KalenderLib.php b/application/libraries/KalenderLib.php
index 935dfebb6..2271b97e6 100644
--- a/application/libraries/KalenderLib.php
+++ b/application/libraries/KalenderLib.php
@@ -44,6 +44,7 @@ class KalenderLib
FROM lehre.tbl_kalender
WHERE von >= timestamp %s
AND bis < timestamp %s
+ limit 4000
)',
$this->_ci->KalenderModel->db->escape(date('Y-m-d H:i:s', strtotime($start_date))),
$this->_ci->KalenderModel->db->escape(date('Y-m-d H:i:s', strtotime($end_date)))
diff --git a/public/js/components/Calendar/Base.js b/public/js/components/Calendar/Base.js
index 7e634ada1..6772028ec 100644
--- a/public/js/components/Calendar/Base.js
+++ b/public/js/components/Calendar/Base.js
@@ -22,6 +22,7 @@ export default {
},
provide() {
return {
+ calendarVisibleDatesChanged: dates => this.$emit('update:visible-dates', dates),
events: Vue.computed(() => this.convertedEvents),
backgrounds: Vue.computed(() => this.convertedBackgrounds),
dropAllowed: Vue.computed(() => !!this.onDrop),
@@ -138,6 +139,7 @@ export default {
"update:date",
"update:mode",
"update:range",
+ "update:visible-dates",
"update:date-range",
"drop"
],
@@ -354,4 +356,4 @@ export default {
`
-}
\ No newline at end of file
+}
diff --git a/public/js/components/Calendar/Base/Grid/Line.js b/public/js/components/Calendar/Base/Grid/Line.js
index be23c856e..c2d77b6d0 100644
--- a/public/js/components/Calendar/Base/Grid/Line.js
+++ b/public/js/components/Calendar/Base/Grid/Line.js
@@ -46,6 +46,16 @@ export default {
},
},
computed: {
+ visibleStart() {
+ return this.date.startOf('day');
+ },
+ visibleEnd() {
+ const end = this.end.startOf('day');
+
+ return end > this.visibleStart
+ ? end
+ : this.visibleStart.plus({ days: 1 });
+ },
formattedEvents() {
let formattedEvents = this.events.map((event) => {
event.rows = [1, -1];
@@ -133,6 +143,8 @@ export default {
template: /* html */ `
{
+ entries.forEach(entry => this.preserveGridLineHeight(entry.target));
+ }));
+ }
+
+ this.intersectionObserver = Vue.markRaw(new IntersectionObserver(entries => {
+ let changed = false;
+
+ entries.forEach(entry => {
+ if (entry.isIntersecting && entry.intersectionRatio > 0) {
+ if (!this.visibleGridLines.has(entry.target)) {
+ this.visibleGridLines.add(entry.target);
+ changed = true;
+ }
+ } else if (this.visibleGridLines.delete(entry.target)) {
+ changed = true;
+ }
+ });
+
+ if (changed)
+ this.scheduleVisibleDatesUpdate();
+ }, {
+ root: this.$el,
+ threshold: 0
+ }));
+
+ this.mutationObserver = Vue.markRaw(new MutationObserver(mutations => {
+ let changed = false;
+
+ mutations.forEach(mutation => {
+ if (mutation.type === 'attributes') {
+ this.applyPreservedGridLineHeight(mutation.target);
+ this.gridLineResizeObserver?.observe(mutation.target);
+ if (this.visibleGridLines.has(mutation.target))
+ changed = true;
+ return;
+ }
+
+ mutation.addedNodes.forEach(node => this.observeGridLines(node));
+ mutation.removedNodes.forEach(node => {
+ this.getGridLines(node).forEach(line => {
+ this.intersectionObserver.unobserve(line);
+ this.gridLineResizeObserver?.unobserve(line);
+ if (this.visibleGridLines.delete(line))
+ changed = true;
+ });
+ });
+ });
+
+ if (changed)
+ this.scheduleVisibleDatesUpdate();
+ }));
+
+ this.observeGridLines(this.$el);
+ this.mutationObserver.observe(this.$el, {
+ subtree: true,
+ childList: true,
+ attributes: true,
+ attributeFilter: ['data-visible-start', 'data-visible-end']
+ });
+ },
+ getGridLines(node) {
+ if (!(node instanceof Element))
+ return [];
+
+ const lines = node.matches('.fhc-calendar-base-grid-line') ? [node] : [];
+ return lines.concat([...node.querySelectorAll('.fhc-calendar-base-grid-line')]);
+ },
+ observeGridLines(node) {
+ this.getGridLines(node).forEach(line => {
+ this.applyPreservedGridLineHeight(line);
+ this.intersectionObserver.observe(line);
+ this.gridLineResizeObserver?.observe(line);
+ });
+ },
+ getGridLineKey(line) {
+ const start = line.dataset.visibleStart;
+ const end = line.dataset.visibleEnd;
+
+ return start && end ? start + '/' + end : null;
+ },
+ applyPreservedGridLineHeight(line) {
+ if (!this.gridLineResizeObserver)
+ return;
+
+ const key = this.getGridLineKey(line);
+ const height = key ? this.gridLineHeights.get(key) : null;
+ line.style.minHeight = height ? height + 'px' : '';
+ },
+ preserveGridLineHeight(line) {
+ const key = this.getGridLineKey(line);
+ if (!key)
+ return;
+
+ const height = Math.ceil(line.getBoundingClientRect().height);
+ const preservedHeight = this.gridLineHeights.get(key) || 0;
+ if (height > preservedHeight)
+ this.gridLineHeights.set(key, height);
+
+ const minHeight = Math.max(height, preservedHeight);
+ if (minHeight && line.style.minHeight !== minHeight + 'px')
+ line.style.minHeight = minHeight + 'px';
+ },
+ scheduleVisibleDatesUpdate() {
+ if (this.visibilityUpdateScheduled)
+ return;
+
+ this.visibilityUpdateScheduled = true;
+ this.$nextTick(() => {
+ this.visibilityUpdateScheduled = false;
+ this.emitVisibleDates();
+ });
+ },
+ emitVisibleDates() {
+ const datesByKey = new Map();
+
+ this.visibleGridLines.forEach(line => {
+ if (!line.isConnected)
+ return;
+
+ const start = line.dataset.visibleStart;
+ const end = line.dataset.visibleEnd;
+ if (start && end)
+ datesByKey.set(start + '/' + end, { start, end });
+ });
+
+ const dates = [...datesByKey.values()]
+ .sort((a, b) => a.start.localeCompare(b.start));
+ if (!dates.length)
+ return;
+
+ const key = dates.map(date => date.start + '/' + date.end).join('|');
+
+ if (key === this.lastVisibleDatesKey)
+ return;
+
+ this.lastVisibleDatesKey = key;
+ this.onVisibleDatesChanged(dates);
+ },
+ stopVisibilityTracking() {
+ this.intersectionObserver?.disconnect();
+ this.mutationObserver?.disconnect();
+ this.gridLineResizeObserver?.disconnect();
+ this.intersectionObserver = null;
+ this.mutationObserver = null;
+ this.gridLineResizeObserver = null;
+ this.visibleGridLines.clear();
+ this.gridLineHeights.clear();
+ this.onVisibleDatesChanged?.(null);
+ },
prevPage() {
return this.slidePages(-1);
},
@@ -93,6 +263,12 @@ export default {
this.extrasAfter = this.extrasBefore = 0;
}
},
+ mounted() {
+ this.startVisibilityTracking();
+ },
+ beforeUnmount() {
+ this.stopVisibilityTracking();
+ },
template: /* html */`
luxon.Interval.fromDateTimes(
+ luxon.DateTime.fromISO(date.start).setZone(this.timezone).minus({ days: 1 }),
+ luxon.DateTime.fromISO(date.end).setZone(this.timezone).plus({ days: 1 })
+ ))
+ .filter(interval => interval.isValid);
+
+ list = list.filter(event => {
+ const eventStart = luxon.DateTime.fromISO(event.isostart).setZone(this.timezone);
+ const eventEnd = luxon.DateTime.fromISO(event.isoend).setZone(this.timezone);
+
+ // Do not hide an event with an invalid date; it is safer to keep it
+ // available for the renderer than to silently discard it.
+ if (!eventStart.isValid || !eventEnd.isValid)
+ return true;
+
+ return visibleIntervals.some(interval =>
+ eventStart < interval.end && eventEnd > interval.start
+ );
+ });
+ }
+
if (Array.isArray(this.visibleLecturers))
{
const visibleLectures = new Set(this.visibleLecturers);
@@ -190,6 +219,18 @@ export default {
return;
this.rangeInterval = rangeInterval;
+ if (
+ this.visibleDates === null
+ && rangeInterval instanceof luxon.Interval
+ && rangeInterval.isValid
+ )
+ {
+ const start = rangeInterval.start.startOf('day');
+ this.visibleDates = [{
+ start: start.toISO(),
+ end: start.plus({ days: 7 }).toISO()
+ }];
+ }
this.$emit('update:range', rangeInterval);
},
handleDateRange({ start, end }) {
@@ -299,6 +340,7 @@ export default {
@update:date="handleDateUpdate"
@update:mode="(newMode, newDate) => { currentMode = newMode; $emit('update:mode', newMode, newDate) }"
@update:range="updateRange"
+ @update:visible-dates="visibleDates = $event"
@update:date-range="handleDateRange"
>