mirror of
https://github.com/FH-Complete/FHC-Core.git
synced 2026-06-01 20:29:29 +00:00
Cleanup
This commit is contained in:
@@ -1,17 +0,0 @@
|
||||
import CalendarHeader from './Header.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CalendarHeader
|
||||
},
|
||||
inject: [
|
||||
'date',
|
||||
'focusDate',
|
||||
'size'
|
||||
],
|
||||
emits: [
|
||||
'updateMode',
|
||||
'change:range',
|
||||
'input'
|
||||
]
|
||||
}
|
||||
@@ -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>`
|
||||
}
|
||||
@@ -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>`
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
`
|
||||
}
|
||||
@@ -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>`
|
||||
}
|
||||
@@ -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>
|
||||
`
|
||||
}
|
||||
@@ -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>
|
||||
`
|
||||
}
|
||||
@@ -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>`
|
||||
}
|
||||
@@ -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>`
|
||||
}
|
||||
@@ -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>
|
||||
`
|
||||
}
|
||||
@@ -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>`
|
||||
}
|
||||
@@ -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>`
|
||||
}
|
||||
@@ -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>`
|
||||
}
|
||||
@@ -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>`,
|
||||
};
|
||||
@@ -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
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user