From 7be64fe3cddedfa94cfc2b9cac30c645f46d4bae Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Mon, 14 Sep 2026 13:54:11 +0200 Subject: [PATCH] WIP --- application/libraries/KalenderLib.php | 1 + public/js/components/Calendar/Base.js | 4 +- .../js/components/Calendar/Base/Grid/Line.js | 12 ++ public/js/components/Calendar/Base/Slider.js | 178 +++++++++++++++++- public/js/components/Calendar/Tempus.js | 42 +++++ 5 files changed, 235 insertions(+), 2 deletions(-) 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" >