mirror of https://github.com/nextcloud/calendar
355 lines
11 KiB
Vue
355 lines
11 KiB
Vue
<!--
|
|
- @copyright Copyright (c) 2020 Georg Ehrke <oc.list@georgehrke.com>
|
|
-
|
|
- @author Georg Ehrke <oc.list@georgehrke.com>
|
|
- @author Richard Steinmetz <richard@steinmetz.cloud>
|
|
-
|
|
- @license AGPL-3.0-or-later
|
|
-
|
|
- This program is free software: you can redistribute it and/or modify
|
|
- it under the terms of the GNU Affero General Public License as
|
|
- published by the Free Software Foundation, either version 3 of the
|
|
- License, or (at your option) any later version.
|
|
-
|
|
- This program is distributed in the hope that it will be useful,
|
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
- GNU Affero General Public License for more details.
|
|
-
|
|
- You should have received a copy of the GNU Affero General Public License
|
|
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
-
|
|
-->
|
|
|
|
<template>
|
|
<FullCalendar v-if="calendarOptions"
|
|
ref="fullCalendar"
|
|
:class="isWidget? 'fullcalendar-widget': ''"
|
|
:options="calendarOptions" />
|
|
</template>
|
|
|
|
<script>
|
|
// Import FullCalendar itself
|
|
import FullCalendar from '@fullcalendar/vue'
|
|
import dayGridPlugin from '@fullcalendar/daygrid'
|
|
import interactionPlugin from '@fullcalendar/interaction'
|
|
import listPlugin from '@fullcalendar/list'
|
|
import timeGridPlugin from '@fullcalendar/timegrid'
|
|
import multiMonthPlugin from '@fullcalendar/multimonth'
|
|
|
|
// Import event sources
|
|
import eventSource from '../fullcalendar/eventSources/eventSource.js'
|
|
|
|
// Import interaction handlers
|
|
import eventAllow from '../fullcalendar/interaction/eventAllow.js'
|
|
import eventClick from '../fullcalendar/interaction/eventClick.js'
|
|
import eventDrop from '../fullcalendar/interaction/eventDrop.js'
|
|
import eventResize from '../fullcalendar/interaction/eventResize.js'
|
|
import navLinkDayClick from '../fullcalendar/interaction/navLinkDayClick.js'
|
|
import navLinkWeekClick from '../fullcalendar/interaction/navLinkWeekClick.js'
|
|
import select from '../fullcalendar/interaction/select.js'
|
|
|
|
// Import localization plugins
|
|
import { getDateFormattingConfig } from '../fullcalendar/localization/dateFormattingConfig.js'
|
|
import { getFullCalendarLocale } from '../fullcalendar/localization/localeProvider.js'
|
|
import momentPluginFactory from '../fullcalendar/localization/momentPlugin.js'
|
|
|
|
// Import rendering handlers
|
|
import dayHeaderDidMount from '../fullcalendar/rendering/dayHeaderDidMount.js'
|
|
import eventDidMount from '../fullcalendar/rendering/eventDidMount.js'
|
|
import eventOrder from '../fullcalendar/rendering/eventOrder.js'
|
|
import noEventsDidMount from '../fullcalendar/rendering/noEventsDidMount.js'
|
|
|
|
// Import timezone plugins
|
|
import VTimezoneNamedTimezone from '../fullcalendar/timezones/vtimezoneNamedTimezoneImpl.js'
|
|
|
|
// Import other dependencies
|
|
import { mapGetters, mapState } from 'vuex'
|
|
import debounce from 'debounce'
|
|
import { getYYYYMMDDFromFirstdayParam } from '../utils/date.js'
|
|
|
|
export default {
|
|
name: 'CalendarGrid',
|
|
components: {
|
|
FullCalendar,
|
|
},
|
|
props: {
|
|
isWidget: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
url: {
|
|
type: String,
|
|
required: false,
|
|
},
|
|
/**
|
|
* Whether or not the user is authenticated
|
|
*/
|
|
isAuthenticatedUser: {
|
|
type: Boolean,
|
|
required: true,
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
updateTodayJob: null,
|
|
updateTodayJobPreviousDate: null,
|
|
calendarOptions: null,
|
|
}
|
|
},
|
|
computed: {
|
|
...mapGetters({
|
|
timezoneId: 'getResolvedTimezone',
|
|
}),
|
|
...mapState({
|
|
locale: (state) => state.settings.momentLocale,
|
|
eventLimit: state => state.settings.eventLimit,
|
|
skipPopover: state => state.settings.skipPopover,
|
|
showWeekends: state => state.settings.showWeekends,
|
|
showWeekNumbers: state => state.settings.showWeekNumbers,
|
|
slotDuration: state => state.settings.slotDuration,
|
|
showTasks: state => state.settings.showTasks,
|
|
timezone: state => state.settings.timezone,
|
|
modificationCount: state => state.calendarObjects.modificationCount,
|
|
}),
|
|
options() {
|
|
return {
|
|
// Initialization:
|
|
initialDate: getYYYYMMDDFromFirstdayParam(this.$route?.params?.firstDay ?? 'now'),
|
|
initialView: this.$route?.params.view ?? 'dayGridMonth',
|
|
// Data
|
|
eventSources: this.eventSources,
|
|
// Plugins
|
|
plugins: this.plugins,
|
|
// Interaction
|
|
editable: this.isEditable,
|
|
selectable: this.isAuthenticatedUser,
|
|
eventAllow,
|
|
eventClick: eventClick(this.$store, this.$router, this.$route, window, this.isWidget, this.$refs),
|
|
eventDrop: this.isWidget ? false : (...args) => eventDrop(this.$store, this.$refs.fullCalendar.getApi())(...args),
|
|
eventResize: this.isWidget ? false : eventResize(this.$store),
|
|
navLinkDayClick: this.isWidget ? false : navLinkDayClick(this.$router, this.$route),
|
|
navLinkWeekClick: this.isWidget ? false : navLinkWeekClick(this.$router, this.$route),
|
|
select: this.isWidget ? false : select(this.$store, this.$router, this.$route, window),
|
|
navLinks: true,
|
|
// Localization
|
|
...getDateFormattingConfig(),
|
|
...getFullCalendarLocale(),
|
|
// Rendering
|
|
dayHeaderDidMount,
|
|
eventDidMount,
|
|
noEventsDidMount,
|
|
eventOrder: ['start', '-duration', 'allDay', eventOrder],
|
|
forceEventDuration: false,
|
|
headerToolbar: false,
|
|
height: '100%',
|
|
slotDuration: this.slotDuration,
|
|
expandRows: true,
|
|
weekNumbers: this.showWeekNumbers,
|
|
weekends: this.showWeekends,
|
|
dayMaxEventRows: this.eventLimit,
|
|
selectMirror: true,
|
|
lazyFetching: false,
|
|
nowIndicator: true,
|
|
progressiveEventRendering: true,
|
|
unselectAuto: false,
|
|
// Timezones:
|
|
timeZone: this.timezoneId,
|
|
// Disable jumping in week view and day view when clicking on any event using the simple editor
|
|
scrollTimeReset: false,
|
|
}
|
|
},
|
|
eventSources() {
|
|
if (this.isWidget) {
|
|
const calendar = this.$store.getters.getCalendarByUrl(this.url)
|
|
if (!calendar) {
|
|
return []
|
|
}
|
|
return [calendar].map(eventSource(this.$store))
|
|
}
|
|
return this.$store.getters.enabledCalendars.map(eventSource(this.$store))
|
|
},
|
|
widgetView() {
|
|
return this.$store.getters.widgetView
|
|
},
|
|
widgetDate() {
|
|
return this.$store.getters.widgetDate
|
|
},
|
|
/**
|
|
* FullCalendar Plugins
|
|
*
|
|
* @return {(PluginDef)[]}
|
|
*/
|
|
plugins() {
|
|
return [
|
|
momentPluginFactory(this.$store),
|
|
VTimezoneNamedTimezone,
|
|
dayGridPlugin,
|
|
interactionPlugin,
|
|
listPlugin,
|
|
timeGridPlugin,
|
|
multiMonthPlugin,
|
|
]
|
|
},
|
|
isEditable() {
|
|
// We do not allow drag and drop when the editor is open.
|
|
return this.isAuthenticatedUser
|
|
&& this.$route?.name !== 'EditPopoverView'
|
|
&& this.$route?.name !== 'EditSidebarView'
|
|
},
|
|
},
|
|
watch: {
|
|
widgetView(newView) {
|
|
const calendarApi = this.$refs.fullCalendar.getApi()
|
|
calendarApi.changeView(newView)
|
|
},
|
|
widgetDate(newDate) {
|
|
const calendarApi = this.$refs.fullCalendar.getApi()
|
|
calendarApi.gotoDate(getYYYYMMDDFromFirstdayParam(newDate))
|
|
},
|
|
eventSources(sources, oldSources) {
|
|
const newSources = sources.filter(source => !oldSources.map(oldSource => oldSource.id).includes(source.id))
|
|
const removedSources = oldSources.filter(oldSource => !sources.map(source => source.id).includes(oldSource.id))
|
|
|
|
// Hackity hack! Unfortunately, calendarOptions.eventSources is not reactive ...
|
|
// Ref https://fullcalendar.io/docs/Calendar-addEventSource
|
|
// TODO: Find a better/safer way to prevent duplicated event sources
|
|
const calendarApi = this.$refs.fullCalendar.getApi()
|
|
for (const source of newSources) {
|
|
calendarApi.addEventSource(source)
|
|
}
|
|
const eventSources = calendarApi.getEventSources()
|
|
for (const source of removedSources) {
|
|
eventSources.find(x => x.id === source.id)?.remove()
|
|
}
|
|
},
|
|
modificationCount: debounce(function() {
|
|
const calendarApi = this.$refs.fullCalendar.getApi()
|
|
calendarApi.refetchEvents()
|
|
}, 50),
|
|
},
|
|
/**
|
|
* FullCalendar 5 is using calculated px values for the width
|
|
* of its views.
|
|
* Hence a simple `width: 100%` won't assure that the calendar-grid
|
|
* is always using the full available width.
|
|
*
|
|
* Toggling the AppNavigation or AppSidebar will change the amount
|
|
* of available space, but it will not be covered by the window
|
|
* resize event, because the actual window size did not change.
|
|
*
|
|
* To make sure, that the calendar-grid is always using all space,
|
|
* we have to register a resize-observer here, that will automatically
|
|
* update the fullCalendar size, when the available space changes.
|
|
*/
|
|
mounted() {
|
|
if (window.ResizeObserver) {
|
|
const resizeObserver = new ResizeObserver(debounce(() => {
|
|
this.$refs.fullCalendar
|
|
.getApi()
|
|
.updateSize()
|
|
}, 100))
|
|
|
|
resizeObserver.observe(this.$refs.fullCalendar.$el)
|
|
}
|
|
},
|
|
async created() {
|
|
this.calendarOptions = await this.options
|
|
|
|
this.updateTodayJob = setInterval(() => {
|
|
const newDate = getYYYYMMDDFromFirstdayParam('now')
|
|
|
|
if (this.updateTodayJobPreviousDate === null) {
|
|
this.updateTodayJobPreviousDate = newDate
|
|
return
|
|
}
|
|
|
|
if (this.updateTodayJobPreviousDate !== newDate) {
|
|
this.updateTodayJobPreviousDate = newDate
|
|
|
|
const calendarApi = this.$refs.fullCalendar.getApi()
|
|
calendarApi.render()
|
|
}
|
|
}, 1000)
|
|
|
|
/**
|
|
* This view is not used as a router view,
|
|
* hence we can't use beforeRouteUpdate directly.
|
|
*/
|
|
if (!this.isWidget) {
|
|
this.$router.beforeEach((to, from, next) => {
|
|
if (to.params.firstDay !== from.params.firstDay) {
|
|
const calendarApi = this.$refs.fullCalendar.getApi()
|
|
calendarApi.gotoDate(getYYYYMMDDFromFirstdayParam(to.params.firstDay))
|
|
}
|
|
if (to.params.view !== from.params.view) {
|
|
const calendarApi = this.$refs.fullCalendar.getApi()
|
|
calendarApi.changeView(to.params.view)
|
|
this.saveNewView(to.params.view)
|
|
}
|
|
|
|
if ((from.name === 'NewPopoverView' || from.name === 'NewSidebarView')
|
|
&& to.name !== 'NewPopoverView'
|
|
&& to.name !== 'NewSidebarView') {
|
|
const calendarApi = this.$refs.fullCalendar.getApi()
|
|
calendarApi.unselect()
|
|
}
|
|
|
|
next()
|
|
})
|
|
|
|
// Trigger the select event programmatically on initial page load to show the new event
|
|
// in the grid. Wait for the next tick because the ref isn't available right away.
|
|
await this.$nextTick()
|
|
if (['NewPopoverView', 'NewSidebarView'].includes(this.$route.name)) {
|
|
const start = new Date(parseInt(this.$route.params.dtstart) * 1000)
|
|
const end = new Date(parseInt(this.$route.params.dtend) * 1000)
|
|
if (!isNaN(start.getTime()) && !isNaN(end.getTime())) {
|
|
const calendarApi = this.$refs.fullCalendar.getApi()
|
|
calendarApi.select({
|
|
start,
|
|
end,
|
|
allDay: this.$route.params.allDay === '1',
|
|
})
|
|
}
|
|
}
|
|
}
|
|
},
|
|
methods: {
|
|
/**
|
|
* When a user changes the view, remember it and
|
|
* use it the next time they open the calendar app
|
|
*/
|
|
saveNewView: debounce(function(initialView) {
|
|
if (this.isAuthenticatedUser) {
|
|
this.$store.dispatch('setInitialView', { initialView })
|
|
}
|
|
}, 5000),
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.calendar-grid-checkbox {
|
|
border-style: solid;
|
|
border-width: 2px;
|
|
border-radius: 4px;
|
|
height: 16px;
|
|
width: 16px;
|
|
}
|
|
|
|
.calendar-grid-checkbox-checked {
|
|
border-style: solid;
|
|
border-width: 8px;
|
|
border-radius: 4px;
|
|
height: 16px;
|
|
width: 16px;
|
|
}
|
|
.fullcalendar-widget{
|
|
min-height: 500px;
|
|
:deep(.fc-col-header-cell-cushion){
|
|
font-size: 9px;
|
|
}
|
|
}
|
|
</style>
|