This commit is contained in:
chfhtw
2025-07-29 15:27:09 +02:00
parent ee5391f11a
commit 3cd00a5e5f
18 changed files with 0 additions and 2420 deletions
-17
View File
@@ -1,17 +0,0 @@
import CalendarHeader from './Header.js';
export default {
components: {
CalendarHeader
},
inject: [
'date',
'focusDate',
'size'
],
emits: [
'updateMode',
'change:range',
'input'
]
}
-301
View File
@@ -1,301 +0,0 @@
import CalendarMonth from './Month.js';
import CalendarMonths from './Months.js';
import CalendarYears from './Years.js';
import CalendarWeek from './Week.js';
import CalendarWeeks from './Weeks.js';
import CalendarDay from './Day.js';
import CalendarMinimized from './Minimized.js';
import CalendarDate from '../../composables/CalendarDate.js';
import CalendarDates from '../../composables/CalendarDates.js';
const todayDate = new Date(new Date().setHours(0, 0, 0, 0));
const today = todayDate.getTime()
export default {
components: {
CalendarMonth,
CalendarMonths,
CalendarYears,
CalendarWeek,
CalendarWeeks,
CalendarDay,
CalendarMinimized,
},
provide() {
return {
today,
todayDate,
date: this.date,
focusDate: this.focusDate,
size: Vue.computed({ get: () => this.size }),
containerHeight: Vue.computed({ get: () => this.containerHeight }),
containerWidth: Vue.computed({ get: () => this.containerWidth }),
events: Vue.computed(() => this.eventsPerDay),
filteredEvents: Vue.computed(() => this.filteredEvents),
minimized: Vue.computed({ get: () => this.minimized, set: v => this.$emit('update:minimized', v) }),
showWeeks: this.showWeeks,
noMonthView: this.noMonthView,
noWeekView: this.noWeekView,
eventsAreNull: Vue.computed(() => this.events === null),
mode: Vue.computed(()=>this.mode),
selectedEvent: Vue.computed(() => this.selectedEvent),
setSelectedEvent: (event)=>{this.selectedEvent = event;},
};
},
props: {
events: Array,
initialDate: {
type: [Date, String],
default: new Date()
},
showWeeks: {
type: Boolean,
default: true
},
initialMode: {
type: String,
default: 'month'
},
minimized: Boolean,
noWeekView: Boolean,
noMonthView: Boolean
},
watch:{
mode(newVal) {
this.$emit('change:mode', newVal)
},
selectedEvent:{
handler(newSelectedEvent) {
this.$emit('selectedEvent', newSelectedEvent);
},
immediate: true,
},
// scroll to the first event if the html element was found
scrollTime(newValue,oldValue){
// return early if the scrollTime is not set
if (!newValue.scrollTime || !newValue?.doScroll) return;
if (newValue?.scrollTime == oldValue?.scrollTime && newValue?.focusDate.d==oldValue?.focusDate.d) {
return;
}
// scroll the LvPlan to the closest event
let previousScrollAnchor = document.getElementById('scroll' + (newValue.scrollTime - 1) + this.focusDate.d + this.focusDate.w)
let scrollAnchor = document.getElementById('scroll' + newValue.scrollTime + this.focusDate.d + this.focusDate.w);
if (previousScrollAnchor) {
previousScrollAnchor.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
else {
if (scrollAnchor) {
scrollAnchor.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
},
},
emits: [
'select:day',
'select:event',
'change:range',
'change:mode',
'update:minimized',
'selectedEvent',
'change:offset'
],
data() {
return {
header: '',
prevMode: null,
currMode: null,
date: new CalendarDate(),
focusDate: new CalendarDate(),
size: 0,
containerWidth: 0,
containerHeight: 0,
selectedEvent:null,
}
},
computed: {
sizeClass() {
// mainly determines calendar font-size
return 'fhc-calendar-' + ['xs', 'sm', 'md', 'lg'][this.size];
},
mode: {
get() { return this.minimized ? 'minimized' : this.currMode; },
set(v) {
if (!v && this.prevMode) {
this.currMode = this.prevMode;
this.prevMode = null;
} else {
this.prevMode = this.currMode;
this.currMode = v;
}
}
},
eventsPerDay() {
if (!this.events)
return {};
return this.events.reduce((result, event) => {
let days = Math.ceil((event.end - event.start) / 86400000) || 1;
while (days-- > 0) {
let day = (new Date(event.start.getFullYear(), event.start.getMonth(), event.start.getDate() + days)).toDateString();
if (!result[day])
result[day] = [];
result[day].push(event);
}
return result;
}, {});
},
// returns the hour of the earliest event, used to scroll to the events in the calendar (week / day view)
scrollTime() {
let doScroll = true;
// return the first beginning time of the filtered events
if(this.filteredEvents && Array.isArray(this.filteredEvents) && this.filteredEvents.length > 0)
{
let scrollTimeEvents = this.filteredEvents.filter(event=>{
return event.type !== 'moodle';
});
// do not compute a new scroll time if there are no other events than moodle events
if(!(scrollTimeEvents.length >0)){
doScroll = false;
}
let scrollTime = parseInt(scrollTimeEvents.sort((a, b) => parseInt(a.beginn) - parseInt(b.beginn))[0]?.beginn);
// to ensure that the scrollTime watcher triggers even if the scrollTime doesn't change, it returns both the scrollTime and the focusDate
return { focusDate: { d: this.focusDate.d, w: this.focusDate.w}, doScroll, scrollTime };
}
// there is no event that matches the current view mode constraints
else
{
return { focusDate: { d: this.focusDate.d, w: this.focusDate.w }, doScroll,scrollTime: null };
}
},
// filters the events based on the current calendar view mode
// week view - filter events based on their week
// day view - filter events based on their day and week
// month view - does not filter the events
filteredEvents: function(){
if (this.events && Array.isArray(this.events) && this.events.length > 0) {
let filteredEvents = this.events.filter(event => {
let eventDate = new CalendarDate(new Date(event.datum));
if (this.mode == 'week' || this.mode == 'Week')
{
// week view filters the elements only for the same week
return this.focusDate.w == eventDate.w;
}
else if (this.mode == 'day' || this.mode == 'Day')
{
// day view filters the elements for the same day and the same week
return this.focusDate.d == eventDate.d && this.focusDate.w == eventDate.w;
}
else
{
// returns all the events, does not filter the events
return true;
}
})
return filteredEvents;
}
else
{
return null;
}
},
},
methods: {
handleRangeOffset(event){
let date = new Date(
this.focusDate.y,
this.focusDate.m,
this.focusDate.d
);
date = date.getFullYear() + "-" +
String(date.getMonth() + 1).padStart(2, "0") + "-" +
String(date.getDate()).padStart(2, "0");
this.$router.push({
name: "LvPlan",
params: {
mode: this.mode[0].toUpperCase() + this.mode.slice(1),
focus_date: date,
lv_id: this.$route.params.lv_id || null
}
});
this.$emit('change:range',event);
},
setMode(mode) {
this.mode = mode
},
handleInput(day) {
this.$emit(day[0], day[1]);
},
},
created() {
const initMode = this.initialMode.toLowerCase()
const allowedInitialModes = ['day'];
if (!this.noWeekView)
allowedInitialModes.push('week');
if (!this.noMonthView)
allowedInitialModes.push('month');
this.mode = allowedInitialModes[allowedInitialModes.indexOf(initMode)] || allowedInitialModes.pop();
this.date.set(new Date(this.initialDate));
this.focusDate.set(this.date);
},
mounted() {
if (this.$refs.container) {
new ResizeObserver(entries => {
for (const entry of entries) {
const w = entry.contentBoxSize ? entry.contentBoxSize[0].inlineSize : entry.contentRect.width;
const h = entry.contentBoxSize ? entry.contentBoxSize[0].blockSize : entry.contentRect.height;
// https://getbootstrap.com/docs/5.0/layout/breakpoints/
// bootstrap breakpoints watch window size and this function monitors container size of calendar itself.
// calendar is using bootstrap breakpoints which influence layout, which retriggers this function
// -> some width constellations will loop so we dont use values around bs5 breakpoints
// ['xs', 'sm', 'md', 'lg'][this.size]
if (w >= 600)
this.size = 3;
else if (w >= 350)
this.size = 2;
else if (w >= 250)
this.size = 1;
else
this.size = 0;
this.containerWidth = w
this.containerHeight = h
}
}).observe(this.$refs.container);
}
},
unmounted(){
CalendarDates.cleanup();
},
template: /*html*/`
<div ref="container" class="fhc-calendar card h-100" :class="sizeClass">
<component :is="'calendar-' + mode" @updateMode="mode = $event" @change:range="handleRangeOffset"
@input="handleInput" >
<template #calendarDownloads>
<slot name="calendarDownloads" ></slot>
</template>
<template #monthPage="{event,day}">
<slot name="monthPage" :event="event" :day="day" ></slot>
</template>
<template #weekPage="{event,day}">
<slot name="weekPage" :event="event" :day="day" ></slot>
</template>
<template #dayPage="{event,day,mobile}">
<slot name="dayPage" :event="event" :day="day" :mobile="mobile"></slot>
</template>
<template #pageMobilContent="{lvMenu, event}">
<slot name="pageMobilContent" :lvMenu="lvMenu" :event="event"></slot>
</template>
<template #minimizedPage="{event,day}">
<slot name="minimizedPage" :event="event" :day="day"></slot>
</template>
</component>
</div>`
}
-61
View File
@@ -1,61 +0,0 @@
import CalendarAbstract from './Abstract.js';
import CalendarPane from './Pane.js';
import CalendarDayPage from './Day/Page.js';
import CalendarDate from '../../composables/CalendarDate.js';
export default {
mixins: [
CalendarAbstract
],
components: {
CalendarDayPage,
CalendarPane
},
computed: {
title() {
return this.focusDate.wYear + ' KW ' + this.focusDate.w;
}
},
methods: {
paneChanged(dir) {
let previousDate = new CalendarDate(this.focusDate);
this.focusDate.d += dir;
this.emitRangeChanged(previousDate);
},
emitRangeChanged(previousDate, mounted) {
this.$emit('change:range', { start: previousDate, end:this.focusDate });
},
prev() {
this.$refs.pane.prev();
this.$emit('change:offset', { y: 0, m: 0, d: -1 });
},
next() {
this.$refs.pane.next();
this.$emit('change:offset', { y: 0, m: 0, d: 1 });
},
selectEvent(event) {
this.$emit('input', ['select:event', event]);
}
},
mounted() {
this.emitRangeChanged(new CalendarDate(this.focusDate.y, this.focusDate.m, this.focusDate.d -1), true);
},
template: /*html*/`
<div class="fhc-calendar-day">
<calendar-header :title="title" @prev="prev" @next="next" @updateMode="$emit('updateMode', $event)" @click="$emit('updateMode', 'week')">
<template #calendarDownloads>
<slot name="calendarDownloads"></slot>
</template>
</calendar-header>
<calendar-pane ref="pane" v-slot="slot" @slid="paneChanged">
<calendar-day-page :active="slot.active" :year="focusDate.y" :week="focusDate.w+slot.offset" @updateMode="$emit('updateMode', $event)" @page:back="prev" @page:forward="next" @input="selectEvent" >
<template #dayPage="{event,day,mobile}">
<slot name="dayPage" :event="event" :day="day" :mobile="mobile" ></slot>
</template>
<template #pageMobilContent="{lvMenu, event}">
<slot name="pageMobilContent" :lvMenu="lvMenu" :event="event" ></slot>
</template>
</calendar-day-page>
</calendar-pane>
</div>`
}
-490
View File
@@ -1,490 +0,0 @@
import CalendarDate from '../../../composables/CalendarDate.js';
import LvModal from "../../../components/Cis/Mylv/LvModal.js";
import ApiLvPlan from '../../../api/factory/lvPlan.js';
import ApiAddons from '../../../api/factory/addons.js';
function ggt(m, n) {
return n == 0 ? m : ggt(n, m % n);
}
function kgv(m, n) {
return (m * n) / ggt(m, n);
}
export default {
name: 'DayPage',
components: {
LvModal,
},
data() {
return {
hourPosition: null,
curHourPosition: null,
hourPositionTime: null,
lvMenu: null,
}
},
inject: [
'today',
'todayDate',
'date',
'focusDate',
'size',
'events',
'noMonthView',
'filteredEvents',
'isSliding',
'calendarScrollTop',
'calendarClientHeight',
'setSelectedEvent',
'selectedEvent',
'rowMinHeight'
],
props: {
year: Number,
week: Number,
active: Boolean,
},
emits: [
'updateMode',
'page:back',
'page:forward',
'input'
],
watch: {
//TODO: on first render non of the day-page components are active and the watcher on selectedEvent does not fetch the lvMenu
//TODO: workaround is to watch the active state and refetch in case the lvMenu is empty
activeAndEventsReady: {
handler({active,events}) {
// handles fetching the lvMenu
if (active) {
if (!this.lvMenu) {
this.fetchLvMenu(this.selectedEvent);
}
if(events){
// if no event is selected, select the first event of the day
if (this.selectedEvent == null && this.events[this.day.toDateString()]?.length > 0) {
let events = this.events[this.day.toDateString()].sort((a,b)=>{
let [a_stunde,a_minute,a_sekunden]= a.beginn.split(":");
let [b_stunde, b_minute, b_sekunden] = b.beginn.split(":");
a_stunde = Number(a_stunde);
a_minute = Number(a_minute);
a_sekunden= Number(a_sekunden);
b_stunde = Number(b_stunde);
b_minute = Number(b_minute);
b_sekunden = Number(b_sekunden);
if(a_stunde > b_stunde){
return 1;
}
else if(b_stunde > a_stunde){
return -1;
}
else if(a_minute > b_minute){
return 1;
}
else if (b_minute > a_minute){
return -1;
}
else{
return a_sekunden > b_sekunden? 1:-1;
}
});
if (Array.isArray(events) && events.length > 0) {
this.setSelectedEvent(events[0]);
}
}
}
}
},
immediate: true,
},
selectedEvent: {
handler(event) {
// return early if the day-page component is not the active carousel item
if (!this.active) {
return;
}
this.lvMenu = null;
this.fetchLvMenu(event);
},
immediate: true,
},
isSliding: {
handler(value) {
if (value) {
this.setSelectedEvent(null);
}
}
}
},
computed: {
activeAndEventsReady(){
return {
active: this.active,
events: this.events,
}
},
allDayEvents() {
let allDayEvents = {};
for (let day in this.events) {
const filteredAllDayEvents = this.events[day].filter(event => event.allDayEvent);
if (filteredAllDayEvents.length > 0) {
allDayEvents[day] = filteredAllDayEvents;
}
};
return allDayEvents;
},
overlayStyle() {
return {
height: this.getDayTimePercent + '%',
}
},
pageHeaderStyle() {
return {
'z-index': 4,
'grid-template-columns': 'repeat(' + this.day.length + ', 1fr)',
'grid-template-rows': 1,
position: 'sticky',
top: 0,
}
},
dayText(){
if(!this.day)return {};
return {
heading: this.day.toLocaleString(this.$p.user_locale.value, { dateStyle: 'short' }),
tag: this.day.toLocaleString(this.$p.user_locale.value, { weekday: this.size < 2 ? 'narrow' : (this.size < 3 ? 'short' : 'long') }),
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]),
}
},
noLvStyle() {
return {
top: (this.calendarScrollTop + 100) + 'px',
position: 'absolute',
left: 0,
'text-align': 'center',
width: '100%',
'z-index': 1,
}
},
indicatorStyle() {
return {
'pointer-events': 'none',
'padding-left': '3.5rem',
'margin-top': '-1px',
'z-index': 2,
'border-color': 'var(--fhc-border)',
top: this.hourPosition + 'px',
left: 0,
right: 0,
}
},
curTime() {
const now = new Date();
return String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');
},
curIndicatorStyle() {
return {
top: this.getDayTimePercent + '%',
}
},
noEventsCondition() {
return !this.isSliding && (this.filteredEvents?.length === 0 || !this.filteredEvents);
},
hours() {
// returns an array with elements starting at 7 and ending at 24
return [...Array(24).keys()].filter(hour => hour >= 7 && hour <= 24);
},
day() {
return new Date(this.focusDate.y, this.focusDate.m, this.focusDate.d);
},
eventsPerDayAndHour() {
// return early if the calendar pane is sliding
if (this.isSliding) return {};
const res = {};
let key = this.day.toDateString();
let nextDay = new Date(this.day);
nextDay.setDate(nextDay.getDate() + 1);
nextDay.setMilliseconds(nextDay.getMilliseconds() - 1);
let d = {events: [], lanes: 1};
if (this.events[key]) {
this.events[key].forEach(evt => {
if (evt.allDayEvent) return;
let event = {
orig: evt,
lane: 1,
maxLane: 1,
start: evt.start < this.day ? this.day : evt.start,
end: evt.end > nextDay ? nextDay : evt.end,
shared: [],
setSharedMaxRecursive(doneItems) {
this.maxLane = Math.max(doneItems[0].maxLane, this.maxLane);
doneItems.push(this);
this.shared.filter(other => !doneItems.includes(other)).forEach(i => i.setSharedMaxRecursive(doneItems));
}
};
event.shared = d.events.filter(other => other.start < event.end && other.end > event.start);
event.shared.forEach(other => other.shared.push(event));
let occupiedLanes = event.shared.map(other => other.lane);
while (occupiedLanes.includes(event.lane))
event.lane++;
event.maxLane = Math.max(...[event.lane], ...occupiedLanes);
if (event.maxLane > 1) {
event.setSharedMaxRecursive([event]);
}
d.events.push(event);
});
d.lanes = d.events.map(e => e.maxLane).reduce((res, i) => kgv(res, i), 1);
}
res[key] = d;
return res;
},
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: {
dayScrollBehavior(event){
this.$refs.dayScrollContainer?.scrollBy({ top: Math.sign(event.deltaY) * 100, behavior: 'instant' });
},
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, var(--calendar-past) '+this.getDayTimePercent+'%, transparent '+this.getDayTimePercent+'%)'
// styleObj.opacity = 0.5; // would opaque the whole column
}
return styleObj
},
fetchLvMenu(event) {
if (event && event.type == 'lehreinheit') {
this.$api
.call(ApiLvPlan.getLehreinheitStudiensemester(event.lehreinheit_id[0]))
.then(res => res.data)
.then(studiensemester_kurzbz => this.$api.call(
ApiAddons.getLvMenu(
event.lehrveranstaltung_id,
studiensemester_kurzbz
)
))
.then(res => {
if (res.data) {
this.lvMenu = res.data;
}
});
}
},
hourGridIdentifier(hour) {
// this is the id attribute that is responsible to scroll the calender to the first event
return 'scroll' + hour + this.focusDate.d + this.week;
},
hourGridStyle(hour) {
return {
'pointer-events': 'none',
top: this.getAbsolutePositionForHour(hour),
left: 0,
right: 0,
'z-index': 0,
}
},
eventGridStyle(day, event) {
return {
'z-index': 1,
'grid-column-start': 1 + (event.lane - 1) * day.lanes / event.maxLane,
'grid-column-end': 1 + event.lane * day.lanes / event.maxLane,
'grid-row-start': this.dateToMinutesOfDay(event.start),
'grid-row-end': this.dateToMinutesOfDay(event.end),
'background-color': event.orig.color,
'--test': this.dateToMinutesOfDay(event.end),
}
},
eventClick(evt) {
let event = evt.orig || evt;
this.setSelectedEvent(event);
this.$emit('input', event);
},
calcHourPosition(event) {
let height = this.$refs.events.getBoundingClientRect().height;
let top = this.$refs.events.getBoundingClientRect().top;
let position = event.clientY - top;
// position percentage of total height
let timePercentage = (position / height) * 100;
// minute percentage of total minutes
let result = (this.hours.length * 60) * (timePercentage / 100);
// calculate time in float
let currentMinutes = ((result + (this.hours[0] * 60)) / 60);
// get hour part of time
let currentHour = Math.floor(currentMinutes);
// get float part of time
let minutePercentage = currentMinutes % currentHour;
// calculate minutes from float part of time
let minute = Math.round(60 * minutePercentage);
// convert minutes to 5 minute interval
if (minute % 5 != 0) {
minute = Math.round(minute / 5) * 5;
}
// in case the rounding made the minutes 60, increase the hour and reset the minutes
if (minute == 60) {
currentHour++;
minute = 0;
}
// ## after rounding the time to the nearest 5 Minute interval, we have to convert the time back to the relative position
// convert current time in minutes
currentMinutes = currentHour * 60 + minute;
// 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
position = height * (timePercentage / 100);
this.hourPosition = position;
// add padding to minutes that consist of only one digit
minute.toString().length == 1 ? minute = "0" + minute : minute;
this.hourPositionTime = currentHour + ":" + minute;
},
getAbsolutePositionForHour(hour) {
// used for the absolute positioning of the gutters of hours
return (100 / this.hours.length) * (hour - (24 - this.hours.length)) + '%';
},
changeToMonth(day) {
if (!this.noMonthView) {
this.date.set(day);
this.focusDate.set(day);
this.$emit('updateMode', 'month');
}
},
dateToMinutesOfDay(day) {
return Math.floor(((day.getHours() - 7) * 60 + day.getMinutes()) / this.smallestTimeFrame) + 1;
}
},
template: /*html*/`
<div class="fhc-calendar-day-page h-100">
<div class="row m-0 h-100">
<div style="overflow:auto" class="col-12 col-xl-6 p-0 h-100">
<div class="d-flex flex-column h-100 border">
<div ref="header" class="fhc-calendar-week-page-header d-grid border-2 border-bottom text-center" :style="pageHeaderStyle">
<div type="button" class="flex-grow-1" :title="dayText.heading" @click.prevent="changeToMonth(day)">
<div class="day fw-bold">{{dayText.tag}}</div>
<a href="#" class="small date text-decoration-none" >{{dayText.datum}}</a>
</div>
</div>
<div @wheel.prevent="dayScrollBehavior" id="scrollContainer" ref="dayScrollContainer" style="height: 100%; overflow-y: scroll;">
<div ref="eventcontainer" class="position-relative flex-grow-1" >
<div class="all-day-event-container" >
<div @wheel.stop class="all-day-event all-day-event-border" v-for="(day,dayindex) in eventsPerDayAndHour">
<template v-for="(events,_day) in allDayEvents" :key="_day">
<div
v-if="dayindex == _day"
v-for="event in events"
:key="event"
style="top:0;"
@click.prevent="eventClick(event)"
:selected="event == selectedEvent"
:style="{'background-color': event?.color, 'margin-bottom':'1px'}"
class="d-grid m-1 small rounded overflow-hidden fhc-entry"
v-contrast
>
<div class="d-none d-xl-block">
<slot name="dayPage" :event="event" :day="day" :mobile="false">
<p>this is a placeholder which means that no template was passed to the Calendar Page slot</p>
</slot>
</div>
<div class="d-block d-xl-none">
<slot name="dayPage" :event="event" :day="day" :mobile="true">
<p>this is a placeholder which means that no template was passed to the Calendar Page slot</p>
</slot>
</div>
</div>
</template>
</div>
</div>
<div >
<h1 v-if="noEventsCondition" class="m-0 bs-body" ref="noEventsText" :style="noLvStyle">{{ $p.t('lehre/noLvFound') }}</h1>
<div class="events position-relative" :class="{'fhc-calendar-no-events-overlay':noEventsCondition}" ref="events" @mousemove="calcHourPosition" @mouseleave="hourPosition = null">
<Transition>
<div v-if="hourPosition && !noEventsCondition" class="position-absolute border-top small" :style="indicatorStyle">
<span class="border border-top-0 px-2">{{hourPositionTime}}</span>
</div>
</Transition>
<Transition>
<div v-if="lookingAtToday && !noEventsCondition" class="curTimeIndicator border-top small" :style="curIndicatorStyle">
<span class="border border-top-0 px-2">{{curTime}}</span>
</div>
</Transition>
<div :id="hourGridIdentifier(hour)" v-for="hour in hours" :key="hour" class="position-absolute box-shadow-border" :style="hourGridStyle(hour)"></div>
<div class="hours">
<div v-for="hour in hours" :style="'min-height:' + rowMinHeight " :key="hour" class="text-end small" :ref="'hour' + hour">{{hour}}:00</div>
</div>
<div v-for="(day,dayindex) in eventsPerDayAndHour" :key="day" class=" day border-start" :style="dayGridStyle(day)">
<div v-if="lookingAtToday && !noEventsCondition" class="overlay" :style="overlayStyle"></div>
<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 border border-secondary 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)">
<slot name="dayPage" :event="event.orig" :day="day" :mobile="false">
<p>this is a slot placeholder</p>
</slot>
</div>
<!-- mobile version of the page template, parent receives slotProp mobile = true -->
<div class="d-block d-xl-none h-100 " @click.prevent="eventClick(event)">
<slot name="dayPage" :event="event.orig" :day="day" :mobile="true">
<p>this is a slot placeholder</p>
</slot>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="d-xl-block col-xl-6 p-4 d-none" style="max-height: 100%">
<div @wheel.stop style="max-height: 100%; overflow-y:auto;" class="w-100">
<template v-if="selectedEvent ">
<slot name="pageMobilContent" :event="selectedEvent" :lvMenu="lvMenu" >
<p>this is a slot placeholder</p>
</slot>
</template>
<template v-else-if="noEventsCondition">
<h3>Keine Lehrveranstaltungen</h3>
</template>
<template v-else>
<div class="p-4 d-flex w-100 justify-content-center align-items-center">
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>
</template>
</div>
</div>
</div>
</div>
`
}
-99
View File
@@ -1,99 +0,0 @@
export default {
data(){
return{
selected: this.mode,
modes:{
day: { mode_bezeichnung: "day", icon: "fa-calendar-day" , condition:true},
week: { mode_bezeichnung: "week", icon: "fa-calendar-week", condition: !this.noWeekView },
month: { mode_bezeichnung: "month", icon: "fa-calendar-days", condition: !this.noMonthView },
},
headerPadding:null,
}
},
inject: [
'eventsAreNull',
'size',
'mode',
'noWeekView',
'noMonthView',
'containerWidth'
],
props: {
title: String
},
emits: [
'updateMode',
'prev',
'next',
'click'
],
methods:{
modeAriaLabelText(mode) {
switch (mode.toLowerCase()) {
case "day": return this.$p.t('LvPlan', 'modeDay');
case "week": return this.$p.t('LvPlan', 'modeWeek');
case "month": return this.$p.t('LvPlan', 'modeMonth');
}
},
},
computed: {
getHeaderClassSide() {
return this.containerWidth > 780 ? 'col-3' : 'col-12'
},
getHeaderClassMiddle() {
return this.containerWidth > 780 ? 'col-6' : 'col-12'
},
previousButtonAriaLabelText(){
switch(this.mode.toLowerCase()){
case "day": return this.$p.t('LvPlan', 'previousDay');
case "week": return this.$p.t('LvPlan', 'previousWeek');
case "weeks": return this.$p.t('LvPlan', 'previousYear');
case "month": return this.$p.t('LvPlan', 'previousMonth');
}
},
nextButtonAriaLabelText() {
switch (this.mode.toLowerCase()) {
case "day": return this.$p.t('LvPlan', 'nextDay');
case "week": return this.$p.t('LvPlan', 'nextWeek');
case "weeks": return this.$p.t('LvPlan', 'nextYear');
case "month": return this.$p.t('LvPlan', 'nextMonth');
}
},
},
template: /*html*/`
<div class="calendar-header card-header w-100">
<div class="row align-items-center ">
<div :class="getHeaderClassSide" class="d-flex justify-content-center justify-content-md-start align-items-center">
<slot name="calendarDownloads"></slot>
</div>
<div :class="getHeaderClassMiddle" :style="{'padding-left':headerPadding}">
<div class="row align-items-center justify-content-center">
<div class="col-auto p-2">
<button class="btn btn-outline-secondary border-0" :class="{'btn-sm':!this.size}" @click="$emit('prev')" v-tooltip.top="{ value: previousButtonAriaLabelText, showDelay: 1000}" :aria-label="previousButtonAriaLabelText"><i class="fa fa-chevron-left" aria-hidden="true"></i></button>
</div>
<div class="justify-content-center text-center col-auto">
<div class="d-flex justify-content-center align-items-center">
<button id="calendarHeaderTitle" class="btn btn-link text-decoration-none" :class="{'btn-sm': !this.size}" @click="$emit('click')">
{{ title }}
<i v-if="eventsAreNull" class="fa fa-spinner fa-pulse"></i>
</button>
</div>
</div>
<div class="col-auto p-2">
<button class="btn btn-outline-secondary border-0" :class="{'btn-sm': !this.size}" @click="$emit('next')" v-tooltip.top="{value:nextButtonAriaLabelText, showDelay:1000}" :aria-label="previousButtonAriaLabelText"><i class="fa fa-chevron-right" aria-hide="true"></i></button>
</div>
</div>
</div>
<div ref="viewButtons" v-if="!noWeekView && !noMonthView" :class="getHeaderClassSide" class="d-flex justify-content-center justify-content-md-end align-items-center" style="pointer-events: none;">
<div style="pointer-events: all;">
<div role="group" aria-label="Kalender Modus">
<button :aria-label="modeAriaLabelText(mode_bezeichnung)" v-tooltip.top="{value:modeAriaLabelText(mode_bezeichnung), showDelay:1000}" type="button" :class="{'active':mode_kurzbz.toLowerCase() === mode.toLowerCase()}" style="margin-right: 4px;" @click.prevent="$emit('updateMode',mode_kurzbz)" class="btn btn-outline-secondary card d-inline-block" v-for="({mode_bezeichnung,icon,condition},mode_kurzbz) in modes">
<i aria-hidden="true" v-if="condition" class="fa" :class="icon" ></i>
</button>
</div>
</div>
</div>
</div>
</div>`
}
@@ -1,26 +0,0 @@
import CalendarAbstract from './Abstract.js';
export default {
mixins: [
CalendarAbstract
],
inject: [
'size',
'minimized',
'date',
],
data() {
return {
start: 0
}
},
methods: {
maximize() {
this.minimized = false;
}
},
template: `
<div class="fhc-calendar-minimized h-100 d-flex flex-column">
<slot name="minimizedPage"></slot>
</div>`
}
-103
View File
@@ -1,103 +0,0 @@
import CalendarAbstract from './Abstract.js';
import CalendarPane from './Pane.js';
import CalendarMonthPage from './Month/Page.js';
import BsModal from "../Bootstrap/Modal.js";
import Months from "./Months.js";
export default {
mixins: [
CalendarAbstract
],
components: {
CalendarMonthPage,
CalendarPane,
BsModal,
Months
},
emits: [
"change:offset"
],
data() {
return {
syncOnNextChange: false
}
},
computed: {
title() {
return this.focusDate.format({month: ['short','long','long','long'][this.size], year: 'numeric'}, this.$p.user_locale.value);
}
},
methods: {
handleMonthChanged(month) {
this.$emit('change:offset', { y: 0, m: month - this.focusDate.m, d: 0 });
this.$refs.modalDatepickerContainer.hide()
},
hideMonthsModal() {
this.$refs.modalDatepickerContainer.hide()
},
handleHeaderClickMonth() {
this.$refs.modalDatepickerContainer.show()
},
paneChanged(dir) {
if (this.syncOnNextChange) {
this.syncOnNextChange = false;
this.focusDate.set(this.date);
} else {
this.focusDate.moveMonthInDirection(dir)
}
this.emitRangeChanged()
},
emitRangeChanged(mounted = false) {
this.$emit('change:range', {
start: new Date(this.focusDate.y, this.focusDate.m, 1),
end: new Date(this.focusDate.y, this.focusDate.m+1, 0),
mounted
});
},
prev() {
this.$refs.pane.prev();
this.$emit('change:offset', { y: 0, m: -1, d: 0 });
},
next() {
this.$refs.pane.next();
this.$emit('change:offset', { y: 0, m: 1, d: 0 });
},
selectDay(day) {
let m = day.getMonth();
if (this.focusDate.m != m) {
this.syncOnNextChange = true;
if (this.focusDate.m-1 == m || (m == 11 && !this.focusDate.m))
this.$refs.pane.prev();
else
this.$refs.pane.next();
} else {
this.focusDate.set(this.date);
}
this.$emit('input', ['select:day',day])
}
},
mounted() {
this.emitRangeChanged(true)
},
template: `
<div class="fhc-calendar-month">
<calendar-header :title="title" @prev="prev" @next="next" @updateMode="$emit('updateMode', $event)" @click="handleHeaderClickMonth">
<template #calendarDownloads>
<slot name="calendarDownloads"></slot>
</template>
</calendar-header>
<calendar-pane ref="pane" v-slot="slot" @slid="paneChanged">
<calendar-month-page :year="focusDate.y" :month="focusDate.m+slot.offset" @updateMode="$emit('updateMode', $event)" @page:back="prev" @page:forward="next" @input="selectDay" >
<template #monthPage="{event,day}">
<slot name="monthPage" :event="event" :day="day" ></slot>
</template>
</calendar-month-page>
</calendar-pane>
</div>
<bs-modal ref="modalDatepickerContainer" dialogClass='modal-lg' class="bootstrap-prompt">
<template v-slot:default>
<months @change="handleMonthChanged"></months>
</template>
</bs-modal>
`
}
-170
View File
@@ -1,170 +0,0 @@
import CalendarDate from '../../../composables/CalendarDate.js';
export default {
name: 'MonthPage',
data(){
return{
highlightedWeek: null,
highlightedDay: null,
}
},
inject: [
'today',
'todayDate',
'date',
'focusDate',
'size',
'events',
'showWeeks',
'noWeekView',
'selectedEvent',
'setSelectedEvent'
],
props: {
year: Number,
month: Number
},
emits: [
'updateMode',
'page:back',
'page:forward',
'input'
],
computed: {
dayText(){
if (!this.size || !this.weeks[0]?.days) return {};
let dayTextMap ={};
this.weeks[0].days.forEach((day)=>{
dayTextMap[day] = day.toLocaleString(this.$p.user_locale.value, { weekday: this.size < 1 ? 'narrow' : (this.size < 3 ? 'short' : 'long') });
});
return dayTextMap;
},
weeks() {
let firstDayOfMonth = new CalendarDate(this.year, this.month, 1);
let startDay = firstDayOfMonth.firstDayOfCalendarMonth;
let endDay = firstDayOfMonth.lastDayOfCalendarMonth;
let res = [];
let week = {no:0,y:0,days:[]};
while (startDay <= endDay) {
week.days.push(new Date(startDay));
if (week.days.length == 7) {
let d = new CalendarDate(week.days[5]);
week.no = d.w;
week.y = d.y;
res.push(week);
week = {no:0,y:0,days:[]};
}
startDay.setDate(startDay.getDate() + 1);
}
return res;
},
},
methods: {
getDayClass(week, day) {
let classstring = 'fhc-calendar-month-page-day text-decoration-none overflow-hidden'
const isHighlightedWeek = this.isHighlightedWeek(week)
const isHighlightedDay = this.isHighlightedDay(day)
const isThisDate = this.focusDate.compare(day)
const isNotThisMonth = day.getMonth() != this.month
const isInThePast = day.getTime() < this.today // this.date is just the focusDate but not the initial Date
if(isThisDate) classstring += ' fhc-calendar-month-page-day-focusday'
if(isHighlightedWeek) classstring += ' fhc-highlight-week'
if(isHighlightedDay) classstring += ' fhc-highlight-day'
if(isNotThisMonth) classstring += ' opacity-25'
if(isInThePast) classstring += ' fhc-calendar-past'
return classstring
},
selectDay(day, event) {
this.setSelectedEvent(event);
this.date.set(day);
this.$emit('input', day);
},
changeToWeek(week) {
if (!this.noWeekView) {
if (!this.focusDate.isInWeek(week.no, week.y))
this.focusDate.set(week.days[0]);
this.$emit('updateMode', 'week');
}
},
highlight(week, day){
this.highlightedWeek = week.no;
this.highlightedDay = day;
},
isHighlightedDay(day) {
return day == this.highlightedDay
},
isHighlightedWeek(week) {
return week.no == this.highlightedWeek
},
clickEvent(day,week) {
if(!this.noWeekView)
{
this.focusDate.set(day);
this.$emit('updateMode', 'day');
}
this.selectDay(day);
},
getNumberStyle(day) {
const styleObj = {}
styleObj.display = 'inline-block';
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'] = 'var(--fhc-primary)';
styleObj.color = 'white';
}
return styleObj
}
},
mounted() {
const container = document.getElementById("calendarContainer")
if(container) container.style['overflow-y'] = 'auto'
},
template: /*html*/`
<div class="fhc-calendar-month-page" :class="{'show-weeks': showWeeks}">
<div v-if="showWeeks" class=" fw-bold border-top border-bottom text-center"></div>
<div v-for="day in weeks[0].days" :key="day" class=" fw-bold border-top border-bottom text-center">
{{dayText[day]}}
</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 "
@click.prevent="changeToWeek(week)">{{week.no}}</a>
<a href="#"
@click.prevent="clickEvent(day,week)"
@mouseover="highlight(week,day)"
@mouseleave="highlightedWeek = null; highlightedDay = null"
v-for="day in week.days"
:key="day"
:class="getDayClass(week, day)"
>
<span @click="clickEvent(day,week)" class="no" :style="getNumberStyle(day)">{{day.getDate()}}</span>
<span v-if="events[day.toDateString()] && events[day.toDateString()].length" class="events">
<div v-for="event in events[day.toDateString()]" :key="event.id"
:style="{'background-color': event.color}" class="fhc-entry" :selected="event == selectedEvent"
v-contrast @click.stop="selectDay(day,event)">
<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>
</div>
</span>
</a>
</template>
</div>
`
}
-37
View File
@@ -1,37 +0,0 @@
import CalendarAbstract from './Abstract.js';
export default {
mixins: [
CalendarAbstract
],
emits: [
'change'
],
inject: [
'size'
],
data() {
return {
// TODO: 36, 24, 16 (2+ 12 + 2) months to enable year switch?
monthIndices: [...Array(12).keys()]
}
},
computed: {
title() {
return this.focusDate.format({year: 'numeric'});
},
months() {
return this.monthIndices.map(i => (new Date(0, i, 1)).toLocaleString(this.$p.user_locale.value, {month: this.size < 2 ? 'short' : 'long'}));
}
},
template: `
<div class="fhc-calendar-months">
<div class="d-flex flex-wrap">
<div v-for="(month, key) in months" :key="key" class="d-grid col-4">
<button @click=" $emit('change', key); focusDate.m = key;" class="btn btn-outline-secondary" :class="{'border-0': key != focusDate.m}">
{{month}}
</button>
</div>
</div>
</div>`
}
-94
View File
@@ -1,94 +0,0 @@
export default {
name: 'Pane',
emits: [
'slid'
],
data() {
return {
carousel: null,
queue: 0,
offset: 0,
slideAnimation:false,
scrollTop:null,
clientHeight:null,
carouselItems:null,
}
},
provide() {
return {
isSliding: Vue.computed(() => this.slideAnimation),
calendarScrollTop: Vue.computed(() =>this.scrollTop),
calendarClientHeight: Vue.computed(() => this.clientHeight),
}
},
computed: {
offsets() {
return [...Array(3).keys()].map(i => (3+i-this.offset)%3-1);
},
activeCarouselItemIndex() {
if (Array.isArray(this.carouselItems) && this.carouselItems.length > 0) {
for(let index=0; index < this.carouselItems.length; index++){
if (this.carouselItems[index] == true){
return index;
}
}
}
return -1;
}
},
methods: {
scrollBehavior(event){
this.$refs.calendarContainer?.scrollBy({ top: Math.sign(event.deltaY) * 100, behavior: 'instant' });
},
scrollCalendar(event){
this.scrollTop = this.$refs.calendarContainer.scrollTop;
this.clientHeight = this.$refs.calendarContainer.clientHeight;
},
prev() {
if (!this.queue--)
this.carousel.prev();
},
next() {
if (!this.queue++)
this.carousel.next();
},
slid(evt) {
let dir = evt.direction == 'left' ? 1 : -1;
this.queue -= dir;
this.$emit('slid', dir);
this.offset = (3+this.offset+dir)%3;
if (this.queue) {
if (this.queue > 0)
this.carousel.next();
else
this.carousel.prev();
}
this.carouselItems = this.$refs.carouselItems.map((item) => { return item.classList.contains('active') });
this.slideAnimation = false;
},
slide(evt) {
this.slideAnimation = true;
}
},
mounted() {
if (this.$refs.carousel) {
this.$refs.carousel.children[0].children[1].classList.add('active');
this.carousel = new window.bootstrap.Carousel(this.$refs.carousel, {
interval: false
});
}
this.carouselItems = this.$refs.carouselItems.map((item)=>{return item.classList.contains('active')});
this.scrollTop = this.$refs.calendarContainer.scrollTop;
this.clientHeight = this.$refs.calendarContainer.clientHeight;
},
template: `
<div ref="carousel" class="calendar-pane carousel slide" @[\`slide.bs.carousel\`]="slide" @[\`slid.bs.carousel\`]="slid" :data-queue="queue">
<div id="calendarContainer" @wheel.prevent="scrollBehavior" @scroll="scrollCalendar" ref="calendarContainer" class="carousel-inner fhc-calendar-pane">
<div ref="carouselItems" v-for="i in [...Array(3).keys()]" :key="i" class="carousel-item fhc-calendar-pane">
<slot :active="i == activeCarouselItemIndex" :index="i" :offset="offsets[i]" />
</div>
</div>
</div>`
}
-83
View File
@@ -1,83 +0,0 @@
import CalendarAbstract from './Abstract.js';
import CalendarPane from './Pane.js';
import CalendarWeekPage from './Week/Page.js';
import BsModal from "../Bootstrap/Modal.js";
import Weeks from "./Weeks.js";
export default {
mixins: [
CalendarAbstract
],
components: {
CalendarWeekPage,
CalendarPane,
BsModal,
Weeks
},
emits: [
"change:offset"
],
computed: {
title() {
return this.focusDate.wYear + ' KW ' + this.focusDate.w;
}
},
methods: {
handleWeekChanged(week) {
// this.$emit('change:offset', { y: 0, m: month - this.focusDate.m, d: 0 });
this.$refs.modalDatepickerContainer.hide()
},
hideMonthsModal() {
this.$refs.modalDatepickerContainer.hide()
},
handleHeaderClickWeek() {
this.$emit('updateMode', 'weeks');//
//this.$refs.modalDatepickerContainer.show()
},
paneChanged(dir) {
this.focusDate.d += dir * 7;
this.emitRangeChanged();
},
emitRangeChanged(mounted = false) {
let start = this.focusDate.firstDayOfWeek;
let end = this.focusDate.lastDayOfWeek;
this.$emit('change:range', { start, end, mounted });
},
prev() {
this.$refs.pane.prev();
this.$emit('change:offset', { y: 0, m: 0, d: -7 });
},
next() {
this.$refs.pane.next();
this.$emit('change:offset', { y: 0, m: 0, d: 7 });
},
selectEvent(event) {
this.$emit('input', ['select:event',event]);
}
},
mounted() {
this.emitRangeChanged(true);
},
template: /*html*/`
<div class="fhc-calendar-week">
<calendar-header :title="title" @prev="prev" @next="next" @updateMode="$emit('updateMode', $event)" @click="handleHeaderClickWeek">
<template #calendarDownloads>
<slot name="calendarDownloads"></slot>
</template>
</calendar-header>
<calendar-pane ref="pane" v-slot="slot" @slid="paneChanged">
<calendar-week-page :active="slot.active" :year="focusDate.wYear" :week="focusDate.w+slot.offset" @updateMode="$emit('updateMode', $event)" @page:back="prev" @page:forward="next" @input="selectEvent" >
<template #weekPage="{event,day}">
<slot name="weekPage" :event="event" :day="day" ></slot>
</template>
</calendar-week-page>
</calendar-pane>
</div>
<bs-modal ref="modalDatepickerContainer" dialogClass='modal-lg' class="bootstrap-prompt">
<template v-slot:default>
<weeks :header="false" @change="handleWeekChanged"></weeks>
</template>
</bs-modal>
`
}
-387
View File
@@ -1,387 +0,0 @@
import CalendarDate from '../../../composables/CalendarDate.js';
function ggt(m,n) { return n==0 ? m : ggt(n, m%n); }
function kgv(m,n) { return (m*n) / ggt(m,n); }
export default {
name: 'WeekPage',
data(){
return{
hourPosition:null,
hourPositionTime:null,
resizeObserver: null,
width: 0,
}
},
inject: [
'today',
'todayDate',
'date',
'focusDate',
'size',
'events',
'noMonthView',
'isSliding',
'selectedEvent',
'setSelectedEvent',
'rowMinHeight'
],
props: {
year: Number,
week: Number,
active: Boolean
},
emits: [
'updateMode',
'page:back',
'page:forward',
'input',
],
computed: {
allDayEvents(){
let allDayEvents = {};
for(let day in this.events){
const filteredAllDayEvents = this.events[day].filter(event=>event.allDayEvent);
if (filteredAllDayEvents.length > 0){
allDayEvents[day]=filteredAllDayEvents;
}
};
return allDayEvents;
},
getGridStyle() {
return {
'min-height': this.rowMinHeight,
// this.size is the magic number anyway which directs font-size,
// which in turn influences a lot of layout
width: '42px'
}
},
laneWidth() {
return (this.width - 42) / this.days.length
},
curTime() {
const now = new Date();
return String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');
},
curIndicatorStyle() {
return {
top: this.getDayTimePercent + '%',
}
},
pageHeaderStyle(){
return {
'z-index': 4,
'grid-template-columns': 'repeat(' + this.days.length + ', 1fr)',
'grid-template-rows': 1,
position: 'sticky',
top: 0,
}
},
overlayStyle() {
return {
height: this.getDayTimePercent + '%',
width: this.laneWidth + 'px',
}
},
hours(){
// returns an array with elements starting at 7 and ending at 24
return [...Array(24).keys()].filter(hour => hour >= 7 && hour <= 24);
},
dayText() {
if (!this.size || !this.days) return {};
let dayTextMap ={};
this.days.forEach((day)=>{
dayTextMap[day] = {
heading: day.toLocaleString(this.$p.user_locale.value, { dateStyle: 'short' }),
tag: day.toLocaleString(this.$p.user_locale.value, { weekday: this.size < 2 ? 'narrow' : (this.size < 3 ? 'short' : 'long') }),
datum: day.toLocaleString(this.$p.user_locale.value, [{ day: 'numeric', month: 'numeric' }, { day: 'numeric', month: 'numeric' }, { day: 'numeric', month: 'numeric' }, { dateStyle: 'short' }][this.size]),
};
});
return dayTextMap;
},
days() {
let tmpDate = new CalendarDate(this.year,1,1); // NOTE(chris): somewhere in the middle of the year
tmpDate.w = this.week;
let startDay = tmpDate.firstDayOfWeek;
let result = [];
for (let i = 0; i < 7; i++) {
result.push(new Date(startDay.getFullYear(), startDay.getMonth(), startDay.getDate() + i));
}
return result;
},
eventsPerDayAndHour() {
// return early if the calendar pane is sliding
if (this.isSliding) return {};
const res = {};
this.days.forEach(day => {
let key = day.toDateString();
let nextDay = new Date(day);
nextDay.setDate(nextDay.getDate()+1);
nextDay.setMilliseconds(nextDay.getMilliseconds()-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 => {
if (evt.allDayEvent) return;
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) {
this.maxLane = Math.max(doneItems[0].maxLane, this.maxLane);
doneItems.push(this);
this.shared.filter(other => !doneItems.includes(other)).forEach(i => i.setSharedMaxRecursive(doneItems));
}};
event.shared = d.events.filter(other => other.start < event.end && other.end > event.start);
event.shared.forEach(other => other.shared.push(event));
let occupiedLanes = event.shared.map(other => other.lane);
while (occupiedLanes.includes(event.lane))
event.lane++;
event.maxLane = Math.max(...[event.lane], ...occupiedLanes);
if (event.maxLane > 1) {
event.setSharedMaxRecursive([event]);
}
d.events.push(event);
});
d.lanes = d.events.map(e => e.maxLane).reduce((res, i) => kgv(res, i), 1);
}
res[key] = d;
});
return res;
},
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()
)
},
indicatorStyle() {
return {
'pointer-events': 'none',
'padding-left': '3.5rem',
'margin-top': '-1px',
'z-index': 2,
'border-color': 'var(--fhc-border)',
top: this.hourPosition + 'px',
left: 0,
right: 0,
}
},
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: {
hourGridIdentifier(hour) {
// this is the id attribute that is responsible to scroll the calender to the first event
return 'scroll' + hour + this.focusDate.d + this.week;
},
hourGridStyle(hour) {
return {
'pointer-events': 'none',
top: this.getAbsolutePositionForHour(hour),
left: 0,
right: 0,
'z-index': 0
}
},
dayGridStyle(day) {
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'] = 'var(--calendar-past)'
styleObj['border-color'] = 'var(--fhc-border)';
} else if (day.isToday) {
//styleObj['backgroundImage'] = 'linear-gradient(to bottom, var(--calendar-past) '+this.getDayTimePercent+'%, transparent '+this.getDayTimePercent+'%)'
styleObj['border-color'] = 'var(--fhc-border)';
}
return styleObj
},
eventGridStyle(day, event) {
return {
'z-index': 1,
'grid-column-start': 1 + (event.lane - 1) * day.lanes / event.maxLane,
'grid-column-end': 1 + event.lane * day.lanes / event.maxLane,
'grid-row-start': this.dateToMinutesOfDay(event.start),
'grid-row-end': this.dateToMinutesOfDay(event.end),
'background-color': event.orig.color,
'max-height': '75px'
};
},
calcHourPosition(event) {
let height = this.$refs['eventsRef' + this.week].getBoundingClientRect().height;
let top = this.$refs['eventsRef' + this.week].getBoundingClientRect().top;
let position = event.clientY - top;
// position percentage of total height
let timePercentage = (position / height) * 100;
// minute percentage of total minutes
let result = (this.hours.length * 60) * (timePercentage / 100);
// calculate time in float
let currentMinutes = ((result + (this.hours[0] * 60)) / 60);
// get hour part of time
let currentHour = Math.floor(currentMinutes);
// get float part of time
let minutePercentage = currentMinutes % currentHour;
// calculate minutes from float part of time
let minute = Math.round(60 * minutePercentage);
// convert minutes to 5 minute interval
if (minute % 5 != 0) {
minute = Math.round(minute / 5) * 5;
}
// in case the rounding made the minutes 60, increase the hour and reset the minutes
if (minute == 60) {
currentHour++;
minute = 0;
}
// ## after rounding the time to the nearest 5 Minute interval, we have to convert the time back to the relative position
// convert current time in minutes
currentMinutes = currentHour * 60 + minute;
// 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
position = height * (timePercentage / 100);
this.hourPosition = position;
// add padding to minutes that consist of only one digit
minute.toString().length == 1 ? minute = "0" + minute : minute;
this.hourPositionTime = currentHour + ":" + minute;
},
getAbsolutePositionForHour(hour){
// used for the absolute positioning of the gutters of hours
return (100 / this.hours.length) * (hour - (24-this.hours.length)) + '%';
},
changeToMonth(day) {
if (!this.noMonthView) {
this.date.set(day);
this.focusDate.set(day);
this.$emit('updateMode', 'month');
}
},
changeToDay(day) {
this.date.set(day);
this.focusDate.set(day);
this.$emit('updateMode', 'day');
},
dateToMinutesOfDay(day) {
// subtract 7 from the total hours because the hours range from 7 to 24
return Math.floor(((day.getHours()-7) * 60 + day.getMinutes()) / this.smallestTimeFrame) + 1;
},
weekPageClick(event, day) {
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-y'] = 'scroll'
container.style['overflow-x'] = 'auto'
}
this.initResizeObserver();
},
beforeUnmount() {
this.destroyResizeObserver();
},
template: /*html*/`
<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="changeToDay(day)">
<div class="day fw-bold">{{dayText[day]?.tag}}</div>
<a href="#" class="date small text-decoration-none" >{{dayText[day]?.datum}}</a>
</div>
</div>
<div ref="eventcontainer" class="position-relative flex-grow-1" >
<div class="all-day-event-container" >
<div @wheel.stop class="all-day-event all-day-event-border" v-for="(day,dayindex) in eventsPerDayAndHour">
<template v-for="(events,_day) in allDayEvents" :key="_day">
<div
v-if="dayindex == _day"
v-for="event in events"
:key="event"
style="top:0;"
@click.prevent="weekPageClick(event, _day)"
:selected="event == selectedEvent"
:style="{'background-color': event?.color, 'margin-bottom':'1px'}"
class="d-grid m-1 small rounded overflow-hidden fhc-entry"
v-contrast
>
<slot class="p-1" 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>
</template>
</div>
</div>
<div class="events position-relative" :ref="'eventsRef'+week" @mousemove="calcHourPosition" @mouseleave="hourPosition = null">
<div :id="hourGridIdentifier(hour)" v-for="hour in hours" :key="hour" class="position-absolute box-shadow-border" :style="hourGridStyle(hour)"></div>
<Transition>
<div v-if="hourPosition" class="fhc-calendar-hour-indicator position-absolute small" :style="indicatorStyle" >
<span class=" border border-top-0 px-2 ">{{hourPositionTime}}</span>
</div>
</Transition>
<div class="hours">
<div v-for="hour in hours" :style="getGridStyle" :key="hour" class="text-end small" :ref="'hour' + hour">{{hour}}:00</div>
</div>
<div v-for="(day,dayindex) in eventsPerDayAndHour" :key="day" :class="{'past':day.isPast}" class=" day border-start position-relative" :style="dayGridStyle(day)">
<Transition>
<div v-if="day.isToday" class="border-top small curTimeIndicator" :style="curIndicatorStyle">
<span class="fhc-body-bg border border-top-0 px-2">{{curTime}}</span>
</div>
</Transition>
<div v-if="day.isToday" class="overlay" :style="overlayStyle"></div>
<div v-for="event in day.events" :key="event" @click.prevent="weekPageClick(event.orig, day)"
:selected="event.orig == selectedEvent"
:style="eventGridStyle(day,event)"
class="mx-2 small rounded overflow-hidden fhc-entry border border-secondary"
v-contrast >
<slot name="weekPage" :event="event.orig" :day="day">
<p>this is a placeholder which means that no template was passed to the Calendar Page slot</p>
</slot>
</div>
</div>
</div>
</div>
</div>
</div>`
}
-69
View File
@@ -1,69 +0,0 @@
import CalendarAbstract from './Abstract.js';
export default {
mixins: [
CalendarAbstract
],
emits: [
'change'
],
inject: [
'size',
'focusDate',
'mode',
],
props: {
header: {
type: Boolean,
default: true
}
},
computed: {
weeks(){
return [...Array(this.focusDate.numWeeks).keys()].map(i => i + 1);
},
title() {
return this.focusDate.format({year: 'numeric'});
}
},
methods: {
setWeek(week) {
// TODO(chris): test is there a week jump on year select? => yes there is if the same month/day are in different weeks ... should we prevent that?
this.focusDate.w = week;
this.$emit('updateMode', 'week');
Vue.nextTick(()=>{
let date = new Date(this.focusDate.y, this.focusDate.m, this.focusDate.d);
date = date.getFullYear() + "-" +
String(date.getMonth() + 1).padStart(2, "0") + "-" +
String(date.getDate()).padStart(2, "0");
this.$router.push({
name: "LvPlan",
params: {
mode: this.mode[0].toUpperCase() + this.mode.slice(1),
focus_date: date,
lv_id: this.$route.params.lv_id || null
}
})
})
},
prev(){
this.focusDate.y--;
this.focusDate._clean();
},
next() {
this.focusDate.y++;
this.focusDate._clean();
},
},
template: `
<div class="fhc-calendar-weeks h-100">
<calendar-header v-if="header" :title="title" @prev="prev" @next="next" @click="$emit('updateMode', 'years')" @updateMode="$emit('updateMode', $event)" />
<div class="d-flex flex-wrap">
<div v-for="(week, key) in weeks" :key="key" class="d-grid col-2">
<button @click="setWeek(week)" class="btn btn-outline-secondary card" :class="{'border-0': week != focusDate.w}">
{{week}}
</button>
</div>
</div>
</div>`
}
-61
View File
@@ -1,61 +0,0 @@
import CalendarAbstract from './Abstract.js';
import CalendarPane from './Pane.js';
import CalendarYearsPage from './Years/Page.js';
export default {
mixins: [
CalendarAbstract
],
components: {
CalendarYearsPage,
CalendarPane
},
inject: [
'size'
],
data() {
return {
start: 0
}
},
computed: {
range() {
switch (this.size) {
case 3:
// eslint-disable-next-line
case 2:
return 24;
}
return 12;
},
end() {
return this.start + this.range - 1;
},
title() {
return this.start + ' - ' + this.end;
}
},
methods: {
paneChanged(dir) {
this.start += this.range * dir;
},
prev() {
this.$refs.pane.prev();
this.$emit('change:offset', { y: -1, m: 0, d: 0 });
},
next() {
this.$refs.pane.next();
this.$emit('change:offset', { y: 1, m: 0, d: 0 });
}
},
created() {
this.start = this.focusDate.y - this.focusDate.y%this.range;
},
template: `
<div class="fhc-calendar-years">
<calendar-header :title="title" @prev="prev" @next="next" @updateMode="$emit('updateMode', $event)" />
<calendar-pane ref="pane" v-slot="slot" @slid="paneChanged">
<calendar-years-page :data-test="slot.index" :start="start+range*slot.offset" :end="start+range*slot.offset+range" @updateMode="$emit('updateMode')"/>
</calendar-pane>
</div>`
}
@@ -1,36 +0,0 @@
export default {
inject: [
'focusDate'
],
props: {
start: Number,
end: Number
},
emits: [
'updateMode'
],
data() {
return {
}
},
computed: {
years() {
return [...Array(this.end - this.start).keys()].map(i => i + this.start);
}
},
mounted() {
const container = document.getElementById("calendarContainer")
if(container) {
container.style['overflow-y'] = 'scroll'
container.style['overflow-x'] = 'auto'
}
},
template: `
<div class="fhc-calendar-years-page d-flex flex-wrap">
<div v-for="year in years" :key="year" class="d-grid col-4">
<button class="btn btn-outline-secondary card justify-content-center" :class="{'border-0': year != focusDate.y}" @click="focusDate.y = year; $emit('updateMode')">
{{year}}
</button>
</div>
</div>`
}
-96
View File
@@ -1,96 +0,0 @@
import BsModal from "../../Bootstrap/Modal.js";
import Alert from "../../Bootstrap/Alert.js";
import LvMenu from "./LvMenu.js"
import ApiLvPlan from '../../../api/factory/lvPlan.js';
import ApiAddons from '../../../api/factory/addons.js';
export default {
components: {
BsModal,
Alert,
LvMenu,
},
mixins: [BsModal],
props: {
event:Object,
title:{
type:String,
default:"title"
},
showMenu:{
type:Boolean,
default:true,
},
/*
* NOTE(chris):
* Hack to expose in "emits" declared events to $props which we use
* in the v-bind directive to forward all events.
* @see: https://github.com/vuejs/core/issues/3432
*/
onHideBsModal: Function,
onHiddenBsModal: Function,
onHidePreventedBsModal: Function,
onShowBsModal: Function,
onShownBsModal: Function,
},
data() {
return {
menu: null,
result: false,
info: null,
};
},
methods:{
onHideModal: function(){
this.menu = null;
},
onModalShow: function()
{
// do not load the menu if the menu is not getting rendered
if(!this.showMenu) return;
if (this.event.type == 'lehreinheit') {
this.$api
.call(ApiLvPlan.getLehreinheitStudiensemester(Array.isArray(this.event.lehreinheit_id) ? this.event.lehreinheit_id[0] : this.event.lehreinheit_id))
.then(res => res.data)
.then(studiensemester_kurzbz => this.$api.call(
ApiAddons.getLvMenu(
this.event.lehrveranstaltung_id,
studiensemester_kurzbz
)
))
.then(res => {
if (res.data) {
this.menu = res.data;
}
});
}
}
},
mounted() {
this.modal = this.$refs.modalContainer.modal;
},
popup(options) {
return BsModal.popup.bind(this)(null, options);
},
template: /*html*/ `
<bs-modal ref="modalContainer" @showBsModal="onModalShow" @hideBsModal="onHideModal" :bodyClass="''" dialogClass='modal-lg' class="bootstrap-alert" :backdrop="false" >
<template v-slot:title>
<slot name="modalTitle"></slot>
</template>
<template v-slot:default>
<slot name="modalContent"></slot>
<template v-if="showMenu && this.menu">
<h5>{{$p.t('lehre','lehrveranstaltungsmenue')}}</h5>
<lv-menu :menu="menu"></lv-menu>
</template>
</template>
<!-- optional footer -->
<template v-slot:footer >
<button class="btn btn-outline-secondary card" @click="hide">{{$p.t('ui','cancel')}}</button>
</template>
<!-- end of optional footer -->
</bs-modal>`,
};
-274
View File
@@ -1,274 +0,0 @@
import {user_locale} from "../plugin/Phrasen.js";
import CalendarDates from "./CalendarDates.js";
const monthDayRanges = []
for(let i = 1; i < 13; i++) {
monthDayRanges.push(new Date(new Date(Date.now()).getFullYear(), i, 0).getDate())
}
class CalendarDate {
constructor(y, m, d) {
this.weekStart = CalendarDate.getWeekStart();
this.watchLocale = Vue.watch(
user_locale,
(newLocale, oldLocale, onCleanup) =>{
this.weekStart = CalendarDate.getWeekStart();
this._clean();
onCleanup((cleanup)=>{
// do clean up
});
},
);
this.set(y, m, d);
this._clean();
CalendarDates.subscribe(this);
}
get y() { return this._y }
set y(v) { this._y = v; this._clean() }
get m() { return this._m }
set m(v) { this._m = v; this._clean() }
get d() { return this._d }
set d(v) { this._d = v; this._clean() }
/**
* @see https://www.smart-rechner.de/kalenderwochen/rechner.php
*/
get w() {
if (this._w === null) {
if (this.weekStart == 1 && this._m == 11 && this._d > 28 && this.wd <= this._d-29) {
this._w = 1;
} else if (this.weekStart == 1 && this._m == 0 && this._d < 4 && 3-this.wd <= -this._d) {
let weekStartOfTheYear = new Date(this.y-1, 0, this.weekStart == 1 ? 4 : 1);
weekStartOfTheYear.setDate(weekStartOfTheYear.getDate() - (weekStartOfTheYear.getDay() + 7 - this.weekStart)%7);
this._w = Math.ceil((Math.floor((new Date(this.y, this.m, this.d) - weekStartOfTheYear) / 86400000) + 1) / 7);
} else {
let weekStartOfTheYear = new Date(this.y, 0, this.weekStart == 1 ? 4 : 1);
weekStartOfTheYear.setDate(weekStartOfTheYear.getDate() - (weekStartOfTheYear.getDay() + 7 - this.weekStart)%7);
this._w = Math.ceil((Math.floor((new Date(this.y, this.m, this.d) - weekStartOfTheYear) / 86400000) + 1) / 7);
}
}
return this._w;
}
set w(v) {
if (this.w != v) {
let lw = this.numWeeks;
this.d += (v - this.w) * 7;
if (v > 0 && v <= lw && this.w != v) {
if (this.weekStart != 1) {
if (this.w == 1) {
this.set(this.firstDayOfWeek);
} else {
this.set(this.lastDayOfWeek);
}
}
if (this.w != v) {
console.error('couldn\'t set the week', this, v);
}
}
}
}
get wYear() {
if( this.w === 1 ) {
return this.cdLastDayOfWeek.y;
}
return this.cdFirstDayOfWeek.y;
}
get wd() {
if (this._wd === null) {
// the .getDay() method from js Date object ALWAYS returns values from 0 to 6, where 0 is Sunday and 6 is Saturday
// aligns the getDay() result of the Date to the weekStart of the CalendarDate
this._wd = ((new Date(this.y, this.m, this.d)).getDay()+7-this.weekStart)%7;
}
return this._wd;
}
get firstDayOfWeek() {
let firstDayOfWeek = new Date(this.y, this.m, this.d);
// to ensure that firstDayOfWeek.getDay() is always greater than this.weekStart we add 7 and wrap the result around with %7 to avoid negative numbers
firstDayOfWeek.setDate(this.d -(firstDayOfWeek.getDay()+7-this.weekStart)%7);
return firstDayOfWeek;
}
get cdFirstDayOfWeek() {
let FirstDayOfWeek = new CalendarDate(this.firstDayOfWeek);
return FirstDayOfWeek;
}
get lastDayOfWeek() {
let lastDayOfWeek = new Date(this.y, this.m, this.d);
// uses the calculation from firstDayOfWeek and adds 6 days to the result to get the last day of the week
lastDayOfWeek.setDate(this.d -(lastDayOfWeek.getDay()+7-this.weekStart)%7 +6);
return lastDayOfWeek;
}
get wholeWorkWeek() {
const days = []
const date = new Date(this.y, this.m, this.d);
for(let i = 0; i < 5; i++) {
days[i] = new Date(this.y, this.m, this.d)
days[i].setDate(this.d -(date.getDay()+7-this.weekStart)%7 + i)
}
return days
}
get cdLastDayOfWeek() {
let LastDayOfWeek = new CalendarDate(this.lastDayOfWeek);
return LastDayOfWeek;
}
get firstDayOfCalendarMonth() {
let firstDayOfMonth = new Date(this.y, this.m, 1);
let offset = (firstDayOfMonth.getDay() + 7 - this.weekStart) % 7;
// offset will be greater than 1 most of the time, using a negative number for a date returns a date in the past
return new Date(this.y, this.m, 1-offset);
}
get cdFirstDayOfCalendarMonth() {
let firstDayOfMonth = new Date(this.y, this.m, 1);
let offset = (firstDayOfMonth.getDay() + 7 - this.weekStart) % 7;
let FirstDayOfCalendarMonth = new CalendarDate(this.y, this.m, 1 - offset);
return FirstDayOfCalendarMonth;
}
get lastDayOfCalendarMonth() {
// In JavaScript, the Date constructor interprets: A day of 0 as the last day of the previous month
let lastDayOfMonth = new Date(this.y, this.m+1, 0);
let offset = (lastDayOfMonth.getDay() + 7 - this.weekStart) % 7;
return new Date(lastDayOfMonth.getFullYear(), lastDayOfMonth.getMonth(), lastDayOfMonth.getDate()+6-offset);
}
get cdLastDayOfCalendarMonth() {
let lastDayOfMonth = new Date(this.y, this.m+1, 0);
let offset = (lastDayOfMonth.getDay() + 7 - this.weekStart) % 7;
let LasyDayOfCalendarMonth = new CalendarDate(lastDayOfMonth.getFullYear(), lastDayOfMonth.getMonth(), lastDayOfMonth.getDate() + 6 - offset);
return LasyDayOfCalendarMonth;
}
get cdLastDayOfNextCalendarMonth() {
let lastDayOfMonth = new Date(this.y, this.m+1, 0);
let offset = (lastDayOfMonth.getDay() + 7 - this.weekStart) % 7;
let LastDayOfNextCalendarMonth = new CalendarDate(lastDayOfMonth.getFullYear(), lastDayOfMonth.getMonth() + 1, lastDayOfMonth.getDate() + 6 - offset);
return LastDayOfNextCalendarMonth;
}
get nextSevenDays() {
const days = []
for(let i = 0; i < 7; i++) {
days[i] = new Date(this.y, this.m, this.d + i)
}
return days
}
get numWeeks() {
// if the week starts with Monday we have to go 3 days in the past from the start of the next year to get the correct numWeek of the current year
// this is because for example 30.12.2024 - 05.01.2025 is the first calendarWeek of 2025
let lastCalendarWeek = new CalendarDate(this.y + 1, 0, this.weekStart == 1 ? -3 : 0);
return lastCalendarWeek.w;
}
set(y,m,d,noClean) {
if (y !== undefined && (m === undefined || m === true) && d === undefined) {
if (this.isDate(y))
{
// set year/month/day from date object
return this.set(y.getFullYear(), y.getMonth(), y.getDate(), m);
}
if (y.y !== undefined && y.m !== undefined && y.d !== undefined)
{
// set year/month/day from CalendarDate object
return this.set(y.y, y.m, y.d, m);
}
}
// initialize year/month/day
this._y = y ?? 0;
this._m = m ?? 0;
this._d = d ?? 0;
if (!noClean)
this._clean();
}
_clean() {
this.set(new Date(this._y, this._m, this._d), true);
this._w = null;
this._wd = null;
}
format(options, lang=undefined) {
return (new Date(this._y, this._m, this._d)).toLocaleString(lang, options);
}
compare(d) {
if (this.isDate(d))
return (this.y === d.getFullYear() && this.m === d.getMonth() && this.d === d.getDate());
return (this.y === d.y && this.m === d.m && this.d === d.d);
}
isInWeek(w, y) {
if (this.y == y && this.w == w)
return true;
if (this.weekStart == 1)
return false;
let edgeDay = this.cdFirstDayOfWeek;console.log(edgeDay);
if (edgeDay.y == y && edgeDay.w == w)
return true;
edgeDay = this.cdLastDayOfWeek;
if (edgeDay.y == y && edgeDay.w == w)
return true;
return false;
}
setLocale(locale) {
this.weekStart = CalendarDate.getWeekStart(locale);
}
// method that checks if the parameter is of type Date
isDate(obj){
return Object.prototype.toString.call(obj) === '[object Date]';
}
cleanup(){
if(this.watchLocale && this.watchLocale.stop) this.watchLocale.stop(); // TODO: ?
}
moveMonthInDirection (dir) {
// avoid setting date in wrong month if we try to create a date which does not exist like 30th of february
const newM = this.m + dir
const maxDaysTarget = monthDayRanges[newM]
if (this.d > maxDaysTarget) this.d = maxDaysTarget
this.m = newM // calls _clean which sets a new Date
}
}
/**
* Returns the weekday number (Date.getDay()) on which the week starts depending on the locale.
* This can be Saturday(6), Sunday(0) or Monday(1)
*
* @see https://stackoverflow.com/questions/53382465/how-can-i-determine-if-week-starts-on-monday-or-sunday-based-on-locale-in-pure-j
*
* @param string locale
*
* @return integer
*/
CalendarDate.getWeekStart = function(locale) {
locale = user_locale.value || locale || navigator.language;
const parts = locale.match(/^([a-z]{2,3})(?:-([a-z]{3})(?=$|-))?(?:-([a-z]{4})(?=$|-))?(?:-([a-z]{2}|\d{3})(?=$|-))?/i);
const language_code = parts[1];
const language_starting_Sat = ['ar','arq','arz','fa'];
const language_starting_Sun = 'amasbndzengnguhehiidjajvkmknkolomhmlmrmtmyneomorpapssdsmsnsutatethtnurzhzu'.match(/../g);
const region_code = parts[4];
const region_starting_Sat = 'AEAFBHDJDZEGIQIRJOKWLYOMQASDSY'.match(/../g);
const region_starting_Sun = 'AGARASAUBDBRBSBTBWBZCACNCODMDOETGTGUHKHNIDILINJMJPKEKHKRLAMHMMMOMTMXMZNINPPAPEPHPKPRPTPYSASGSVTHTTTWUMUSVEVIWSYEZAZW'.match(/../g);
if (region_code){
if (region_starting_Sun.includes(region_code))
return 0;
else if (region_starting_Sat.includes(region_code))
return 6;
else
return 1;
}
else if(language_code)
{
if (language_starting_Sun.includes(language_code))
return 0;
else if (language_starting_Sat.includes(language_code))
return 6;
else
return 1;
}
else
{
return 1;
}
}
export default CalendarDate
-16
View File
@@ -1,16 +0,0 @@
class CalendarDates{
static subscribers_array = [];
static subscribe(subscriber){
this.subscribers_array.push(subscriber);
}
static unsubscribe(subscriber){
this.subscribers_array = this.subscribers_array.filter(sub => !sub.compare(subscriber));
}
static cleanup(){
this.subscribers_array.forEach(sub => { sub.cleanup() });
}
}
export default CalendarDates;