basic fullcalendar integration

Signed-off-by: Georg Ehrke <developer@georgehrke.com>
This commit is contained in:
Georg Ehrke 2018-11-22 01:15:33 +01:00
parent b4dd09f9c6
commit 6126cb4f59
No known key found for this signature in database
GPG Key ID: 9D98FD9380A1CB43
17 changed files with 541 additions and 113 deletions

View File

@ -1,66 +1,6 @@
/**
* Calendar App
*
* @author Raghu Nayyar
* @author Georg Ehrke
* @copyright 2016 Raghu Nayyar <hey@raghunayyar.com>
* @copyright 2016 Georg Ehrke <oc.list@georgehrke.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*
*/
.app {
height: 100%;
width: 100%;
}
.modal-content #app-sidebar {
position: fixed;
}
#fullcalendar table {
white-space: inherit !important;
}
.fc-state-highlight.fc-day-number,
#fullcalendar tbody tr,
#fullcalendar tbody tr:hover,
#fullcalendar tbody tr:focus {
background: transparent !important;
}
#popover-container,
#importpopover-container {
max-height: 0 !important;
max-width: 0 !important;
overflow: hidden !important;
}
#fullcalendar .fc-axis,
#fullcalendar .fc-day-header {
font-size: 100%;
opacity: .5;
}
#fullcalendar td.fc-day.fc-sat,
#fullcalendar td.fc-day.fc-sun {
background-color: var(--color-background-darker);
}
@import 'calendarlist.scss';
@import 'confirmation.scss';
@import 'datepicker.scss';
@import 'eventdialog.scss';
@import 'fullcalendar.scss';

View File

@ -1,11 +1,13 @@
/**
* Calendar App
*
* @copyright 2016 Raghu Nayyar <hey@raghunayyar.com>
* @copyright 2018 Georg Ehrke <oc.list@georgehrke.com>
* @copyright 2017 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author Raghu Nayyar
* @author Georg Ehrke
* @copyright 2016 Raghu Nayyar <hey@raghunayyar.com>
* @copyright 2016 Georg Ehrke <oc.list@georgehrke.com>
* @copyright 2017 John Molakvoæ <skjnldsv@protonmail.com>
* @author John Molakvoæ
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE

View File

@ -334,3 +334,16 @@ button.delete:focus {
.dropdown-menu li a:hover {
background: #eee;
}
.modal-content #app-sidebar {
position: fixed;
}
#popover-container,
#importpopover-container {
max-height: 0 !important;
max-width: 0 !important;
overflow: hidden !important;
}

View File

@ -1,3 +1,32 @@
/**
* Calendar App
*
* @copyright 2018 Georg Ehrke <oc.list@georgehrke.com>
*
* @author Georg Ehrke
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*
*/
#app-content {
padding-top: 50px;
}
/* Fullcalendar modifications */
@ -68,3 +97,29 @@
.fc-unthemed td.fc-today {
background: #fcf8e3 !important;
}
#fullcalendar .fc-axis,
#fullcalendar .fc-day-header {
font-size: 100%;
opacity: .5;
}
#fullcalendar td.fc-day.fc-sat,
#fullcalendar td.fc-day.fc-sun {
background-color: nc-darken($color-main-background, 3%);
}
#fullcalendar table {
white-space: inherit !important;
}
.fc-state-highlight.fc-day-number,
#fullcalendar tbody tr,
#fullcalendar tbody tr:hover,
#fullcalendar tbody tr:focus {
background: transparent !important;
}

21
css/app/icons.scss Normal file
View File

@ -0,0 +1,21 @@
/**
* Calendar App
*
* @copyright 2018 Georg Ehrke <oc.list@georgehrke.com>
*
* @author Georg Ehrke
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*
*/

20
package-lock.json generated
View File

