Compare commits

...
7 changed files with 518 additions and 215 deletions
+1 -1
View File
@@ -243,6 +243,7 @@ class KalenderLib
$parsedResources = is_string($resources) ? json_decode($resources, true) : $resources;
$events[$id] = (object) [
'kalender_id' => $id,
'eindeutige_kalender_gruppen_id' => $row->eindeutige_kalender_gruppen_id,
'type' => $row->typ,
'beginn' => $von->format('H:i:s'),
@@ -261,7 +262,6 @@ class KalenderLib
'lehrveranstaltung_id' => $row->lehrveranstaltung_id,
'verplante_stunden' => $row->verplante_stunden,
'organisationseinheit' => isset($row->oe_kurzbz) ? $row->oe_kurzbz : '',
'kalender_id' => $id,
'lehreinheit_id' => [],
'lektor' => [],
'teilnehmer_gruppe' => [],
+98 -29
View File
@@ -6,19 +6,19 @@
@import './components/Notiz.css';
html {
font-size: .875em;
height: 100%;
font-size: 0.875em;
height: 100%;
}
body {
/*display: flex;*/
height: 100%;
height: 100%;
}
.heightfull {
height: 95%;
height: 95%;
}
.navbar-dark .navbar-brand:focus {
box-shadow: 0 0 0 .25rem rgba(13,110,253,.25);
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
z-index: 3;
}
#main {
@@ -28,13 +28,13 @@ body {
height: 100%;
}
.searchbar {
margin-right: 0!important;
margin-right: 0 !important;
}
.searchbar > .input-group {
margin-right: 0!important;
margin-right: 0 !important;
}
.searchbar > .input-group > * {
border-radius: 0!important;
border-radius: 0 !important;
}
#sidebarMenu {
@@ -55,7 +55,7 @@ body {
@media (min-width: 768px) {
#sidebarMenu {
visibility: visible!important;
visibility: visible !important;
transform: none;
position: inherit;
z-index: 1;
@@ -64,27 +64,24 @@ body {
.toast.toast-success {
color: #0f5132;
background-color: #d1e7dd!important;
border-color: #badbcc!important;
background-color: #d1e7dd !important;
border-color: #badbcc !important;
}
.toast.toast-danger {
color: #842029;
background-color: #f8d7da!important;
border-color: #f5c2c7!important;
background-color: #f8d7da !important;
border-color: #f5c2c7 !important;
}
.has-filter .fa-filter {
color: var(--bs-success);
}
#parkingslot {
border: 1px dashed;
min-height: 5vh;
max-height: 15vh;
color: #AAAAAA;
color: #aaaaaa;
text-align: center;
font-size: large;
margin-top: 1.25rem;
@@ -96,7 +93,7 @@ body {
}
.course-picker {
border: 1px dashed #AAAAAA;
border: 1px dashed #aaaaaa;
display: flex;
flex-direction: column;
flex: 1 1 0;
@@ -132,11 +129,11 @@ body {
}
.eckerltest {
box-shadow: 3px 3px 3px #ccc;
box-shadow: 3px 3px 3px #ccc;
}
.verband-selection {
border: 1px dashed #AAAAAA;
border: 1px dashed #aaaaaa;
margin-bottom: 0.5rem;
max-height: 450px;
overflow-y: auto;
@@ -145,7 +142,7 @@ body {
}
.lecture-selection {
border: 1px dashed #AAAAAA;
border: 1px dashed #aaaaaa;
margin-bottom: 0.5rem;
max-height: 20vh;
overflow-y: auto;
@@ -154,7 +151,7 @@ body {
}
.room-selection {
border: 1px dashed #AAAAAA;
border: 1px dashed #aaaaaa;
margin-bottom: 0.5rem;
padding: 0.5rem 0 0.5rem 0.5rem;
}
@@ -169,10 +166,8 @@ body {
transform: scale(1.1);
}
.bg-lecturer-wish {
opacity: .15;
opacity: 0.15;
}
.bg-lecturer-block {
background: rgba(255, 0, 0, 0.15);
@@ -195,18 +190,21 @@ body {
.wish-w--2 {
background-color: #FF2200;
background-color: #ff2200;
}
.wish-w--1 {
background-color: #FF9922;
background-color: #ff9922;
}
.wish-w-1 {
background-color: #CCFFCC;
background-color: #ccffcc;
}
.wish-w-2 {
background-color: #48FA66;
background-color: #48fa66;
}
.fhc-calendar-base-grid-line-event {
position: relative !important;
}
.room-option {
display: grid;
@@ -214,3 +212,74 @@ body {
align-items: center;
width: 100%;
}
.updated-event {
position: relative !important;
z-index: 10;
animation: modernFocus 1s ease-out;
border-radius: 0.25rem;
}
.updated-event-long {
position: relative !important;
z-index: 10;
animation: modernFocus 1.8s ease-out;
border-radius: 0.25rem;
}
@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);
}
}
.spinner-overlay {
position: absolute;
inset: 0; /* top:0 right:0 bottom:0 left:0 */
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
background: rgba(255, 255, 255, 1);
opacity: 0.4;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #ddd;
border-top: 4px solid #007bff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
+17 -19
View File
@@ -1,5 +1,5 @@
import LineEvent from "./Line/Event.js";
import LineBackground from "./Line/Background.js";
import LineEvent from './Line/Event.js';
import LineBackground from './Line/Background.js';
/**
* TODO(chris):
@@ -7,19 +7,19 @@ import LineBackground from "./Line/Background.js";
*/
export default {
name: "GridLine",
name: 'GridLine',
components: {
LineEvent,
LineBackground,
},
inject: {
axisRow: "axisRow",
axisRow: 'axisRow',
shouldCompactEvents: {
from: "shouldCompactEvents",
from: 'shouldCompactEvents',
default: undefined,
},
compactibleEventTypes: {
from: "compactibleEventTypes",
from: 'compactibleEventTypes',
default: undefined,
},
},
@@ -50,19 +50,20 @@ export default {
let formattedEvents = this.events.map((event) => {
event.rows = [1, -1];
if (event.startsHere) {
event.rows[0] =
"t_" + event.start.diff(this.date).toMillis();
event.rows[0] = 't_' + event.start.diff(this.date).toMillis();
}
if (event.endsHere) {
event.rows[1] = "t_" + event.end.diff(this.date).toMillis();
event.rows[1] = 't_' + event.end.diff(this.date).toMillis();
}
return event;
});
if (this.shouldCompactEvents && this.compactibleEventTypes?.length) {
formattedEvents =
this.compactEvents(formattedEvents, this.compactibleEventTypes);
formattedEvents = this.compactEvents(
formattedEvents,
this.compactibleEventTypes,
);
}
return formattedEvents;
@@ -71,12 +72,9 @@ export default {
methods: {
compactEvents(events, compactibleEventTypes) {
let formattedEvents = events
.filter(
(event) =>
!compactibleEventTypes.includes(event.type),
)
.filter((event) => !compactibleEventTypes.includes(event.type))
.map((event) => {
event.display = "default";
event.display = 'default';
return event;
});
let eventsToBeCompacted = events.filter((event) =>
@@ -110,17 +108,17 @@ export default {
compactedEvents.forEach((compactedEvent) => {
if (compactedEvent.events.length < 4) {
formattedEvents.push({
display: "compacted",
display: 'compacted',
...compactedEvent,
});
} else {
formattedEvents.push({
display: "compacted",
display: 'compacted',
events: compactedEvent.events.slice(0, 3),
rows: compactedEvent.rows,
});
formattedEvents.push({
display: "compactedExtra",
display: 'compactedExtra',
events: compactedEvent.events.slice(3),
rows: compactedEvent.rows,
});
@@ -3,72 +3,74 @@ import drop from '../../../../../directives/drop.js';
import CalClick from '../../../../../directives/Calendar/Click.js';
export default {
name: "GridLineEvent",
name: 'GridLineEvent',
directives: {
draggable,
drop,
CalClick
CalClick,
},
emits: [
'resize-start'
],
emits: ['resize-start'],
data() {
return {
contextMenu: {
show: false,
x: 0,
y: 0
}
y: 0,
},
};
},
inject: {
draggableEvents: "draggableEvents",
draggableEvents: 'draggableEvents',
resizableEvents: {
from: "resizableEvents",
default: () => () => false
from: 'resizableEvents',
default: () => () => false,
},
mode: "mode",
mode: 'mode',
contextMenuActions: {
from: "contextMenuActions",
default: () => ({})
from: 'contextMenuActions',
default: () => ({}),
},
onDrop: {
from: "onDrop",
default: () => null
from: 'onDrop',
default: () => null,
},
onDropEvent: {
from: "onDropEvent",
default: () => () => {}
}
from: 'onDropEvent',
default: () => () => {},
},
},
props: {
event: {
type: Object,
required: true,
validator(value) {
return (value.start && value.end && value.orig);
}
}
return value.start && value.end && value.orig;
},
},
},
computed: {
isHeaderOrFooter() {
return ['header', 'footer'].includes(this.event.orig);
},
draggable() {
return !this.isHeaderOrFooter && this.draggableEvents(this.event.orig, this.mode);
return (
!this.isHeaderOrFooter &&
this.draggableEvents(this.event.orig, this.mode)
);
},
resizable() {
return !this.isHeaderOrFooter && this.resizableEvents(this.event.orig, this.mode);
return (
!this.isHeaderOrFooter &&
this.resizableEvents(this.event.orig, this.mode)
);
},
classes() {
const classes = [];
if (this.isHeaderOrFooter) {
classes.push('event-' + this.event.orig);
} else {
if (this.event.startsHere)
classes.push('event-begin');
if (this.event.endsHere)
classes.push('event-end');
if (this.event.startsHere) classes.push('event-begin');
if (this.event.endsHere) classes.push('event-end');
}
classes.push(`calender_id-${this.event.orig.kalender_id}`);
@@ -85,9 +87,14 @@ export default {
activeContextActions() {
if (this.isHeaderOrFooter) return [];
const type = this.event.orig?.type ?? 'lehreinheit';
const actions = this.contextMenuActions[type] ?? this.contextMenuActions['default'] ?? [];
return actions.filter(action => !action.visible || action.visible(this.event.orig));
}
const actions =
this.contextMenuActions[type] ??
this.contextMenuActions['default'] ??
[];
return actions.filter(
(action) => !action.visible || action.visible(this.event.orig),
);
},
},
methods: {
onResizeStart(edge, evt) {
@@ -95,7 +102,7 @@ export default {
edge,
evt,
el: this.$refs.eventEl,
event: this.event
event: this.event,
});
},
onRightClick(evt) {
@@ -116,27 +123,24 @@ export default {
evt.dataTransfer.setData('fhc-grab-offset-x', evt.clientX - rect.left);
},
onDropOnCard(evt, items) {
if (this.isHeaderOrFooter || !this.onDrop)
return;
if (this.isHeaderOrFooter || !this.onDrop) return;
const list = Array.isArray(items) ? items : [items];
const obj = list[0];
if (!obj)
return;
if (!obj) return;
if ((evt.ctrlKey || evt.metaKey) && obj.type === 'lehreinheit')
{
if ((evt.ctrlKey || evt.metaKey) && obj.type === 'lehreinheit') {
return this.onDrop({
item: [obj],
ctrlKey: true,
targetKalenderId: this.event.orig?.kalender_id ?? null
targetKalenderId: this.event.orig?.kalender_id ?? null,
});
}
return this.onDropEvent(evt, items, this.event.start.startOf('day'));
},
},
template:`
template: `
<div
class="fhc-calendar-base-grid-line-event event"
:class="classes"
@@ -195,5 +199,5 @@ export default {
</ul>
</teleport>
</div>
`
}
`,
};
+92 -70
View File
@@ -1,4 +1,4 @@
import FhcCalendar from "./Base.js";
import FhcCalendar from './Base.js';
import { useEventLoader } from '../../composables/EventLoader.js';
@@ -8,11 +8,10 @@ import ModeTable from './Mode/Table.js';
import ApiKalender from '../../api/factory/tempus/kalender.js';
import draggable from '../../directives/draggable.js';
export default {
name: "CalendarTempus",
name: 'CalendarTempus',
components: {
FhcCalendar
FhcCalendar,
},
inject: {
renderers: {from: 'renderers'},
@@ -23,9 +22,9 @@ export default {
appConfig: {
from: 'appConfig',
default: {
visible_status: 'all'
}
}
visible_status: 'all',
},
},
},
directives: {
draggable,
@@ -33,35 +32,35 @@ export default {
props: {
timezone: {
type: String,
required: true
required: true,
},
date: {
type: [Date, String, Number, luxon.DateTime],
default: luxon.DateTime.local()
default: luxon.DateTime.local(),
},
mode: {
type: String,
default: 'Week'
default: 'Week',
},
getPromiseFunc: {
type: Function,
required: true
required: true,
},
parkedEvents: {
type: Object,
default: () => new Set()
default: () => new Set(),
},
visibleLecturers: {
type: Array,
default: null
default: null,
},
extraBackgrounds: {
type: Array,
default: () => []
default: () => [],
},
visibleStatus: {
type: Array,
default: () => ['all']
default: () => ['all'],
},
showEvents: {
type: Boolean,
@@ -77,7 +76,8 @@ export default {
"resize",
"event-hover",
"event-unhover",
"open-reservierung"
"open-reservierung",
"events-reloaded",
],
data() {
@@ -90,11 +90,11 @@ export default {
modeOptions: {
day: {
emptyMessage: Vue.computed(() => this.$p.t('lehre/noLvFound')),
emptyMessageDetails: Vue.computed(() => this.$p.t('lehre/noLvFound'))
emptyMessageDetails: Vue.computed(() => this.$p.t('lehre/noLvFound')),
},
week: {
collapseEmptyDays: false
}
collapseEmptyDays: false,
},
},
currentMode: this.mode,
teachingunits: null,
@@ -102,57 +102,62 @@ export default {
showRaster: true,
};
},
watch: {
events() {
this.$emit('events-reloaded');
}
},
computed: {
backgrounds() {
let now = luxon.DateTime.now().setZone(this.timezone);
let past = [];
if (this.mode == 'Month')
{
past = [{
class: 'background-past',
end: now.startOf('day')
}];
}
else
{
past = [{
class: 'background-past',
end: now,
label: now.startOf('minute').toISOTime({ suppressSeconds: true, includeOffset: false })
}];
if (this.mode == 'Month') {
past = [
{
class: 'background-past',
end: now.startOf('day'),
},
];
} else {
past = [
{
class: 'background-past',
end: now,
label: now
.startOf('minute')
.toISOTime({ suppressSeconds: true, includeOffset: false }),
},
];
}
return [
...past,
...(this.extraBackgrounds || [])
];
return [...past, ...(this.extraBackgrounds || [])];
},
visibleEvents()
{
visibleEvents() {
let list = this.events;
if (Array.isArray(this.visibleLecturers))
{
if (Array.isArray(this.visibleLecturers)) {
const visibleLectures = new Set(this.visibleLecturers);
list = list.filter(event => {
if (!event.lektor?.length)
return true;
return event.lektor.some(lektor => visibleLectures.has(lektor.mitarbeiter_uid));
list = list.filter((event) => {
if (!event.lektor?.length) return true;
return event.lektor.some((lektor) =>
visibleLectures.has(lektor.mitarbeiter_uid),
);
});
}
if (!this.visibleStatus.length || this.visibleStatus.includes('all'))
return list;
return list.filter(event => this.visibleStatus.includes(event.status_kurzbz));
return list.filter((event) =>
this.visibleStatus.includes(event.status_kurzbz),
);
},
},
methods: {
eventStyle(event) {
if (!event.farbe)
return undefined;
if (!event.farbe) return undefined;
return '--event-bg:#' + event.farbe;
},
updateRange(rangeInterval) {
@@ -171,7 +176,7 @@ export default {
ondrop(payload){
this.$emit('drop', payload);
},
onresize(payload){
onresize(payload) {
this.$emit('resize', payload);
},
resetEventLoader() {
@@ -183,13 +188,34 @@ export default {
navigateNext() {
this.$refs.calendar.clickNext();
},
clearOutCalendarEventEmphasis() {
this.$refs.calendar.$el
.querySelectorAll(
'.fhc-calendar-base-grid .fhc-calendar-base-grid-line-event',
)
.forEach((el) => {
const spinner = el.querySelector('.spinner-overlay');
if (spinner) {
spinner.remove();
}
el.classList.remove(
'updating-event',
'updated-event',
'updated-event-long',
);
});
},
},
setup(props, context) {
const rangeInterval = Vue.ref(null);
const { events, lv, reset } = useEventLoader(rangeInterval, props.getPromiseFunc);
const { events, lv, reset } = useEventLoader(
rangeInterval,
props.getPromiseFunc,
);
Vue.watch(lv, newValue => {
Vue.watch(lv, (newValue) => {
context.emit('update:lv', newValue);
});
@@ -197,31 +223,27 @@ export default {
rangeInterval,
events,
lv,
reset
reset,
};
},
created() {
this.$api
.call(ApiKalender.getStunden())
.then(res => {
return this.teachingunits = res.data.map(el => ({
id: el.stunde,
start: el.beginn,
end: el.ende
}));
});
this.$api.call(ApiKalender.getStunden()).then((res) => {
return (this.teachingunits = res.data.map((el) => ({
id: el.stunde,
start: el.beginn,
end: el.ende,
})));
});
this.$api
.call(ApiKalender.getCalendarHours())
.then(res => {
this.hoursplan = {
start: res.data.start,
end: res.data.end
};
});
this.$api.call(ApiKalender.getCalendarHours()).then((res) => {
this.hoursplan = {
start: res.data.start,
end: res.data.end,
};
});
},
template: /* html */`
template: /* html */ `
<fhc-calendar
ref="calendar"
class="fhc-calendar-lvplan"
+190 -5
View File
@@ -136,6 +136,7 @@ export default {
showEvents: true,
reservierungPending: false,
bcc: null,
currentlyUpdatedEvent: null,
};
},
computed: {
@@ -456,25 +457,34 @@ export default {
if (hasLektoren)
filter.uid = this.lecturers.map((lecture) => lecture.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) => {
this.scrollToAndEmphasizeUpdatedEvent();
});
}
return response;
},
_parseDates(start, end) {
const startDT = luxon.DateTime.fromISO(start);
@@ -516,7 +526,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;
}
});
},
@@ -527,12 +541,26 @@ export default {
const { item, start, end } = payload;
const obj = item[0];
if (!obj?.orig?.kalender_id)
return alert('Kein gültiges Kalender-Event zum Resizen');
return;
const dates = this._parseDates(start, end);
if (!dates) return;
if (
luxon.DateTime.fromISO(obj.orig.isostart).toMillis() ===
dates.startDT.toMillis() &&
luxon.DateTime.fromISO(obj.orig.isoend).toMillis() ===
dates.endDT.toMillis()
)
return;
this.updateKalenderEventElementDisplay(
obj.orig.eindeutige_kalender_gruppen_id,
dates.startDT,
dates.endDT,
);
return this._updateKalenderEvent(
obj,
dates.startDT,
@@ -581,8 +609,22 @@ export default {
const dates = this._parseDates(start, end);
if (!dates) return;
if (
luxon.DateTime.fromISO(obj.orig.isostart).toMillis() ===
dates.startDT.toMillis() &&
luxon.DateTime.fromISO(obj.orig.isoend).toMillis() ===
dates.endDT.toMillis()
)
return;
const { startDT, endDT, start_time, end_time } = dates;
this.updateKalenderEventElementDisplay(
obj.orig.eindeutige_kalender_gruppen_id,
startDT,
endDT,
);
if (obj.type === 'reservierung') {
this.reservierungPending = true;
this.$refs.reservierung.show(start_time, end_time);
@@ -872,6 +914,148 @@ export default {
focusSearchbar() {
this.$refs.header?.focusSearchbar();
},
scrollToAndEmphasizeUpdatedEvent() {
if (!this.currentlyUpdatedEvent) return;
document
.querySelectorAll(
'.fhc-calendar-base-grid .fhc-calendar-base-grid-line-event',
)
.forEach((el) => {
const spinner = el.querySelector('.spinner-overlay');
if (spinner) {
spinner.remove();
}
el.classList.remove(
'updating-event',
'updated-event',
'updated-event-long',
);
});
setTimeout(() => {
const eventEl = document.querySelector(
`[data-group-id="event-group-${this.currentlyUpdatedEvent.eindeutige_kalender_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';
if (!isInsideScrolledView) timeout = 300;
setTimeout(() => {
eventEl.classList.add(emphasizeUpdateClassName);
}, timeout);
this.currentlyUpdatedEvent = null;
}, 100);
},
updateKalenderEventElementDisplay(calendarGruppenId, startDT, endDT) {
if (!calendarGruppenId)
return;
let startOfDay = startDT.startOf('day');
let newPotentialStart = startDT.diff(startOfDay).toMillis() ?? 1;
let newPotentialEnd = endDT.diff(startOfDay).toMillis();
const calendar = this.$refs.calendar?.$el;
let element = calendar?.querySelector(
`[data-group-id="event-group-${calendarGruppenId}"]`,
);
if (!element) return;
const targetGridLine = [...calendar.querySelectorAll(
'.fhc-calendar-base-grid-line',
)].find((gridLine) => {
const [rowStart, columnStart, rowEnd] = getComputedStyle(
gridLine,
).gridArea.split(' / ');
return (
rowStart === '1' &&
columnStart === String(startDT.weekday) &&
rowEnd === '-1'
);
});
const changedDay =
targetGridLine && element.parentElement !== targetGridLine;
setTimeout(() => {
if (!calendar.contains(element)) return;
if (changedDay) {
targetGridLine.insertBefore(element, null);
element.classList.add('tempus-temporary-calendar-event');
}
element.style.gridRowEnd = 't_' + newPotentialEnd;
element.style.gridRowStart = 't_' + newPotentialStart;
}, 100);
const outerDiv = document.createElement('div');
outerDiv.className = 'spinner-overlay';
const innerDiv = document.createElement('div');
innerDiv.className = 'spinner';
outerDiv.appendChild(innerDiv);
element.appendChild(outerDiv);
const eventRect = element.getBoundingClientRect();
const offset = 300;
const isInsideScrolledView =
element.offsetLeft < calendar.scrollLeft + calendar.clientWidth &&
element.offsetLeft + element.offsetWidth > calendar.scrollLeft &&
element.offsetTop < calendar.scrollTop + calendar.clientHeight &&
element.offsetTop + element.offsetHeight > calendar.scrollTop;
const rect = element.getBoundingClientRect();
if (!isInsideScrolledView) {
element.scrollIntoView({
behavior: 'smooth',
inline: 'center',
block: 'nearest',
});
}
},
clearTemporaryEvents() {
const calendar = this.$refs.calendar?.$el;
if (!calendar) return;
const tempEvents = calendar.querySelectorAll(
'.tempus-temporary-calendar-event',
);
tempEvents.forEach((event) => {
event.remove();
});
}
},
watch: {
lecturers: {
@@ -1021,6 +1205,7 @@ export default {
@open-reservierung="openReservierung"
:extra-backgrounds="extraBackgrounds"
@update:range="handleRange"
@events-reloaded="clearTemporaryEvents"
class="responsive-calendar"
/>
</template>
+75 -50
View File
@@ -1,17 +1,28 @@
// 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));
const allEvents = Vue.computed(() =>
events.value.concat(loadingEvents.value),
);
const lv = Vue.ref(null);
const eventsLoaded = [];
const mergePromiseArr = (n, o) => {
if (Array.isArray(n))
return o.concat(n);
return o.push(n), o;
if (Array.isArray(n)) return o.concat(n);
return (o.push(n), o);
};
const markEventsLoaded = (start, end) => {
@@ -20,10 +31,10 @@ export function useEventLoader(rangeInterval, getPromiseFunc) {
// empty: add new chunk
eventsLoaded.push(start.ts, end.ts);
} else {
if (eventsLoaded[eventsLoaded.length-1] + 1 == start.ts) {
if (eventsLoaded[eventsLoaded.length - 1] + 1 == start.ts) {
// add to the end of last chunk
eventsLoaded[eventsLoaded.length-1] = end.ts;
} else if (eventsLoaded[eventsLoaded.length-1] < start.ts) {
eventsLoaded[eventsLoaded.length - 1] = end.ts;
} else if (eventsLoaded[eventsLoaded.length - 1] < start.ts) {
// add new chunk after the last chunk
eventsLoaded.push(start.ts, end.ts);
} else if (eventsLoaded[0] == end.ts + 1) {
@@ -32,14 +43,13 @@ export function useEventLoader(rangeInterval, getPromiseFunc) {
} else if (eventsLoaded[0] > end.ts) {
eventsLoaded.unshift(start.ts, end.ts);
} else {
let index = eventsLoaded.findIndex(e => e >= start.ts);
let index = eventsLoaded.findIndex((e) => e >= start.ts);
if (index % 2) {
// starts inside an existing chunk
if (eventsLoaded[index] >= end.ts)
return []; // Already loaded
if (eventsLoaded[index] >= end.ts) return []; // Already loaded
let indexIsLast = (index == eventsLoaded.length - 1);
let indexIsLast = index == eventsLoaded.length - 1;
if (indexIsLast || eventsLoaded[index + 1] > end.ts) {
// extend an existing chunk
@@ -48,14 +58,16 @@ export function useEventLoader(rangeInterval, getPromiseFunc) {
start = start.plus(nStart - start.ts);
if (!indexIsLast && eventsLoaded[index + 1] == end.ts + 1)
eventsLoaded.splice(index, 2);
else
eventsLoaded[index] = end.ts;
else eventsLoaded[index] = end.ts;
} else {
// merge exising chunks
// and load the rest if necessary
if (eventsLoaded[index + 2] < end.ts) {
let rStart = eventsLoaded[index + 2] + 1;
result = mergePromiseArr(markEventsLoaded(start.plus(rStart - start.ts), end), result);
result = mergePromiseArr(
markEventsLoaded(start.plus(rStart - start.ts), end),
result,
);
}
let nStart = eventsLoaded[index] + 1;
@@ -71,24 +83,32 @@ export function useEventLoader(rangeInterval, getPromiseFunc) {
// and load the rest if necessary
if (eventsLoaded[1] < end.ts) {
let rStart = eventsLoaded[1] + 1;
result = mergePromiseArr(markEventsLoaded(start.plus(rStart - start.ts), end), result);
result = mergePromiseArr(
markEventsLoaded(start.plus(rStart - start.ts), end),
result,
);
}
let nEnd = eventsLoaded[0] - 1;
end = end.plus(nEnd - end.ts);
eventsLoaded[0] = start.ts;
} else if (eventsLoaded[index] == start.ts) {
// starts at the same position as an existing chunk
if (eventsLoaded[index + 1] >= end.ts)
return []; // Already loaded
if (eventsLoaded[index + 1] >= end.ts) return []; // Already loaded
// load the rest
let rStart = eventsLoaded[index + 1] + 1;
result = mergePromiseArr(markEventsLoaded(start.plus(rStart - start.ts), end), result);
result = mergePromiseArr(
markEventsLoaded(start.plus(rStart - start.ts), end),
result,
);
} else {
// extend an existing chunk
// and load the rest if necessary
if (eventsLoaded[index + 1] < end.ts) {
let rStart = eventsLoaded[index + 1] + 1;
result = mergePromiseArr(markEventsLoaded(start.plus(rStart - start.ts), end), result);
result = mergePromiseArr(
markEventsLoaded(start.plus(rStart - start.ts), end),
result,
);
}
let nEnd = eventsLoaded[index] - 1;
end = end.plus(nEnd - end.ts);
@@ -98,52 +118,57 @@ export function useEventLoader(rangeInterval, getPromiseFunc) {
}
}
if (start.ts >= end.ts)
return result;
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);
};
const reload = () => {
const range = Vue.toValue(rangeInterval);
if (!(range instanceof luxon.Interval))
return;
const promises = markEventsLoaded(range.start, range.end);
Promise
.allSettled(promises)
.then(results => {
results.forEach(res => {
if (
res.status === 'fulfilled'
&& res.value.meta.status === "success"
) {
if (res.value.meta.lv)
lv.value = res.value.meta.lv;
if (!(range instanceof luxon.Interval)) return;
events.value = events.value.concat(res.value.data);
loadingEvents.value = [];
}
})
if (!rangeIntervalHolder || !range.equals(rangeIntervalHolder)) {
hasFirstLoadOccurred = false;
rangeIntervalHolder = range;
}
const promises = markEventsLoaded(range.start, range.end);
Promise.allSettled(promises).then((results) => {
results.forEach((res) => {
if (res.status === 'fulfilled' && res.value.meta.status === 'success') {
if (res.value.meta.lv) lv.value = res.value.meta.lv;
tempEventsHolder = tempEventsHolder.concat(res.value.data);
loadingEvents.value = [];
}
});
events.value = tempEventsHolder;
});
hasFirstLoadOccurred = true;
};
Vue.watchEffect(reload);
const reset = () => {
loading_id = 0;
events.value = [];
loadingEvents.value = [];
tempEventsHolder = [];
//loadingEvents.value = [];
eventsLoaded.splice(0, eventsLoaded.length);
reload();
}
};
return { events: allEvents, lv, reset }
}
return { events: allEvents, lv, reset };
}