past shadow on day page if it is today; current time line on day & week page; mobile checkup; close lvInfoModal on stundenplan unmount;

This commit is contained in:
Johann Hoffmann
2025-02-04 18:04:41 +01:00
parent 9082c3cb8c
commit d70cd58698
9 changed files with 246 additions and 51 deletions
+18 -1
View File
@@ -185,8 +185,25 @@
aspect-ratio: 1;
}
.fhc-calendar-past {
background-color:#F5E9D7;
border-color: #E8E8E8;
opacity: 0.5;
}
.fhc-calendar-month-page-day-highlight {
background-color: #f5f5f5;
/*background-color: #f5f5f5;*/
/*background-color: red;*/
}
.fhc-highlight-week {
/*border-color: black !important;*/
}
.fhc-highlight-day {
border-width: 2px !important;
border-color: black !important;
}
.fhc-calendar-sm .fhc-calendar-month-page-day.active .no,
+10
View File
@@ -78,6 +78,11 @@ const app = Vue.createApp({
appSideMenuEntries: {}
}),
components: {},
computed: {
isMobile() {
return /Mobi|Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
},
methods: {
isInternalRoute(href) {
const internalBase = window.location.origin
@@ -97,6 +102,11 @@ const app = Vue.createApp({
if(!res?.matched?.length) return
event.preventDefault(); // Prevent browser navigation
if(this.isMobile) { // toggle the menu
document.getElementById('nav-main-btn').click();
}
this.$router.push(route);
}
}
+5 -1
View File
@@ -8,7 +8,9 @@ import CalendarMinimized from './Minimized.js';
import CalendarDate from '../../composables/CalendarDate.js';
import CalendarDates from '../../composables/CalendarDates.js';
// TODO(chris): week/month toggle
const todayDate = new Date(new Date().setHours(0, 0, 0, 0));
const today = todayDate.getTime()
export default {
components: {
@@ -22,6 +24,8 @@ export default {
},
provide() {
return {
today,
todayDate,
date: this.date,
focusDate: this.focusDate,
size: Vue.computed({ get: () => this.size, set: v => this.size = v }),
+53 -14
View File
@@ -17,11 +17,14 @@ export default {
data() {
return {
hourPosition: null,
curHourPosition: null,
hourPositionTime: null,
lvMenu: null,
}
},
inject: [
'today',
'todayDate',
'date',
'focusDate',
'size',
@@ -107,12 +110,6 @@ export default {
datum: this.day.toLocaleString(this.$p.user_locale.value, [{ day: 'numeric', month: 'numeric' }, { day: 'numeric', month: 'numeric' }, { day: 'numeric', month: 'numeric' }, { dateStyle: 'short' }][this.size]),
}
},
dayGridStyle() {
return {
'grid-template-columns': '1 1fr',
'grid-template-rows': 'repeat(' + (this.hours.length * 60 / this.smallestTimeFrame) + ', 1fr)',
}
},
noLvStyle() {
return {
top: (this.calendarScrollTop + 100) + 'px',
@@ -135,6 +132,22 @@ export default {
right: 0,
}
},
curTime() {
const now = new Date();
return String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');
},
curIndicatorStyle() {
return {
'pointer-events': 'none',
'padding-left': '7rem',
'margin-top': '-1px',
'z-index': 2,
'border-color': '#00649C!important',
top: this.getDayTimePercent + '%',
left: 0,
right: 0,
}
},
noEventsCondition() {
return !this.isSliding && this.filteredEvents?.length === 0;
},
@@ -192,8 +205,32 @@ export default {
smallestTimeFrame() {
return [30, 15, 10, 5][this.size];
},
lookingAtToday() {
return this.date.compare(this.todayDate)
},
getDayTimePercent() {
const now = new Date(Date.now())
const currentMinutes = now.getMinutes() + now.getHours() * 60
let timePercentage = ((currentMinutes - (this.hours[0] * 60)) / (this.hours.length * 60)) * 100;
return timePercentage
}
},
methods: {
dayGridStyle(day) {
const styleObj = {
'grid-template-columns': '1 1fr',
'grid-template-rows': 'repeat(' + (this.hours.length * 60 / this.smallestTimeFrame) + ', 1fr)',
}
if(this.date.compare(this.todayDate)) {
styleObj['backgroundImage'] = 'linear-gradient(to bottom, #F5E9D7 '+this.getDayTimePercent+'%, #FFFFFF '+this.getDayTimePercent+'%)'
styleObj['border-color'] = '#E8E8E8';
// styleObj.opacity = 0.5; // would opaque the whole column
}
return styleObj
},
fetchLvMenu(event) {
if (event && event.type == 'lehreinheit') {
this.$fhcApi.factory.stundenplan.getLehreinheitStudiensemester(event.lehreinheit_id[0]).then(
@@ -270,6 +307,7 @@ export default {
// calculate the minutes percentage of the total minutes
timePercentage = ((currentMinutes - (this.hours[0] * 60)) / (this.hours.length * 60)) * 100;
// calculate the relative position of the time percentage
console.log('height: ', height)
position = height * (timePercentage / 100);
this.hourPosition = position;
@@ -290,13 +328,9 @@ export default {
},
dateToMinutesOfDay(day) {
return Math.floor(((day.getHours() - 7) * 60 + day.getMinutes()) / this.smallestTimeFrame) + 1;
},
}
},
mounted() {
const container = document.getElementById("calendarContainer")
if(container) container.style.overflow = 'hidden'
},
template: /*html*/`
<div class="fhc-calendar-day-page h-100">
<div class="row m-0 h-100">
@@ -308,7 +342,7 @@ export default {
<a href="#" class="small text-secondary text-decoration-none" >{{dayText.datum}}</a>
</div>
</div>
<div id="scroll g-0" style="height: 100%; overflow: scroll;">
<div id="scroll g-0" style="height: 100%; overflow-y: scroll;">
<div ref="eventcontainer" class="position-relative flex-grow-1" @mousemove="calcHourPosition" @mouseleave="hourPosition = null" >
<div :id="hourGridIdentifier(hour)" v-for="hour in hours" :key="hour" class="position-absolute box-shadow-border-top" :style="hourGridStyle(hour)"></div>
@@ -318,6 +352,11 @@ export default {
<span class="border border-top-0 px-2 bg-white">{{hourPositionTime}}</span>
</div>
</Transition>
<Transition>
<div v-if="lookingAtToday && !noEventsCondition" class="position-absolute border-top small" :style="curIndicatorStyle">
<span class="border border-top-0 px-2 bg-white">{{curTime}}</span>
</div>
</Transition>
<div>
<h1 v-if="noEventsCondition" class="m-0 text-secondary" ref="noEventsText" :style="noLvStyle">Keine Lehrveranstaltungen</h1>
<div :class="{'fhc-calendar-no-events-overlay':noEventsCondition, 'events':true}">
@@ -325,7 +364,7 @@ export default {
<div class="hours">
<div v-for="hour in hours" style="min-height:100px" :key="hour" class="text-muted text-end small" :ref="'hour' + hour">{{hour}}:00</div>
</div>
<div v-for="day in eventsPerDayAndHour" :key="day" class=" day border-start" :style="dayGridStyle">
<div v-for="day in eventsPerDayAndHour" :key="day" class=" day border-start" :style="dayGridStyle(day)">
<div v-for="event in day.events" :key="event" :style="eventGridStyle(day,event)" v-contrast :selected="event.orig == selectedEvent" class="fhc-entry mx-2 small rounded overflow-hidden " >
<!-- desktop version of the page template, parent receives slotProp mobile = false -->
<div class="d-none d-xl-block h-100 " @click.prevent="eventClick(event)">
@@ -348,7 +387,7 @@ export default {
</div>
</div>
</div>
<div class="d-xl-block col-xl-6 p-4" style="max-height: 100%">
<div class="d-xl-block col-xl-6 p-4 d-none" style="max-height: 100%">
<div style="z-index:0; max-height: 100%" class="sticky-top d-flex justify-content-center align-items-center flex-column">
<div style="max-height: 100%; overflow-y:auto;" class="w-100">
<template v-if="selectedEvent && lvMenu">
+54 -13
View File
@@ -9,6 +9,8 @@ export default {
}
},
inject: [
'today',
'todayDate',
'date',
'focusDate',
'size',
@@ -63,14 +65,20 @@ export default {
methods: {
getDayClass(week, day) {
let classstring = 'fhc-calendar-month-page-day text-decoration-none overflow-hidden'
const isHighlighted = this.isHighlighted(week, day)
const isHighlightedWeek = this.isHighlightedWeek(week)
const isHighlightedDay = this.isHighlightedDay(day)
const isThisDate = this.date.compare(day)
const isNotThisMonth = day.getMonth() != this.month
const isInThePast = this.date.isInPast(day)
if(isHighlighted) classstring += ' fhc-calendar-month-page-day-highlight'
if(isThisDate) classstring += ' active'
if(isNotThisMonth || isInThePast) classstring += ' opacity-50'
const isInThePast = day.getTime() < this.today // this.date is just the focusDate but not the initial Date
if(isHighlightedWeek) classstring += ' fhc-highlight-week'
if(isHighlightedDay) classstring += ' fhc-highlight-day'
if(isThisDate) classstring += ' active'
if(isNotThisMonth) classstring += ' opacity-25'
if(isInThePast) classstring += ' fhc-calendar-past'
return classstring
},
selectDay(day) {
@@ -85,12 +93,14 @@ export default {
}
},
highlight(week, day){
console.log('highlight method')
this.highlightedWeek = week.no;
this.highlightedDay = day;
},
isHighlighted(week, day) {
return this.noWeekView ? day == this.highlightedDay : week.no == this.highlightedWeek;
isHighlightedDay(day) {
return day == this.highlightedDay
},
isHighlightedWeek(week) {
return week.no == this.highlightedWeek
},
clickEvent(day,week) {
if(!this.noWeekView)
@@ -99,12 +109,41 @@ export default {
this.$emit('updateMode', 'day');
}
this.selectDay(day);
}
},
getNumberStyle(day) {
// TODO: move this to active css class in calendar.css
const styleObj = {}
// styleObj.display = 'inline-block';
// styleObj.widt = '32px'; /* Adjust based on the calendar cell size */
// styleObj.height = '32px';
// styleObj['line-height'] = '32px';
// styleObj['text-align'] = 'center';
// styleObj['font-weight'] = 'bold';
// styleObj['font-size'] = '14px';
//
// if(day.getDate() === this.todayDate.getDate()
// && day.getMonth() === this.todayDate.getMonth()
// && day.getFullYear() === this.todayDate.getFullYear()) {
// styleObj['background-color'] = '#00649c'; // fh blau
// styleObj.color = 'white';
// } else {
// // styleObj['background-color'] = '#ffffff'; // fh blau
// // styleObj.color = 'white'; /* White text for contrast */
// }
return styleObj
}
},
mounted() {
const container = document.getElementById("calendarContainer")
if(container) container.style.overflow = 'scroll'
if(container) container.style['overflow-y'] = 'scroll'
},
// unmounted() {
// const container = document.getElementById("calendarContainer")
// if(container) container.style['overflow-y'] = ''
// },
template: /*html*/`
<div class="fhc-calendar-month-page" :class="{'show-weeks': showWeeks}">
<div v-if="showWeeks" class=" bg-light fw-bold border-top border-bottom text-center"></div>
@@ -113,7 +152,8 @@ export default {
</div>
<template v-for="week in weeks"
:key="week.no">
<a href="#" v-if="showWeeks" class="fhc-calendar-month-page-weekday text-decoration-none text-end opacity-25" @click.prevent="changeToWeek(week)">{{week.no}}</a>
<a href="#" v-if="showWeeks" class="fhc-calendar-month-page-weekday text-decoration-none text-end opacity-25"
@click.prevent="changeToWeek(week)">{{week.no}}</a>
<a href="#"
@click.prevent="clickEvent(day,week)"
@mouseover="highlight(week,day)"
@@ -122,9 +162,10 @@ export default {
:key="day"
:class="getDayClass(week, day)"
>
<span class="no">{{day.getDate()}}</span>
<span class="no" :style="getNumberStyle(day)">{{day.getDate()}}</span>
<span v-if="events[day.toDateString()] && events[day.toDateString()].length" class="events">
<div @click="setSelectedEvent(event);" v-for="event in events[day.toDateString()]" :key="event.id" :style="{'background-color': event.color}" class="fhc-entry" :selected="event == selectedEvent" v-contrast >
<div @click="setSelectedEvent(event);" v-for="event in events[day.toDateString()]" :key="event.id"
:style="{'background-color': event.color}" class="fhc-entry" :selected="event == selectedEvent" v-contrast >
<slot name="monthPage" :event="event" :day="day" >
<p>this is a placeholder which means that no template was passed to the Calendar Page slot</p>
</slot>
+93 -7
View File
@@ -9,9 +9,13 @@ export default {
return{
hourPosition:null,
hourPositionTime:null,
resizeObserver: null,
width: 0
}
},
inject: [
'today',
'todayDate',
'date',
'focusDate',
'size',
@@ -32,6 +36,10 @@ export default {
'input',
],
computed: {
curTime() {
const now = new Date();
return String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');
},
pageHeaderStyle(){
return {
'z-index': 4,
@@ -91,7 +99,9 @@ export default {
let nextDay = new Date(day);
nextDay.setDate(nextDay.getDate()+1);
nextDay.setMilliseconds(nextDay.getMilliseconds()-1);
let d = {events:[],lanes:1};
let d = {events:[],lanes:1, isPast: false};
d.isPast = nextDay.getTime() < this.today
d.isToday = nextDay.getFullYear() === this.todayDate.getFullYear() && nextDay.getMonth() === this.todayDate.getMonth() && nextDay.getDate() === this.todayDate.getDate()
if (this.events[key]) {
this.events[key].forEach(evt => {
let event = {orig:evt,lane:1,maxLane:1,start: evt.start < day ? day : evt.start, end: evt.end > nextDay ? nextDay : evt.end,shared:[],setSharedMaxRecursive(doneItems) {
@@ -118,6 +128,37 @@ export default {
},
smallestTimeFrame() {
return [30,15,10,5][this.size];
},
lookingAtToday() {
return this.days.some(d =>
d.getFullYear() === this.todayDate.getFullYear() &&
d.getMonth() === this.todayDate.getMonth() &&
d.getDate() === this.todayDate.getDate()
)
},
curIndicatorStyle() {
const todayIndex = this.days.findIndex(d => d.getFullYear() === this.todayDate.getFullYear() &&
d.getMonth() === this.todayDate.getMonth() &&
d.getDate() === this.todayDate.getDate()
)
return {
'pointer-events': 'none',
'padding-left': '1rem',
'margin-top': '-1px',
'z-index': 2,
'border-color': '#00649C!important',
top: this.getDayTimePercent + '%',
left: 'calc(('+this.width+'px + 3em) /'+this.days.length+'*'+todayIndex+')',
right: 'calc(('+this.width+'px - 3em)/'+this.days.length+'*'+(this.days.length - 1 - todayIndex)+')',
}
},
getDayTimePercent() {
const now = new Date(Date.now())
const currentMinutes = now.getMinutes() + now.getHours() * 60
let timePercentage = ((currentMinutes - (this.hours[0] * 60)) / (this.hours.length * 60)) * 100;
return timePercentage
}
},
methods: {
@@ -135,10 +176,23 @@ export default {
}
},
dayGridStyle(day) {
return {
const styleObj = {
'grid-template-columns': 'repeat(' + day.lanes + ', 1fr)',
'grid-template-rows': 'repeat(' + (this.hours.length * 60 / this.smallestTimeFrame) + ', 1fr)',
}
if(day.isPast) {
styleObj['background-color'] = '#F5E9D7'
styleObj['border-color'] = '#E8E8E8';
styleObj.opacity = 0.5;
} else if (day.isToday) {
styleObj['backgroundImage'] = 'linear-gradient(to bottom, #F5E9D7 '+this.getDayTimePercent+'%, #FFFFFF '+this.getDayTimePercent+'%)'
styleObj['border-color'] = '#E8E8E8';
styleObj.opacity = 0.5;
}
return styleObj
},
eventGridStyle(day, event) {
return {
@@ -208,17 +262,44 @@ export default {
this.setSelectedEvent(event);
this.focusDate.set(new CalendarDate(new Date(event.datum)));
this.$emit('input', event)
}
},
initResizeObserver() {
const events = this.$refs['eventsRef'+this.week];
if (!events) return;
this.resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
const { width, height } = entry.contentRect;
if(width > 0) this.width = width
}
});
this.resizeObserver.observe(events);
},
destroyResizeObserver() {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
},
},
mounted() {
setTimeout(() => this.$refs.eventcontainer.scrollTop = this.$refs.eventcontainer.scrollHeight / 3 + 1, 0);
const container = document.getElementById("calendarContainer")
if(container) container.style.overflow = 'scroll'
if(container) {
container.style['overflow-y'] = 'scroll'
container.style['overflow-x'] = 'auto'
}
this.initResizeObserver();
},
beforeUnmount() {
this.destroyResizeObserver();
},
template: /*html*/`
<div class="fhc-calendar-week-page" style="min-width: 700px;">
<div ref="page" class="fhc-calendar-week-page" style="min-width: 700px;">
<div class="d-flex flex-column">
<div class="fhc-calendar-week-page-header d-grid border-2 border-bottom text-center" :style="pageHeaderStyle" >
<div type="button" v-for="day in days" :key="day" class="flex-grow-1" :title="dayText[day]?.heading" @click.prevent="changeToMonth(day)">
@@ -233,7 +314,12 @@ export default {
<span class="border border-top-0 px-2 bg-white">{{hourPositionTime}}</span>
</div>
</Transition>
<div class="events">
<Transition>
<div v-if="lookingAtToday" class="position-absolute border-top small" :style="curIndicatorStyle">
<span class="border border-top-0 px-2 bg-white">{{curTime}}</span>
</div>
</Transition>
<div class="events" :ref="'eventsRef'+week">
<div class="hours">
<div v-for="hour in hours" style="min-height:100px" :key="hour" class="text-muted text-end small" :ref="'hour' + hour">{{hour}}:00</div>
</div>
@@ -243,7 +329,7 @@ export default {
:style="eventGridStyle(day,event)"
class="mx-2 small rounded overflow-hidden fhc-entry "
v-contrast >
<slot name="weekPage" :event="event" :day="day">
<slot name="weekPage" :event="event" :day="day">
<p>this is a placeholder which means that no template was passed to the Calendar Page slot</p>
</slot>
</div>
@@ -151,6 +151,9 @@ export const Stundenplan = {
{
this.loadEvents();
},
beforeUnmount() {
if(this.$refs.lvmodal) this.$refs.lvmodal.hide()
},
template:/*html*/`
<h2>{{$p.t('lehre/stundenplan')}}</h2>
<hr>
+9 -9
View File
@@ -120,8 +120,6 @@ export default {
.catch((err) => {
console.error("ERROR: ", err);
});
},
mounted() {
@@ -141,10 +139,13 @@ export default {
}
}).observe(this.$refs.container);
}
this.carouselInstance = new bootstrap.Carousel(this.$refs.carousel, {
wrap: false, // keep this off even though it actually wraps
interval: false
});
Vue.nextTick(()=>{
this.carouselInstance = new bootstrap.Carousel(this.$refs.carousel, {
wrap: false, // keep this off even though it actually wraps
interval: false
});
})
},
template: /*html*/ `
<div ref="container" class="widgets-news h-100" :class="sizeClass" :style="getNewsWidgetStyle">
@@ -177,9 +178,8 @@ export default {
<div class="container h-100" style="padding: 0px;" ref="carocontainer">
<div id="FhcCarouselContainer" style="height: 100%;" ref="carousel" class="carousel slide fhc-carousel" data-bs-ride="carousel" data-bs-interval="false">
<div class="carousel-inner" ref="carouselInner" style="height: 100%; max-width: 100%;">
<div ref="carouselItems" v-for="(news, index) in newsList" class="carousel-item " style="overflow-y: auto; height: 100%;" :id="'card-'+news.news_id" v-html="news.content_obj.content">
</div>
<div class="carousel-inner" ref="carouselInner" style="height: 100%; max-width: 100%;">
<div ref="carouselItems" v-for="(news, index) in newsList" class="carousel-item " style="overflow-y: auto; overflow-x: hidden; height: 100%;" :id="'card-'+news.news_id" v-html="news.content_obj.content"/>
</div>
<button @click="setPrev" @focus="$event.target.blur()" style="z-index: 100; color: black; overflow: hidden; margin-left: 10px; width:35px;" data-bs-target="#FhcCarouselContainer" class="carousel-control-prev" type="button">
<div class="border rounded-circle" style="padding-left: 0.4rem; padding-right: 0.4rem; background-color:rgba(138,138,138,0.4)">
+1 -6
View File
@@ -199,11 +199,7 @@ class CalendarDate {
return true;
return false;
}
isInPast(d) {
if (this.isDate(d))
return (this.y > d.getFullYear() || this.m > d.getMonth() || this.d > d.getDate());
return false
}
setLocale(locale) {
this.weekStart = CalendarDate.getWeekStart(locale);
}
@@ -261,5 +257,4 @@ CalendarDate.getWeekStart = function(locale) {
}
}
export default CalendarDate