@ -4610,7 +4610,7 @@
},
"fecha": {
"version": "2.3.3",
"resolved": "http://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz",
"integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg=="
},
"figures": {
@ -7081,6 +7081,11 @@
"verror": "1.10.0"
}
},
"jstz": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/jstz/-/jstz-2.1.1.tgz",
"integrity": "sha512-8hfl5RD6P7rEeIbzStBz3h4f+BQHfq/ABtoU6gXKQv5OcZhnmrIpG7e1pYaZ8hS9e0mp+bxUj08fnDUbKctYyA=="
},
"kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
@ -7749,18 +7754,11 @@
"integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y="
},
"moment-timezone": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.21.tgz",
"integrity": "sha512-j96bAh4otsgj3lKydm3K7kdtA3iKf2m6MY2iSYCzCm5a1zmHo1g+aK3068dDEeocLZQIS9kU8bsdQHLqEvgW0A==",
"version": "0.5.23",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.23.tgz",
"integrity": "sha512-WHFH85DkCfiNMDX5D3X7hpNH3/PUhjTGcD0U1SgfBGZxJ3qUmJh5FdvaFjcClxOvB3rzdfj4oRffbI38jEnC1w==",
"requires": {
"moment": ">= 2.9.0"
},
"dependencies": {
"moment": {
"version": "2.22.2",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz",
"integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y="
}
}
},
"move-concurrently": {

View File

@ -41,6 +41,7 @@
"debounce": "^1.2.0",
"fullcalendar": "^4.0.0-alpha.2",
"ical.js": "^1.2.2",
"jstz": "^2.1.1",
"moment": "^2.22.2",
"nextcloud-axios": "^0.1.2",
"nextcloud-vue": "^0.4.3",

View File

@ -25,10 +25,114 @@
</template>
<script>
import { Calendar } from 'fullcalendar'
import '../../node_modules/fullcalendar/dist/fullcalendar.css'
import debounce from 'debounce'
import '../fullcalendar/timeZoneImpl'
export default {
name: 'FullCalendar',
mounted: () => {
props: {
// single events used for new event, etc.
events: {
type: Array,
default() {
return []
}
},
// event sources for calendars
eventSources: {
type: Array,
default() {
return []
}
},
// config options
config: {
type: Object,
required: true
}
},
data() {
return {
defaultConfig: {
// dayNames: [],
// dayNamesShort: [],
defaultView: 'month',
editable: true,
firstDay: null,
forceEventDuration: true,
header: false,
// locale: null,
// monthNames: [],
// monthNamesShort: [],
slotDuration: '00:15:00',
nowIndicator: true,
weekNumbers: false, // TODO is this the default in view controller?
weekends: true,
eventSources: this.eventSources,
timeZone: 'America/New_York',
timeZoneImpl: 'vtimezone-timezone',
},
calendar: null,
currentDate: null
}
},
watch: {
events: {
deep: true,
handler(newValue, oldValue) {
}
},
eventSources: {
deep: true,
handler(newEventSources, oldEventSources) {
const toAdd = newEventSources.filter((es) => oldEventSources.find((oes) => es.id === oes.id) === undefined)
const toRemove = oldEventSources.filter((oes) => newEventSources.find((es) => es.id === oes.id) === undefined)
toAdd.forEach((es) => {
this.calendar.addEventSource(es)
})
toRemove.forEach((es) => {
this.calendar.getEventSourceById(es.id).remove()
})
}
},
config: {
deep: true,
handler(newValue, oldValue) {
}
},
'$route'({ params }) {
if (params.view !== this.calendar.getView().type) {
this.calendar.changeView(params.view)
}
if (params.firstday !== this.currentDate) {
this.calendar.gotoDate(params.firstday)
this.currentDate = params.firstday
}
}
},
mounted: function() {
window.addEventListener('resize', debounce(() => {
const windowHeight = window.innerHeight
const headerHeight = document.getElementById('header').clientHeight
this.calendar.setOption('height', windowHeight - headerHeight)
}, 500))
const windowHeight = window.innerHeight
const headerHeight = document.getElementById('header').clientHeight
const height = windowHeight - headerHeight
this.calendar = new Calendar(this.$el,
Object.assign({}, { height }, this.defaultConfig, this.config))
this.calendar.render()
},
beforeDestroy: () => {

View File

@ -0,0 +1,70 @@
/**
* @copyright Copyright (c) 2018 Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* 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/>.
*
*/
import { NamedTimeZoneImpl, registerNamedTimeZoneImpl } from 'fullcalendar'
import { getTimezone } from '../services/timezoneDataProviderService'
import ICAL from 'ical.js'
/**
* Our own Fullcalendar Timezone implementation based on the VTimezones we ship
*/
class VTimezoneNamedTimezone extends NamedTimeZoneImpl {
/**
* gets UTC offset for given date of this timezone
*
* @param {Number[]} date an array that mirrors the parameters from new Date()
* @returns {Number} offset in minutes
*/
offsetForArray([year, month, day, hour, minute, second]) {
const timezone = getTimezone(this.name)
month += 1
const time = new ICAL.Time({ year, month, day, hour, minute, second, isDate: false })
// TODO - all these operations require complex RRULE expansion for DST / Standard
// this function is called dozens of dozens of times, result should probably be cached
console.debug(timezone.utcOffset(time) / 60)
return timezone.utcOffset(time) / 60
}
/**
* returns parameters for Date object in this timezone based on given timestamp
*
* @param {Number[]} ms Timestamp in milliseconds
* @returns {Number[]}
*/
timestampToArray(ms) {
const timezone = getTimezone(this.name)
const time = ICAL.Time.fromData({ year: 1970, month: 1, day: 1, hour: 0, minute: 0, second: 0 }) // just create a dummy object because fromUnixTime is not exposed on ICAL.Time
time.fromUnixTime(Math.floor(ms / 1000))
const local = time.convertToZone(timezone)
// TODO - all these operations require complex RRULE expansion for DST / Standard
// this function is called dozens of dozens of times, result should probably be cached
console.debug([local.year, local.month - 1, local.day, local.hour, local.minute, local.second, 0])
return [local.year, local.month - 1, local.day, local.hour, local.minute, local.second, 0]
}
}
registerNamedTimeZoneImpl('vtimezone-timezone', VTimezoneNamedTimezone)

View File

@ -1,7 +1,9 @@
import Vue from 'vue'
import Router from 'vue-router'
import Calendar from './views/Calendar'
import Edit from './views/Edit'
import EditSimple from './views/EditSimple'
import EditSidebar from './views/EditSidebar'
import { dateFactory, getYYYYMMDDFromDate } from './services/date.js'
Vue.use(Router)
@ -13,32 +15,60 @@ const router = new Router({
{
path: '/',
name: 'Root',
component: Calendar,
redirect: {
name: 'View',
name: 'CalendarView',
params: {
view: oca_calendar.initialView,
firstday: getYYYYMMDDFromDate(dateFactory())
},
}
},
// {
// // This route can be used in order to link to events without knowing it's date
// path: '/edit/:object',
// name: 'EditNoDateNoRecurrenceId',
// redirect: {
// name: 'Edit',
//
// }
// },
// {
// // This route can be used in order to link to events without knowing it's date
// path: '/edit/:object/:recurrenceId',
// name: 'EditNoDate',
// redirect: {
// name: 'Edit',
//
// }
// },
{
path: '/:view/:firstday',
component: Calendar,
name: 'CalendarView',
children: [
// {
// path: '',
// name: 'CalendarView',
// },
{
path: '/',
name: 'View',
component: Calendar,
path: '/:view/:firstday/edit/popover/:object/:recurrenceId',
name: 'EditPopoverView',
component: EditSimple,
},
{
path: '/edit/:mode/:object/:recurrenceId',
name: 'Edit',
component: Edit,
path: '/:view/:firstday/edit/sidebar/:object/:recurrenceId',
name: 'EditSidebarView',
component: EditSidebar,
},
{
path: '/new/:mode/:dtstart/:dtend',
name: 'Edit',
component: Edit,
path: '/:view/:firstday/new/popover/:dtstart/:dtend',
name: 'NewPopoverView',
component: EditSimple,
},
{
path: '/:view/:firstday/new/sidebar/:dtstart/:dtend',
name: 'NewSidebarView',
component: EditSidebar,
},
],
},

View File

@ -26,12 +26,19 @@ const colors = []
/**
* get the appropriate text color to be used on top of an rgb value
*
* @param {Number} red decimal value for red
* @param {Number|String} red decimal value for red
* @param {Number} green decimal value for green
* @param {Number} blue decimal value for blue
* @returns {string}
*/
export function generateTextColorFromRGB(red, green, blue) {
if (typeof red === 'string') {
const { r, g, b } = extractRGBFromHexString(red)
red = r
green = g
blue = b
}
const brightness = (((red * 299) + (green * 587) + (blue * 114)) / 1000)
return (brightness > 130) ? '#000000' : '#FAFAFA'
}

View File

@ -0,0 +1,38 @@
/**
* @copyright Copyright (c) 2018 Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* 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/>.
*
*/
/**
*
* @param {Object} calendar calendar object from Vuex store
* @returns {Function}
*/
export default function(calendar) {
return ({ start, startStr, end, endStr, timeZone }, callback) => {
console.debug(calendar)
console.debug(start)
console.debug(startStr)
console.debug(end)
console.debug(endStr)
console.debug(timeZone)
console.debug(callback)
}
}

View File

@ -0,0 +1,102 @@
/**
* @copyright Copyright (c) 2018 Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* 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/>.
*
*/
import tzData from '../../timezones/zones.json'
import ICAL from 'ical.js'
console.debug(`The calendar app is using version ${tzData.version} of the timezone database`)
/**
* Checks whether the given timezone is a so-called Olsen Timezone
* @see https://en.wikipedia.org/wiki/Tz_database
*
* @param {String} tzName name of timezone
* @returns {boolean}
*/
export function isOlsonTimezone(tzName) {
const hasSlash = tzName.indexOf('/') !== -1
const hasSpace = tzName.indexOf(' ') !== -1
const startsWithETC = tzName.startsWith('Etc')
const startsWithUS = tzName.startsWith('US/')
return hasSlash && !hasSpace && !startsWithETC && !startsWithUS
}
/**
* checks if timezone is just an alias
*
* @param {String} tzName name of timezone
* @returns {boolean}
*/
export function isAlias(tzName) {
return tzData.aliases.hasOwnProperty(tzName)
}
/**
* gets timezone object for given name
*
* @param {String} tzName name of timezone
* @returns {icaltimezone}
*/
export function getTimezone(tzName) {
if (isAlias(tzName)) {
return getTimezone(tzData.aliases[tzName])
}
// GMT maps to UTC, so only check UTC
if (tzName === 'UTC') {
return ICAL.TimezoneService.get('UTC')
} else if (tzName === 'floating') {
return ICAL.Timezone.localTimezone
}
if (!tzData.zones.hasOwnProperty(tzName)) {
throw new Error('Unknown timezone')
}
const ics = tzData.zones[tzName].ics
const jCal = ICAL.parse(ics)
const components = new ICAL.Component(jCal)
if (components.name === 'vtimezone') {
return new ICAL.Timezone(components)
} else {
return new ICAL.Timezone(components.getFirstSubcomponent('vtimezone'))
}
}
/**
* lists all timezones available (without aliases)
*
* @returns {String[]}
*/
export function listAllTimezones() {
const olsonAliases = []
tzData.aliases.forEach((value, key) => {
if (isOlsonTimezone(key)) {
olsonAliases.push(key)
}
})
const timezones = Object.keys(tzData.zones).concat(olsonAliases)
timezones.sort()
return timezones
}

View File

@ -0,0 +1,29 @@
/**
* @copyright Copyright (c) 2018 Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* 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/>.
*
*/
import jstz from 'jstz';
/**
* Returns the current timezone of the user
*
* @returns {String} Current timezone of user
*/
export default () => jstz.determine().name

View File

@ -1,39 +1,44 @@
<template>
<div class="view">
<div id="app-content">
<!-- Full calendar -->
<full-calendar />
<full-calendar :events="events" :event-sources="eventSources" :config="config" />
<!-- Edit modal -->
<router-view />
</div>
</template>
<script>
<script>
import FullCalendar from '../components/FullCalendar.vue'
import '../../node_modules/fullcalendar/dist/fullcalendar.css'
import { generateTextColorFromRGB } from '../services/colorService'
import fullCalendarEventService from '../services/fullCalendarEventService'
export default {
name: 'Calendar',
components: {
FullCalendar
},
// props: {
// view: {
// type: String,
// required: true,
// },
// firstday: {
// type: String,
// required: true,
// }
// },
data() {
return {
calendar: null
computed: {
events() {
return []
},
eventSources() {
console.debug(this.$store.getters.enabledCalendars)
return this.$store.getters.enabledCalendars.map((enabledCalendar) => ({
id: enabledCalendar.id,
// coloring
backgroundColor: enabledCalendar.color,
borderColor: enabledCalendar.color,
textColor: generateTextColorFromRGB(enabledCalendar.color),
// html foo
className: enabledCalendar.id,
editable: !enabledCalendar.readOnly,
events: fullCalendarEventService(enabledCalendar),
}))
},
config() {
return {}
}
},
mounted() {
// this.calendar = new Calendar(this.$refs.fullcalendar)
// this.calendar.render()
},
}
}
</script>

View File

@ -1,5 +1,6 @@
<template>
<div class="editor">
<div id="app-sidebar">
FOO VAR
<!-- title -->
<!-- timepicker -->
<!-- details -->
@ -15,6 +16,6 @@
</template>
<script>
export default {
name: 'Edit',
name: 'EditSidebar',
}
</script>

12
src/views/EditSimple.vue Normal file
View File

@ -0,0 +1,12 @@
<template>
<div class="editor">
<!-- title -->
<!-- timepicker -->
<!-- details -->
</div>
</template>
<script>
export default {
name: 'EditSimple',
}
</script>