From 4edec4df009f8ecb62805c100a91d8e84ff8db1b Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Mon, 22 Jun 2026 15:06:53 +0200 Subject: [PATCH] add changes for calendar flicker on event reload, and calendar scroll mods --- application/libraries/KalenderLib.php | 6 +- public/css/Tempus.css | 66 +++++++ .../js/components/Calendar/Base/Grid/Line.js | 2 +- .../Calendar/Base/Grid/Line/Event.js | 1 + public/js/components/Tempus/Tempus.js | 180 +++++++++++++----- public/js/composables/EventLoader.js | 39 +++- 6 files changed, 238 insertions(+), 56 deletions(-) diff --git a/application/libraries/KalenderLib.php b/application/libraries/KalenderLib.php index 108447c37..b0b3ad0ad 100644 --- a/application/libraries/KalenderLib.php +++ b/application/libraries/KalenderLib.php @@ -32,6 +32,7 @@ class KalenderLib $end_date = date('Y-m-d', strtotime($end_date . ' +1 day')); $this->_ci->KalenderModel->addSelect('tbl_kalender.kalender_id, + tbl_kalender.eindeutige_gruppen_id, tbl_kalender.status_kurzbz, tbl_kalender.typ, tbl_kalender.von, @@ -130,6 +131,8 @@ class KalenderLib $this->_ci->KalenderModel->db->where('tbl_kalender.von >=', $start_date); $this->_ci->KalenderModel->db->where('tbl_kalender.bis <', $end_date); + + $this->_ci->KalenderModel->addOrder('tbl_kalender.eindeutige_gruppen_id', 'DESC'); } private function _mapEvents($data, $collisionCheck = true) @@ -151,6 +154,8 @@ class KalenderLib $bis = new DateTime($row->bis); $events[$id] = (object) [ + 'kalender_id' => $id, + 'eindeutige_gruppen_id' => $row->eindeutige_gruppen_id, 'type' => $row->typ, 'beginn' => $von->format('H:i:s'), 'ende' => $bis->format('H:i:s'), @@ -166,7 +171,6 @@ class KalenderLib 'farbe' => isset($row->farbe) ? $row->farbe : '', 'lehrveranstaltung_id' => $row->lehrveranstaltung_id, 'organisationseinheit' => isset($row->oe_kurzbz) ? $row->oe_kurzbz : '', - 'kalender_id' => $id, 'lehreinheit_id' => [], 'lektor' => [], 'teilnehmer_gruppe' => [], diff --git a/public/css/Tempus.css b/public/css/Tempus.css index 3e32789e1..72384c9c7 100644 --- a/public/css/Tempus.css +++ b/public/css/Tempus.css @@ -185,3 +185,69 @@ body { } +.updated-event { + position: relative; + z-index: 10; + animation: modernFocus 1s ease-out; +} + +.updated-event-long { + position: relative; + z-index: 10; + animation: modernFocus 1.8s ease-out; +} + +@keyframes modernFocus { + 0% { + background-color: #cfd4d8; + box-shadow: + 0 0 0 0 rgba(120, 120, 120, 0.8), + 0 0 0 0 rgba(255, 255, 255, 0.6); + } + + 30% { + background-color: #eef1f3; + box-shadow: + 0 0 0 8px rgba(120, 120, 120, 0.25), + 0 0 30px 8px rgba(255, 255, 255, 0.7); + } + + 60% { + background-color: #f7f8f9; + box-shadow: + 0 0 0 14px rgba(120, 120, 120, 0.15), + 0 0 45px 12px rgba(255, 255, 255, 0.5); + } + + 100% { + background-color: inherit; + box-shadow: + 0 0 0 24px rgba(120, 120, 120, 0), + 0 0 60px 20px rgba(255, 255, 255, 0); + } +} + +.deemphasized-event { + animation: deEmphasize 1s ease-out; +} + +.deemphasized-event-long { + animation: deEmphasize 1.8s ease-out; +} + +@keyframes deEmphasize { + 0% { + opacity: 1; + filter: saturate(1) brightness(1); + } + + 30% { + opacity: 0.55; + filter: saturate(0.4) brightness(0.4); + } + + 100% { + opacity: 1; + filter: saturate(1) brightness(1); + } +} \ 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 ec248afaa..501faed6d 100644 --- a/public/js/components/Calendar/Base/Grid/Line.js +++ b/public/js/components/Calendar/Base/Grid/Line.js @@ -71,7 +71,7 @@ export default { > l.uid); + let response = null; if (this.previewRole === "lektor") - return [ + response = [ this.$api.call( ApiKalender.getPlanLecturer(start.toISODate(), end.toISODate()), ), ]; if (this.previewRole === "student") - return [ + response = [ this.$api.call( ApiKalender.getPlanStudent(start.toISODate(), end.toISODate()), ), ]; - return [ + response = [ this.$api.call( ApiKalender.getPlan(filter, start.toISODate(), end.toISODate()), ), ]; + + if (response) { + response[0].then((result) => { + if (!this.currentlyUpdatedEvent) return; + + document.querySelectorAll(".updated-event").forEach((el) => { + el.classList.remove("updated-event"); + }); + document.querySelectorAll(".updated-event-long").forEach((el) => { + el.classList.remove("updated-event-long"); + }); + document.querySelectorAll(".deemphasized-event").forEach((el) => { + el.classList.remove("deemphasized-event"); + }); + document + .querySelectorAll(".deemphasized-event-long") + .forEach((el) => { + el.classList.remove("deemphasized-event-long"); + }); + + setTimeout(() => { + const eventEl = document.querySelector( + `[data-group-id="event-group-${this.currentlyUpdatedEvent.eindeutige_gruppen_id}"]`, + ); + if (!eventEl) return; + + const calendar = document.querySelector(".fhc-calendar-base-grid"); + const eventRect = eventEl.getBoundingClientRect(); + + const offset = 300; + + const isInsideScrolledView = + eventEl.offsetLeft < calendar.scrollLeft + calendar.clientWidth && + eventEl.offsetLeft + eventEl.offsetWidth > calendar.scrollLeft && + eventEl.offsetTop < calendar.scrollTop + calendar.clientHeight && + eventEl.offsetTop + eventEl.offsetHeight > calendar.scrollTop; + + const rect = eventEl.getBoundingClientRect(); + if (!isInsideScrolledView) { + eventEl.scrollIntoView({ + behavior: "smooth", + inline: "center", + block: "nearest", + }); + } + + let timeout = 0; + let emphasizeUpdateClassName = isInsideScrolledView + ? "updated-event" + : "updated-event-long"; + let deemphasizedUpdateClassName = isInsideScrolledView + ? "deemphasized-event" + : "deemphasized-event-long"; + + if (!isInsideScrolledView) timeout = 500; + + setTimeout(() => { + eventEl.classList.add(emphasizeUpdateClassName); + document + .querySelectorAll(".fhc-calendar-base-grid-line-event") + .forEach((el) => { + if (el !== eventEl) { + el.classList.add(deemphasizedUpdateClassName); + } + }); + }, timeout); + + this.currentlyUpdatedEvent = null; + }, 100); + }); + } + + return response; }, toDateTime(value, timezone) { if (luxon.DateTime.isDateTime(value)) return value; @@ -502,7 +577,11 @@ export default { ApiKalender.updateKalenderEvent(obj.orig.kalender_id, updatedInfos), ) .then(() => { - if (onSuccess) onSuccess(); + if (onSuccess) { + onSuccess(); + this.$refs.calendar.$refs.calendar.$refs.mode.$refs.view.$refs.grid.disableAutoScroll(); + this.currentlyUpdatedEvent = obj.orig; + } }); }, @@ -720,37 +799,41 @@ export default { ); if (getAssignedResources.meta.status === "success") { return getAssignedResources.data - .filter((unit) => !!unit) - .map((unit) => { - return { - isNoteTextareaShown: unit.anmerkung && unit.anmerkung.trim() !== "", - ...unit, - }; - }).filter((unit) => !!unit) + .filter((unit) => !!unit) + .map((unit) => { + return { + isNoteTextareaShown: + unit.anmerkung && unit.anmerkung.trim() !== "", + ...unit, + }; + }) + .filter((unit) => !!unit); } else { - this.$fhcAlert.alertError(this.$p.t("ui", "failed_assigned_resources_fetch_error_message")); + this.$fhcAlert.alertError( + this.$p.t("ui", "failed_assigned_resources_fetch_error_message"), + ); } return []; }, async fetchSchedulableResourcesByCalender(calendarID) { - let getSchedulableResourcesByCalendar = - await this.$api.call( - ApiOperationalResourceToCalender.getSchedulableResourcesByCalendar(calendarID), - ); - if ( - getSchedulableResourcesByCalendar.meta.status === - "success" - ) { + let getSchedulableResourcesByCalendar = await this.$api.call( + ApiOperationalResourceToCalender.getSchedulableResourcesByCalendar( + calendarID, + ), + ); + if (getSchedulableResourcesByCalendar.meta.status === "success") { return getSchedulableResourcesByCalendar.data; } else { - this.$fhcAlert.alertError(this.$p.t("ui", "failed_schedulable_resources_fetch_error_message")); + this.$fhcAlert.alertError( + this.$p.t("ui", "failed_schedulable_resources_fetch_error_message"), + ); } return []; }, filterAvailableResources(event) { - this.resourcesAssignmentModal.filteredAvailableResources + this.resourcesAssignmentModal.filteredAvailableResources; const query = event.query.toLowerCase(); if (!query) { return (this.resourcesAssignmentModal.filteredAvailableResources = [ @@ -764,21 +847,23 @@ export default { return (this.resourcesAssignmentModal.filteredAvailableResources = this.dropdownParsedAvailableResources - .filter((unit) => { - return !this.resourcesAssignmentModal.assignedResources.some( - (assigned) => assigned.betriebsmittel_id === unit.value, - ); - }) - .filter((unit) => { - return unit.label.toLowerCase().includes(query); - })); + .filter((unit) => { + return !this.resourcesAssignmentModal.assignedResources.some( + (assigned) => assigned.betriebsmittel_id === unit.value, + ); + }) + .filter((unit) => { + return unit.label.toLowerCase().includes(query); + })); }, toggleAssignedResourceNoteInput(resource) { const index = this.resourcesAssignmentModal.assignedResources.findIndex( (assigned) => assigned.betriebsmittel_id === resource.betriebsmittel_id, ); if (index !== -1) { - this.resourcesAssignmentModal.assignedResources[index].isNoteTextareaShown = + this.resourcesAssignmentModal.assignedResources[ + index + ].isNoteTextareaShown = !this.resourcesAssignmentModal.assignedResources[index] .isNoteTextareaShown; } @@ -788,35 +873,42 @@ export default { removeAssignedResource(resource) { this.resourcesAssignmentModal.assignedResources = this.resourcesAssignmentModal.assignedResources.filter( - (assigned) => assigned.betriebsmittel_id !== resource.betriebsmittel_id, + (assigned) => + assigned.betriebsmittel_id !== resource.betriebsmittel_id, ); this.resourcesAssignmentModal.areFormButtonsDisplayed = true; }, async refreshResourcesAssignmentModalData(calenderItem) { this.resourcesAssignmentModal.availableResources = - await this.fetchSchedulableResourcesByCalender(calenderItem.kalender_id); + await this.fetchSchedulableResourcesByCalender( + calenderItem.kalender_id, + ); this.resourcesAssignmentModal.filteredAvailableResources = [ ...this.dropdownParsedAvailableResources, ]; - - this.resourcesAssignmentModal.assignedResources = await this.fetchAssignedResourcesByCalender(calenderItem.kalender_id); + + this.resourcesAssignmentModal.assignedResources = + await this.fetchAssignedResourcesByCalender(calenderItem.kalender_id); this.resourcesAssignmentModal.selectedAvailableResource = null; this.resourcesAssignmentModal.areFormButtonsDisplayed = false; }, async saveAssignedResourcesToCalendarItem(calenderItem, assignedResources) { - let getSchedulableResourcesByCalendar = - await this.$api.call( - ApiOperationalResourceToCalender.storeResourcesToCalendarRelationship(calenderItem.kalender_id, assignedResources) + let getSchedulableResourcesByCalendar = await this.$api.call( + ApiOperationalResourceToCalender.storeResourcesToCalendarRelationship( + calenderItem.kalender_id, + assignedResources, + ), + ); + if (getSchedulableResourcesByCalendar.meta.status === "success") { + this.$fhcAlert.alertSuccess( + this.$p.t("ui", "assigned_resources_save_success_message"), ); - if ( - getSchedulableResourcesByCalendar.meta.status === - "success" - ) { - this.$fhcAlert.alertSuccess(this.$p.t("ui", "assigned_resources_save_success_message")); await this.refreshResourcesAssignmentModalData(calenderItem); } else { - this.$fhcAlert.alertError(this.$p.t("ui", "failed_assigned_resources_save_error_message")); + this.$fhcAlert.alertError( + this.$p.t("ui", "failed_assigned_resources_save_error_message"), + ); } this.$refs.calendar.resetEventLoader(); diff --git a/public/js/composables/EventLoader.js b/public/js/composables/EventLoader.js index a61295dd4..9be7c55c6 100644 --- a/public/js/composables/EventLoader.js +++ b/public/js/composables/EventLoader.js @@ -1,7 +1,12 @@ // TODO(chris): load events that are longer than the interval without doubling it -export function useEventLoader(rangeInterval, getPromiseFunc) { +export function useEventLoader(rangeInterval, getPromiseFunc, isLoaderVisible = true, isLoaderInitiallyVisible = true) { + let hasFirstLoadOccurred = false; let loading_id = 0; + + let tempEventsHolder = []; + let rangeIntervalHolder = null; + const events = Vue.ref([]); const loadingEvents = Vue.ref([]); const allEvents = Vue.computed(() => events.value.concat(loadingEvents.value)); @@ -100,13 +105,15 @@ export function useEventLoader(rangeInterval, getPromiseFunc) { if (start.ts >= end.ts) return result; - - loadingEvents.value.push({ - loading_id: loading_id++, - type: "loading", - isostart: start.toISODate() + 'T' + start.toISOTime(), - isoend: end.toISODate() + 'T' + end.toISOTime() - }); + + if (isLoaderVisible && (!hasFirstLoadOccurred && isLoaderInitiallyVisible)) { + loadingEvents.value.push({ + loading_id: loading_id++, + type: "loading", + isostart: start.toISODate() + 'T' + start.toISOTime(), + isoend: end.toISODate() + 'T' + end.toISOTime() + }); + } return mergePromiseArr(getPromiseFunc(start, end), result); }; @@ -115,7 +122,14 @@ export function useEventLoader(rangeInterval, getPromiseFunc) { const range = Vue.toValue(rangeInterval); if (!(range instanceof luxon.Interval)) return; + + if (!rangeIntervalHolder || !range.equals(rangeIntervalHolder)) { + hasFirstLoadOccurred = false; + rangeIntervalHolder = range; + } + const promises = markEventsLoaded(range.start, range.end); + Promise .allSettled(promises) .then(results => { @@ -127,18 +141,23 @@ export function useEventLoader(rangeInterval, getPromiseFunc) { if (res.value.meta.lv) lv.value = res.value.meta.lv; - events.value = events.value.concat(res.value.data); + tempEventsHolder = tempEventsHolder.concat(res.value.data); loadingEvents.value = []; } }) + + events.value = tempEventsHolder; + }); + + hasFirstLoadOccurred = true; }; Vue.watchEffect(reload); const reset = () => { loading_id = 0; - events.value = []; + tempEventsHolder = []; loadingEvents.value = []; eventsLoaded.splice(0, eventsLoaded.length); reload();