add changes for calendar flicker on event reload, and calendar scroll mods

This commit is contained in:
Ivymaster
2026-06-22 15:06:53 +02:00
parent f5cd0f1a5d
commit 4edec4df00
6 changed files with 238 additions and 56 deletions
+5 -1
View File
@@ -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' => [],
+66
View File
@@ -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);
}
}
@@ -71,7 +71,7 @@ export default {
></line-background>
<line-event
v-for="(event, i) in eventsWithRowInfo"
:key="i"
:key="event.orig.eindeutige_gruppen_id || i"
:style="'grid-' + axisRow + ': ' + event.rows.join('/')"
:event="event"
@resize-start="$emit('resize-start', $event)"
@@ -110,6 +110,7 @@ export default {
style="z-index: 2"
:draggable="draggable"
:data-id="'event-' + event.orig.kalender_id"
:data-group-id="'event-group-' + event.orig.eindeutige_gruppen_id"
ref="eventEl"
@dragstart="onDragStart"
v-draggable:move.noimage="draggable ? dragKalenderCollection : {}"
+136 -44
View File
@@ -163,6 +163,7 @@ export default {
assignedResources: [],
areFormButtonsDisplayed: false,
},
currentlyUpdatedEvent: null,
};
},
computed: {
@@ -399,25 +400,99 @@ export default {
if (hasStg) filter.stg = this.stg;
if (hasLektoren) filter.uid = this.lecturers.map((l) => 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();
+29 -10
View File
@@ -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();