This commit is contained in:
Ivymaster
2026-09-14 13:54:11 +02:00
parent c4953c964d
commit 7be64fe3cd
5 changed files with 235 additions and 2 deletions
+1
View File
@@ -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)))
+3 -1
View File
@@ -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 {
</bs-modal>
</div>
`
}
}
@@ -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 */ `
<div
class="fhc-calendar-base-grid-line"
:data-visible-start="visibleStart.toISO()"
:data-visible-end="visibleEnd.toISO()"
:class="{
'fhc-calendar-base-grid-line-sunday': $props.date.weekday === 7
}"
+177 -1
View File
@@ -4,6 +4,10 @@ export default {
time: {
from: "sliderTime",
default: ".3s"
},
onVisibleDatesChanged: {
from: "calendarVisibleDatesChanged",
default: null
}
},
emits: [
@@ -15,7 +19,14 @@ export default {
extrasAfter: 0,
extrasBefore: 0,
running: false,
promiseResolve: null
promiseResolve: null,
intersectionObserver: null,
mutationObserver: null,
gridLineResizeObserver: null,
visibleGridLines: Vue.markRaw(new Set()),
gridLineHeights: Vue.markRaw(new Map()),
visibilityUpdateScheduled: false,
lastVisibleDatesKey: null
}
},
computed: {
@@ -61,6 +72,165 @@ export default {
}
},
methods: {
startVisibilityTracking() {
if (!this.onVisibleDatesChanged || typeof IntersectionObserver === 'undefined')
return;
if (
this.$el.closest('.fhc-calendar-mode-range')
&& typeof ResizeObserver !== 'undefined'
)
{
this.gridLineResizeObserver = Vue.markRaw(new ResizeObserver(entries => {
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 */`
<div
class="fhc-calendar-base-slider h-100"
+42
View File
@@ -103,6 +103,7 @@ export default {
],
data() {
return {
visibleDates: null,
modes: {
week: Vue.markRaw(ModeWeek),
month: Vue.markRaw(ModeMonth),
@@ -162,6 +163,34 @@ export default {
{
let list = this.events;
// Start with the first week and then keep events overlapping a grid line
// that is visible inside the slider viewport, including a one-day buffer
// before and after the visible dates.
console.log('visibleDates', this.visibleDates);
if (this.currentMode === 'range' && Array.isArray(this.visibleDates))
{
const visibleIntervals = this.visibleDates
.map(date => 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"
>
<template v-slot="{ event, mode }">