mirror of
https://bitbucket.org/studiotem/jandl-taskmanager-app.git
synced 2026-10-21 14:29:34 +00:00
vue-cal
This commit is contained in:
1 parent
f6d0764969
commit
944ea92cef
56 files changed
+5535
No files matched your search
@@ -0,0 +1,47 @@
|
|||||||
|
<template lang="pug">
|
||||||
|
.vuecal__flex.vuecal__all-day(:style="cellOrSplitMinWidth && { height }")
|
||||||
|
.vuecal__all-day-text(v-if="!cellOrSplitMinWidth" style="width: 3em")
|
||||||
|
span {{ label }}
|
||||||
|
.vuecal__flex.vuecal__cells(
|
||||||
|
:class="`${view.id}-view`"
|
||||||
|
grow
|
||||||
|
:style="cellOrSplitMinWidth ? `min-width: ${cellOrSplitMinWidth}px` : ''")
|
||||||
|
vuecal-cell(
|
||||||
|
v-for="(cell, i) in cells"
|
||||||
|
:key="i"
|
||||||
|
:options="options"
|
||||||
|
:edit-events="editEvents"
|
||||||
|
:data="cell"
|
||||||
|
:all-day="true"
|
||||||
|
:cell-width="options.hideWeekdays.length && (vuecal.isWeekView || vuecal.isMonthView) && vuecal.cellWidth"
|
||||||
|
:min-timestamp="options.minTimestamp"
|
||||||
|
:max-timestamp="options.maxTimestamp"
|
||||||
|
:cell-splits="daySplits")
|
||||||
|
template(#event="{ event, view }")
|
||||||
|
slot(name="event" :view="view" :event="event")
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Cell from './cell.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
inject: ['vuecal', 'view', 'editEvents'],
|
||||||
|
components: { 'vuecal-cell': Cell },
|
||||||
|
props: {
|
||||||
|
// Vue-cal main component options (props).
|
||||||
|
options: { type: Object, required: true },
|
||||||
|
cells: { type: Array, required: true },
|
||||||
|
label: { type: String, required: true },
|
||||||
|
daySplits: { type: Array, default: () => [] },
|
||||||
|
shortEvents: { type: Boolean, default: true },
|
||||||
|
height: { type: String, default: '' },
|
||||||
|
cellOrSplitMinWidth: { type: Number, default: null }
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
hasCellOrSplitWidth () {
|
||||||
|
return !!(this.options.minCellWidth || (this.daySplits.length && this.options.minSplitWidth))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,615 @@
|
|||||||
|
<template lang="pug">
|
||||||
|
transition-group.vuecal__cell(
|
||||||
|
:class="cellClasses"
|
||||||
|
:name="`slide-fade--${transitionDirection}`"
|
||||||
|
tag="div"
|
||||||
|
:appear="options.transitions"
|
||||||
|
:style="cellStyles")
|
||||||
|
.vuecal__flex.vuecal__cell-content(
|
||||||
|
v-for="(split, i) in (splitsCount ? splits : 1)"
|
||||||
|
:key="options.transitions ? `${view.id}-${data.content}-${i}` : i"
|
||||||
|
:class="splitsCount && splitClasses(split)"
|
||||||
|
:data-split="splitsCount ? split.id : false"
|
||||||
|
column
|
||||||
|
tabindex="0"
|
||||||
|
:aria-label="data.content"
|
||||||
|
@focus="onCellFocus($event)"
|
||||||
|
@keypress.enter="onCellkeyPressEnter($event)"
|
||||||
|
@touchstart="!isDisabled && onCellTouchStart($event, splitsCount ? split.id : null)"
|
||||||
|
@mousedown="!isDisabled && onCellMouseDown($event, splitsCount ? split.id : null)"
|
||||||
|
@click="!isDisabled && onCellClick($event)"
|
||||||
|
@dblclick="!isDisabled && onCellDblClick($event)"
|
||||||
|
@contextmenu="!isDisabled && options.cellContextmenu && onCellContextMenu($event)"
|
||||||
|
@dragenter="!isDisabled && editEvents.drag && dnd && dnd.cellDragEnter($event, $data, data.startDate)"
|
||||||
|
@dragover="!isDisabled && editEvents.drag && dnd && dnd.cellDragOver($event, $data, data.startDate, splitsCount ? split.id : null)"
|
||||||
|
@dragleave="!isDisabled && editEvents.drag && dnd && dnd.cellDragLeave($event, $data, data.startDate)"
|
||||||
|
@drop="!isDisabled && editEvents.drag && dnd && dnd.cellDragDrop($event, $data, data.startDate, splitsCount ? split.id : null)")
|
||||||
|
|
||||||
|
.cell-time-labels(v-if="options.showTimeInCells && options.time && isWeekOrDayView && !allDay")
|
||||||
|
span.cell-time-label(v-for="(cell, i) in vuecal.timeCells" :key="i").
|
||||||
|
{{ cell.label }}
|
||||||
|
|
||||||
|
template(v-if="isWeekOrDayView && !allDay && specialHours.length")
|
||||||
|
.vuecal__special-hours(
|
||||||
|
v-for="(block, i) in specialHours"
|
||||||
|
:class="`vuecal__special-hours--day${block.day} ${block.class}`"
|
||||||
|
:style="`height: ${block.height}px;top: ${block.top}px`")
|
||||||
|
.special-hours-label(v-if="block.label" v-html="block.label")
|
||||||
|
slot(
|
||||||
|
name="cell-content"
|
||||||
|
:events="events"
|
||||||
|
:select-cell="$event => selectCell($event, true)"
|
||||||
|
:split="splitsCount ? split : false")
|
||||||
|
.vuecal__cell-events(
|
||||||
|
v-if="eventsCount && (isWeekOrDayView || (view.id === 'month' && options.eventsOnMonthView))")
|
||||||
|
event(
|
||||||
|
v-for="(event, j) in (splitsCount ? split.events : events)" :key="j"
|
||||||
|
:cell-formatted-date="data.formattedDate"
|
||||||
|
:event="event"
|
||||||
|
:all-day="allDay"
|
||||||
|
:cell-events="splitsCount ? split.events : events"
|
||||||
|
:overlaps="((splitsCount ? split.overlaps[event._eid] : cellOverlaps[event._eid]) || []).overlaps"
|
||||||
|
:event-position="((splitsCount ? split.overlaps[event._eid] : cellOverlaps[event._eid]) || []).position"
|
||||||
|
:overlaps-streak="splitsCount ? split.overlapsStreak : cellOverlapsStreak")
|
||||||
|
template(#event="{ event, view }")
|
||||||
|
slot(name="event" :view="view" :event="event")
|
||||||
|
.vuecal__now-line(
|
||||||
|
v-if="timelineVisible"
|
||||||
|
:style="`top: ${todaysTimePosition}px`"
|
||||||
|
:key="options.transitions ? `${view.id}-now-line` : 'now-line'"
|
||||||
|
:title="utils.date.formatTime(vuecal.now)")
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Event from './event.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
inject: ['vuecal', 'utils', 'modules', 'view', 'domEvents'],
|
||||||
|
components: { Event },
|
||||||
|
props: {
|
||||||
|
// Vue-cal main component options (props).
|
||||||
|
options: { type: Object, default: () => ({}) },
|
||||||
|
editEvents: { type: Object, required: true },
|
||||||
|
data: { type: Object, required: true },
|
||||||
|
cellSplits: { type: Array, default: () => [] },
|
||||||
|
minTimestamp: { type: [Number, null], default: null },
|
||||||
|
maxTimestamp: { type: [Number, null], default: null },
|
||||||
|
cellWidth: { type: [Number, Boolean], default: false },
|
||||||
|
allDay: { type: Boolean, default: false }
|
||||||
|
},
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
cellOverlaps: {},
|
||||||
|
cellOverlapsStreak: 1, // Largest amount of simultaneous events in cell.
|
||||||
|
// On mouse down, save the time at cursor so it can be reused on cell focus event
|
||||||
|
// where there is no cursor coords.
|
||||||
|
timeAtCursor: null,
|
||||||
|
highlighted: false, // On event drag over.
|
||||||
|
highlightedSplit: null
|
||||||
|
}),
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getSplitAtCursor ({ target }) {
|
||||||
|
const targetIsSplit = target.classList.contains('vuecal__cell-split')
|
||||||
|
let split = targetIsSplit ? target : this.vuecal.findAncestor(target, 'vuecal__cell-split')
|
||||||
|
|
||||||
|
if (split) {
|
||||||
|
split = split.attributes['data-split'].value
|
||||||
|
// Convert to a numeric value if split id is a number.
|
||||||
|
if (parseInt(split).toString() === split.toString()) split = parseInt(split)
|
||||||
|
}
|
||||||
|
return split || null
|
||||||
|
},
|
||||||
|
|
||||||
|
splitClasses (split) {
|
||||||
|
return {
|
||||||
|
'vuecal__cell-split': true,
|
||||||
|
'vuecal__cell-split--highlighted': this.highlightedSplit === split.id,
|
||||||
|
[split.class]: !!split.class
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
checkCellOverlappingEvents () {
|
||||||
|
// If splits, checkCellOverlappingEvents() is called from within computed splits.
|
||||||
|
if (this.options.time && this.eventsCount && !this.splitsCount) {
|
||||||
|
if (this.eventsCount === 1) {
|
||||||
|
this.cellOverlaps = []
|
||||||
|
this.cellOverlapsStreak = 1
|
||||||
|
}
|
||||||
|
// If only 1 event remains re-init the overlaps.
|
||||||
|
else [this.cellOverlaps, this.cellOverlapsStreak] = this.utils.event.checkCellOverlappingEvents(this.events, this.options)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isDOMElementAnEvent (el) {
|
||||||
|
return this.vuecal.isDOMElementAnEvent(el)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a cell and go to narrower view on double click or single click according to vuecalProps option.
|
||||||
|
*/
|
||||||
|
selectCell (DOMEvent, force = false) {
|
||||||
|
// If splitting days, also return the clicked split on cell click when emitting event.
|
||||||
|
const split = this.splitsCount ? this.getSplitAtCursor(DOMEvent) : null
|
||||||
|
|
||||||
|
this.utils.cell.selectCell(force, this.timeAtCursor, split)
|
||||||
|
this.timeAtCursor = null
|
||||||
|
},
|
||||||
|
|
||||||
|
onCellkeyPressEnter (DOMEvent) {
|
||||||
|
if (!this.isSelected) this.onCellFocus(DOMEvent)
|
||||||
|
|
||||||
|
// If splitting days, also return the clicked split on cell keypress when emitting event.
|
||||||
|
const split = this.splitsCount ? this.getSplitAtCursor(DOMEvent) : null
|
||||||
|
|
||||||
|
this.utils.cell.keyPressEnterCell(this.timeAtCursor, split)
|
||||||
|
this.timeAtCursor = null
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On cell focus, from tab key or from mousedown, highlight the cell and emit
|
||||||
|
* the cell-focus event with the date of the cell start when focusing from tab or
|
||||||
|
* the date & time at cursor if click/touch.
|
||||||
|
*/
|
||||||
|
onCellFocus (DOMEvent) {
|
||||||
|
if (!this.isSelected && !this.isDisabled) {
|
||||||
|
this.isSelected = this.data.startDate // Highlight the cell.
|
||||||
|
|
||||||
|
// If splitting days, also return the clicked split on cell focus when emitting event.
|
||||||
|
const split = this.splitsCount ? this.getSplitAtCursor(DOMEvent) : null
|
||||||
|
|
||||||
|
// Cell-focus event returns the cell start date (at midnight) if triggered from tab key,
|
||||||
|
// or cursor coords time if clicked.
|
||||||
|
const date = this.timeAtCursor || this.data.startDate
|
||||||
|
this.vuecal.$emit('cell-focus', split ? { date, split } : date)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onCellMouseDown (DOMEvent, split = null, touch = false) {
|
||||||
|
// Prevent a double mouse down on touch devices.
|
||||||
|
if ('ontouchstart' in window && !touch) return false
|
||||||
|
|
||||||
|
if (!this.isSelected) this.onCellFocus(DOMEvent)
|
||||||
|
|
||||||
|
const { clickHoldACell, focusAnEvent } = this.domEvents
|
||||||
|
// Reinit the click trigger on each mousedown.
|
||||||
|
// In some cases we explicitly set this flag to prevent the click event to trigger,
|
||||||
|
// and cancel event creation.
|
||||||
|
this.domEvents.cancelClickEventCreation = false
|
||||||
|
// Also reinit this var on each mousedown.
|
||||||
|
clickHoldACell.eventCreated = false
|
||||||
|
|
||||||
|
this.timeAtCursor = new Date(this.data.startDate)
|
||||||
|
const { minutes, cursorCoords: { y } } = this.vuecal.minutesAtCursor(DOMEvent)
|
||||||
|
this.timeAtCursor.setMinutes(minutes)
|
||||||
|
|
||||||
|
const mouseDownOnEvent = this.isDOMElementAnEvent(DOMEvent.target)
|
||||||
|
// Unfocus an event if any is focused and clicking on cell outside of an event.
|
||||||
|
if (!mouseDownOnEvent && focusAnEvent._eid) {
|
||||||
|
(this.view.events.find(e => e._eid === focusAnEvent._eid) || {}).focused = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only if event creation is allowed and mousedown is on a cell (not on event).
|
||||||
|
if (this.editEvents.create && !mouseDownOnEvent) this.setUpEventCreation(DOMEvent, y)
|
||||||
|
},
|
||||||
|
|
||||||
|
setUpEventCreation (DOMEvent, startCursorY) {
|
||||||
|
// If dragToCreateEvent is true, start the event creation from dragging
|
||||||
|
// only on week and day views (doesn't make sense on month view).
|
||||||
|
if (this.options.dragToCreateEvent && ['week', 'day'].includes(this.view.id)) {
|
||||||
|
const { dragCreateAnEvent } = this.domEvents
|
||||||
|
dragCreateAnEvent.startCursorY = startCursorY
|
||||||
|
|
||||||
|
// If splitting days, store the clicked split to create an event in it from the global
|
||||||
|
// mousemove handler in index.vue.
|
||||||
|
dragCreateAnEvent.split = this.splitsCount ? this.getSplitAtCursor(DOMEvent) : null
|
||||||
|
|
||||||
|
// Save the time at cursor on initial mousedown.
|
||||||
|
dragCreateAnEvent.start = this.timeAtCursor
|
||||||
|
|
||||||
|
// If snapToTime, set the start to the closest intervaled number.
|
||||||
|
if (this.options.snapToTime) {
|
||||||
|
let timeMinutes = this.timeAtCursor.getHours() * 60 + this.timeAtCursor.getMinutes()
|
||||||
|
const plusHalfSnapTime = timeMinutes + this.options.snapToTime / 2
|
||||||
|
timeMinutes = plusHalfSnapTime - (plusHalfSnapTime % this.options.snapToTime)
|
||||||
|
|
||||||
|
dragCreateAnEvent.start.setHours(0, timeMinutes, 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the cellClickHold option is true and not mousedown on an event, click & hold to create an event.
|
||||||
|
else if (this.options.cellClickHold && ['month', 'week', 'day'].includes(this.view.id)) {
|
||||||
|
this.setUpCellHoldTimer(DOMEvent)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// When click & holding a cell, and if allowed, set a timeout to create an event (can be cancelled).
|
||||||
|
setUpCellHoldTimer (DOMEvent) {
|
||||||
|
const { clickHoldACell } = this.domEvents
|
||||||
|
clickHoldACell.cellId = `${this.vuecal._.uid}_${this.data.formattedDate}`
|
||||||
|
// If splitting days, store the clicked split to create an event in it from the global
|
||||||
|
// mousemove handler in index.vue.
|
||||||
|
clickHoldACell.split = this.splitsCount ? this.getSplitAtCursor(DOMEvent) : null
|
||||||
|
|
||||||
|
clickHoldACell.timeoutId = setTimeout(() => {
|
||||||
|
if (clickHoldACell.cellId && !this.domEvents.cancelClickEventCreation) {
|
||||||
|
const { _eid } = this.utils.event.createAnEvent(
|
||||||
|
this.timeAtCursor,
|
||||||
|
null,
|
||||||
|
clickHoldACell.split ? { split: clickHoldACell.split } : {}
|
||||||
|
)
|
||||||
|
|
||||||
|
clickHoldACell.eventCreated = _eid
|
||||||
|
}
|
||||||
|
}, clickHoldACell.timeout)
|
||||||
|
},
|
||||||
|
|
||||||
|
onCellTouchStart (DOMEvent, split = null) {
|
||||||
|
// If not mousedown on an event.
|
||||||
|
this.onCellMouseDown(DOMEvent, split, true)
|
||||||
|
},
|
||||||
|
|
||||||
|
onCellClick (DOMEvent) {
|
||||||
|
if (!this.isDOMElementAnEvent(DOMEvent.target)) this.selectCell(DOMEvent)
|
||||||
|
},
|
||||||
|
|
||||||
|
onCellDblClick (DOMEvent) {
|
||||||
|
const date = new Date(this.data.startDate)
|
||||||
|
date.setMinutes(this.vuecal.minutesAtCursor(DOMEvent).minutes)
|
||||||
|
|
||||||
|
// If splitting days, also return the clicked split on cell dblclick when emitting event.
|
||||||
|
const split = this.splitsCount ? this.getSplitAtCursor(DOMEvent) : null
|
||||||
|
|
||||||
|
this.vuecal.$emit('cell-dblclick', split ? { date, split } : date)
|
||||||
|
|
||||||
|
if (this.options.dblclickToNavigate) this.vuecal.switchToNarrowerView()
|
||||||
|
},
|
||||||
|
|
||||||
|
onCellContextMenu (DOMEvent) {
|
||||||
|
DOMEvent.stopPropagation()
|
||||||
|
DOMEvent.preventDefault()
|
||||||
|
|
||||||
|
const date = new Date(this.data.startDate)
|
||||||
|
const { cursorCoords, minutes } = this.vuecal.minutesAtCursor(DOMEvent)
|
||||||
|
date.setMinutes(minutes)
|
||||||
|
|
||||||
|
// If splitting days, also return the clicked split on cell contextmenu when emitting event.
|
||||||
|
const split = this.splitsCount ? this.getSplitAtCursor(DOMEvent) : null
|
||||||
|
|
||||||
|
this.vuecal.$emit('cell-contextmenu', { date, ...cursorCoords, ...(split || {}), e: DOMEvent })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
// Drag & drop module.
|
||||||
|
dnd () {
|
||||||
|
return this.modules.dnd
|
||||||
|
},
|
||||||
|
nowInMinutes () {
|
||||||
|
return this.utils.date.dateToMinutes(this.vuecal.now)
|
||||||
|
},
|
||||||
|
isBeforeMinDate () {
|
||||||
|
return this.minTimestamp !== null && this.minTimestamp > this.data.endDate.getTime()
|
||||||
|
},
|
||||||
|
isAfterMaxDate () {
|
||||||
|
return this.maxTimestamp && this.maxTimestamp < this.data.startDate.getTime()
|
||||||
|
},
|
||||||
|
// Is the current cell disabled or not.
|
||||||
|
isDisabled () {
|
||||||
|
const { disableDays } = this.options
|
||||||
|
const { isYearsOrYearView } = this.vuecal
|
||||||
|
if (disableDays.length && disableDays.includes(this.data.formattedDate) && !isYearsOrYearView) return true
|
||||||
|
return this.isBeforeMinDate || this.isAfterMaxDate
|
||||||
|
},
|
||||||
|
// Is the current cell selected or not.
|
||||||
|
isSelected: {
|
||||||
|
get () {
|
||||||
|
let selected = false
|
||||||
|
const { selectedDate } = this.view
|
||||||
|
|
||||||
|
if (this.view.id === 'years') {
|
||||||
|
selected = selectedDate.getFullYear() === this.data.startDate.getFullYear()
|
||||||
|
}
|
||||||
|
else if (this.view.id === 'year') {
|
||||||
|
selected = (selectedDate.getFullYear() === this.data.startDate.getFullYear() &&
|
||||||
|
selectedDate.getMonth() === this.data.startDate.getMonth())
|
||||||
|
}
|
||||||
|
else selected = selectedDate.getTime() === this.data.startDate.getTime()
|
||||||
|
|
||||||
|
return selected
|
||||||
|
},
|
||||||
|
set (date) {
|
||||||
|
this.view.selectedDate = date
|
||||||
|
this.vuecal.$emit('update:selected-date', this.view.selectedDate)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Cache result for performance.
|
||||||
|
isWeekOrDayView () {
|
||||||
|
return ['week', 'day'].includes(this.view.id)
|
||||||
|
},
|
||||||
|
transitionDirection () {
|
||||||
|
return this.vuecal.transitionDirection
|
||||||
|
},
|
||||||
|
specialHours () {
|
||||||
|
// this.data.specialHours is always an array, but may be empty.
|
||||||
|
return this.data.specialHours.map(block => {
|
||||||
|
let { from, to } = block
|
||||||
|
from = Math.max(from, this.options.timeFrom)
|
||||||
|
to = Math.min(to, this.options.timeTo)
|
||||||
|
return {
|
||||||
|
...block,
|
||||||
|
height: (to - from) * this.timeScale,
|
||||||
|
top: (from - this.options.timeFrom) * this.timeScale
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
events () {
|
||||||
|
const { startDate: cellStart, endDate: cellEnd } = this.data
|
||||||
|
let events = []
|
||||||
|
|
||||||
|
// Calculate events on month/week/day views or years/year if eventsCountOnYearView.
|
||||||
|
if (!(['years', 'year'].includes(this.view.id) && !this.options.eventsCountOnYearView)) {
|
||||||
|
// Means that when vuecal.view.events changes all the cells will be looking up new value. :/
|
||||||
|
// Also clone array to prevent modifying original.
|
||||||
|
events = this.view.events.slice(0)
|
||||||
|
|
||||||
|
if (this.view.id === 'month') {
|
||||||
|
events.push(...this.view.outOfScopeEvents)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only keep events in cell time range.
|
||||||
|
events = events.filter(e => this.utils.event.eventInRange(e, cellStart, cellEnd))
|
||||||
|
|
||||||
|
if (this.options.showAllDayEvents && this.view.id !== 'month') events = events.filter(e => !!e.allDay === this.allDay)
|
||||||
|
|
||||||
|
// From events in view, filter the ones that are out of `time-from`-`time-to` range in this cell.
|
||||||
|
if (this.options.time && this.isWeekOrDayView && !this.allDay) {
|
||||||
|
const { timeFrom, timeTo } = this.options
|
||||||
|
|
||||||
|
events = events.filter(e => {
|
||||||
|
const segment = (e.daysCount > 1 && e.segments[this.data.formattedDate]) || {}
|
||||||
|
const singleDayInRange = e.daysCount === 1 && e.startTimeMinutes < timeTo && e.endTimeMinutes > timeFrom
|
||||||
|
const multipleDayInRange = e.daysCount > 1 && (segment.startTimeMinutes < timeTo && segment.endTimeMinutes > timeFrom)
|
||||||
|
const recurrMultDayInRange = false // e.daysCount > 1 && e.repeat && recurringEventInRange(e, cellStart, cellEnd)
|
||||||
|
return (e.allDay || singleDayInRange || multipleDayInRange || recurrMultDayInRange)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position events with time in the timeline when there is a timeline and not in allDay slot.
|
||||||
|
if (this.options.time && this.isWeekOrDayView && !(this.options.showAllDayEvents && this.allDay)) {
|
||||||
|
// Sort events in chronological order.
|
||||||
|
events.sort((a, b) => a.start < b.start ? -1 : 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If splits, checkCellOverlappingEvents() is called from within computed splits.
|
||||||
|
if (!this.cellSplits.length) this.$nextTick(this.checkCellOverlappingEvents)
|
||||||
|
}
|
||||||
|
|
||||||
|
return events
|
||||||
|
},
|
||||||
|
eventsCount () {
|
||||||
|
return this.events.length
|
||||||
|
},
|
||||||
|
splits () {
|
||||||
|
return this.cellSplits.map((item, i) => {
|
||||||
|
const events = this.events.filter(e => e.split === item.id)
|
||||||
|
const [overlaps, streak] = this.utils.event.checkCellOverlappingEvents(events.filter(e => !e.background && !e.allDay), this.options)
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
overlaps,
|
||||||
|
overlapsStreak: streak,
|
||||||
|
events
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
splitsCount () {
|
||||||
|
return this.splits.length
|
||||||
|
},
|
||||||
|
cellClasses () {
|
||||||
|
return {
|
||||||
|
[this.data.class]: !!this.data.class,
|
||||||
|
'vuecal__cell--current': this.data.current, // E.g. Current year in years view.
|
||||||
|
'vuecal__cell--today': this.data.today,
|
||||||
|
'vuecal__cell--out-of-scope': this.data.outOfScope,
|
||||||
|
'vuecal__cell--before-min': this.isDisabled && this.isBeforeMinDate,
|
||||||
|
'vuecal__cell--after-max': this.isDisabled && this.isAfterMaxDate,
|
||||||
|
'vuecal__cell--disabled': this.isDisabled,
|
||||||
|
'vuecal__cell--selected': this.isSelected,
|
||||||
|
'vuecal__cell--highlighted': this.highlighted,
|
||||||
|
'vuecal__cell--has-splits': this.splitsCount,
|
||||||
|
'vuecal__cell--has-events': this.eventsCount
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cellStyles () {
|
||||||
|
return {
|
||||||
|
// cellWidth is only applied when hiding weekdays on month and week views.
|
||||||
|
...(this.cellWidth ? { width: `${this.cellWidth}%` } : {})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
timelineVisible () {
|
||||||
|
const { time, timeTo } = this.options
|
||||||
|
return this.data.today && this.isWeekOrDayView && time && !this.allDay && this.nowInMinutes <= timeTo
|
||||||
|
},
|
||||||
|
todaysTimePosition () {
|
||||||
|
// Skip the Maths if not relevant.
|
||||||
|
if (!this.data.today || !this.options.time) return
|
||||||
|
|
||||||
|
const minutesFromTop = this.nowInMinutes - this.options.timeFrom
|
||||||
|
return Math.round(minutesFromTop * this.timeScale)
|
||||||
|
},
|
||||||
|
timeScale () {
|
||||||
|
return this.options.timeCellHeight / this.options.timeStep
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.vuecal__cell {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
transition: 0.15s ease-in-out background-color;
|
||||||
|
|
||||||
|
// Cell modifiers.
|
||||||
|
// -------------------------------------------------
|
||||||
|
.vuecal__cells.month-view &,
|
||||||
|
.vuecal__cells.week-view & {
|
||||||
|
width: 14.2857%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuecal--hide-weekends .vuecal__cells.month-view &,
|
||||||
|
.vuecal--hide-weekends .vuecal__cells.week-view & {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuecal__cells.years-view & {width: 20%;}
|
||||||
|
.vuecal__cells.year-view & {width: 33.33%;}
|
||||||
|
.vuecal__cells.day-view & {flex: 1;}
|
||||||
|
.vuecal--overflow-x.vuecal--day-view & {width: auto;}
|
||||||
|
|
||||||
|
.vuecal--click-to-navigate &:not(&--disabled) {cursor: pointer;}
|
||||||
|
.vuecal--view-with-time &,
|
||||||
|
.vuecal--week-view.vuecal--no-time &:not(.vuecal__cell--has-splits),
|
||||||
|
.vuecal--day-view.vuecal--no-time &:not(.vuecal__cell--has-splits) {display: block;}
|
||||||
|
|
||||||
|
&.vuecal__cell--has-splits {
|
||||||
|
flex-direction: row;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
z-index: 0;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: -1px;
|
||||||
|
bottom: -1px;
|
||||||
|
border: 1px solid rgba(196, 196, 196, 0.25);
|
||||||
|
}
|
||||||
|
.vuecal--overflow-x.vuecal--day-view &:before {bottom: 0;}
|
||||||
|
|
||||||
|
&--today,
|
||||||
|
&--current {
|
||||||
|
background-color: rgba(240, 240, 255, 0.4);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--selected {
|
||||||
|
background-color: rgba(235, 255, 245, 0.4);
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
.vuecal--day-view & {background: none;}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--out-of-scope {color: rgba(0, 0, 0, 0.25);}
|
||||||
|
&--disabled {color: rgba(0, 0, 0, 0.25);cursor: not-allowed;}
|
||||||
|
|
||||||
|
// Cells/splits get highlighted when dragging an event over it.
|
||||||
|
&--highlighted:not(.vuecal__cell--has-splits), &-split.vuecal__cell-split--highlighted {
|
||||||
|
background-color: rgba(0, 0, 0, 0.04);
|
||||||
|
// Drag over feedback must be fast. Then it can fade away with longer duration.
|
||||||
|
transition-duration: 5ms;
|
||||||
|
}
|
||||||
|
// -------------------------------------------------
|
||||||
|
|
||||||
|
&-content {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
.vuecal--years-view &,
|
||||||
|
.vuecal--year-view &,
|
||||||
|
.vuecal--month-view & {justify-content: center;}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-time-labels {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.cell-time-label {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 0.8em;
|
||||||
|
opacity: 0.3;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
// .cell-time-label:hover {opacity: 0.7;}
|
||||||
|
|
||||||
|
&-split {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
transition: 0.15s ease-in-out background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-events {width: 100%;}
|
||||||
|
|
||||||
|
&-events-count {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 65%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
min-width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
line-height: 12px;
|
||||||
|
padding: 0 3px;
|
||||||
|
background: #999;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuecal__special-hours {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuecal--overflow-x.vuecal--week-view .vuecal__cell, .vuecal__cell-split {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuecal__no-event {
|
||||||
|
padding-top: 1em;
|
||||||
|
color: #aaa;
|
||||||
|
justify-self: flex-start;
|
||||||
|
margin-bottom: auto; // Vertical align top within flex column and align center.
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuecal__all-day .vuecal__no-event {display: none;}
|
||||||
|
|
||||||
|
.vuecal__now-line {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
color: red;
|
||||||
|
border-top: 1px solid currentColor;
|
||||||
|
opacity: 0.6;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: -6px;
|
||||||
|
left: 0;
|
||||||
|
border: 5px solid transparent;
|
||||||
|
border-left-color: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,426 @@
|
|||||||
|
<template lang="pug">
|
||||||
|
.vuecal__event(
|
||||||
|
:class="eventClasses"
|
||||||
|
:style="eventStyles"
|
||||||
|
tabindex="0"
|
||||||
|
@focus="focusEvent"
|
||||||
|
@keypress.enter.stop="onEnterKeypress"
|
||||||
|
@mouseenter="onMouseEnter"
|
||||||
|
@mouseleave="onMouseLeave"
|
||||||
|
@touchstart.stop="onTouchStart"
|
||||||
|
@mousedown="onMouseDown($event) /* Don't stop mousedown propagation. See in onMouseDown(). */"
|
||||||
|
@mouseup="onMouseUp"
|
||||||
|
@touchend="onMouseUp"
|
||||||
|
@touchmove="onTouchMove"
|
||||||
|
@dblclick="onDblClick"
|
||||||
|
:draggable="draggable"
|
||||||
|
@dragstart="draggable && onDragStart($event)"
|
||||||
|
@dragend="draggable && onDragEnd()")
|
||||||
|
.vuecal__event-delete(
|
||||||
|
v-if="vuecal.editEvents.delete && event.deletable"
|
||||||
|
@click.stop="deleteEvent"
|
||||||
|
@touchstart.stop="touchDeleteEvent") {{ vuecal.texts.deleteEvent }}
|
||||||
|
slot(name="event" :event="event" :view="view.id")
|
||||||
|
//- Force contenteditable="false" for new events without content.
|
||||||
|
.vuecal__event-resize-handle(
|
||||||
|
v-if="resizable"
|
||||||
|
contenteditable="false"
|
||||||
|
@mousedown.stop.prevent="onResizeHandleMouseDown"
|
||||||
|
@touchstart.stop.prevent="onResizeHandleMouseDown")
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
inject: ['vuecal', 'utils', 'modules', 'view', 'domEvents', 'editEvents'],
|
||||||
|
props: {
|
||||||
|
cellFormattedDate: { type: String, default: '' },
|
||||||
|
event: { type: Object, default: () => ({}) },
|
||||||
|
cellEvents: { type: Array, default: () => [] },
|
||||||
|
overlaps: { type: Array, default: () => [] },
|
||||||
|
// If multiple simultaneous events, the events are placed from left to right from the
|
||||||
|
// one starting first to the last. (See utils/event.js > checkCellOverlappingEvents)
|
||||||
|
eventPosition: { type: Number, default: 0 },
|
||||||
|
overlapsStreak: { type: Number, default: 0 },
|
||||||
|
allDay: { type: Boolean, default: false } // Is the event displayed in the all-day bar.
|
||||||
|
},
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
// Event touch detection with 30px threshold.
|
||||||
|
touch: {
|
||||||
|
dragThreshold: 30, // px.
|
||||||
|
startX: 0,
|
||||||
|
startY: 0,
|
||||||
|
// Detect if the event touch start + touch end was a drag or a tap.
|
||||||
|
// If it was a drag, don't call the event-click handler.
|
||||||
|
dragged: false
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* On event mousedown.
|
||||||
|
* Do not prevent propagation to trigger cell mousedown which highlights the cell if not highlighted.
|
||||||
|
*/
|
||||||
|
onMouseDown (e, touch = false) {
|
||||||
|
// Prevent a double mouse down on touch devices.
|
||||||
|
if ('ontouchstart' in window && !touch) return false
|
||||||
|
const { clickHoldAnEvent, focusAnEvent, resizeAnEvent, dragAnEvent } = this.domEvents
|
||||||
|
|
||||||
|
// If the delete button is already out and event is on focus then delete event.
|
||||||
|
// Return true so the event-click function (if any) is not called.
|
||||||
|
if (focusAnEvent._eid === this.event._eid && clickHoldAnEvent._eid === this.event._eid) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus the clicked event.
|
||||||
|
this.focusEvent()
|
||||||
|
|
||||||
|
clickHoldAnEvent._eid = null // Reinit click hold on each click.
|
||||||
|
|
||||||
|
// Show event delete button.
|
||||||
|
if (this.vuecal.editEvents.delete && this.event.deletable) {
|
||||||
|
clickHoldAnEvent.timeoutId = setTimeout(() => {
|
||||||
|
if (!resizeAnEvent._eid && !dragAnEvent._eid) {
|
||||||
|
clickHoldAnEvent._eid = this.event._eid
|
||||||
|
this.event.deleting = true
|
||||||
|
}
|
||||||
|
}, clickHoldAnEvent.timeout)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mouseup handler is global (whole document) and initialized in index.vue on mounted.
|
||||||
|
* It handles the mouseup on cell, events, and everything.
|
||||||
|
* All mouseup on event, should be put there to avoid conflicts with other cases.
|
||||||
|
* This function is also called on touchend on the event.
|
||||||
|
*/
|
||||||
|
onMouseUp (e) {
|
||||||
|
// Don't allow mouseup to be fired on different event than mousedown for the onEventClick function.
|
||||||
|
if (this.domEvents.focusAnEvent._eid === this.event._eid && !this.touch.dragged) {
|
||||||
|
// This is used in the global mouseup handler.
|
||||||
|
this.domEvents.focusAnEvent.mousedUp = true
|
||||||
|
}
|
||||||
|
this.touch.dragged = false // After the touchend happens, reset the dragged flag.
|
||||||
|
},
|
||||||
|
|
||||||
|
onMouseEnter (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.vuecal.emitWithEvent('event-mouse-enter', this.event)
|
||||||
|
},
|
||||||
|
|
||||||
|
onMouseLeave (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.vuecal.emitWithEvent('event-mouse-leave', this.event)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Detect if user taps on an event or drags it. If dragging, don't fire the event-click handler (if any).
|
||||||
|
onTouchMove (e) {
|
||||||
|
// Skip the maths if there is no event click handler.
|
||||||
|
if (typeof this.vuecal.onEventClick !== 'function') return
|
||||||
|
|
||||||
|
const { clientX, clientY } = e.touches[0]
|
||||||
|
const { startX, startY, dragThreshold } = this.touch
|
||||||
|
if (Math.abs(clientX - startX) > dragThreshold || Math.abs(clientY - startY) > dragThreshold) {
|
||||||
|
this.touch.dragged = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onTouchStart (e) {
|
||||||
|
this.touch.startX = e.touches[0].clientX
|
||||||
|
this.touch.startY = e.touches[0].clientY
|
||||||
|
this.onMouseDown(e, true)
|
||||||
|
},
|
||||||
|
|
||||||
|
onEnterKeypress (e) {
|
||||||
|
if (typeof this.vuecal.onEventClick === 'function') return this.vuecal.onEventClick(this.event, e)
|
||||||
|
},
|
||||||
|
|
||||||
|
onDblClick (e) {
|
||||||
|
if (typeof this.vuecal.onEventDblclick === 'function') return this.vuecal.onEventDblclick(this.event, e)
|
||||||
|
},
|
||||||
|
|
||||||
|
onDragStart (e) {
|
||||||
|
this.dnd && this.dnd.eventDragStart(e, this.event)
|
||||||
|
},
|
||||||
|
|
||||||
|
onDragEnd () {
|
||||||
|
this.dnd && this.dnd.eventDragEnd(this.event)
|
||||||
|
},
|
||||||
|
|
||||||
|
onResizeHandleMouseDown () {
|
||||||
|
this.focusEvent()
|
||||||
|
|
||||||
|
this.domEvents.dragAnEvent._eid = null
|
||||||
|
this.domEvents.resizeAnEvent = Object.assign(this.domEvents.resizeAnEvent, {
|
||||||
|
_eid: this.event._eid,
|
||||||
|
start: (this.segment || this.event).start,
|
||||||
|
split: this.event.split || null,
|
||||||
|
segment: !!this.segment && this.utils.date.formatDateLite(this.segment.start),
|
||||||
|
originalEnd: new Date((this.segment || this.event).end),
|
||||||
|
originalEndTimeMinutes: this.event.endTimeMinutes
|
||||||
|
})
|
||||||
|
|
||||||
|
this.event.resizing = true
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteEvent (touch = false) {
|
||||||
|
// Prevent a double mouse down on touch devices.
|
||||||
|
if ('ontouchstart' in window && !touch) return false
|
||||||
|
|
||||||
|
this.utils.event.deleteAnEvent(this.event)
|
||||||
|
},
|
||||||
|
|
||||||
|
touchDeleteEvent (event) {
|
||||||
|
this.deleteEvent(true)
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelDeleteEvent () {
|
||||||
|
this.event.deleting = false
|
||||||
|
},
|
||||||
|
|
||||||
|
focusEvent () {
|
||||||
|
const { focusAnEvent } = this.domEvents
|
||||||
|
const focusedEvent = focusAnEvent._eid
|
||||||
|
|
||||||
|
// If event is already focus cancel refocusing.
|
||||||
|
if (focusedEvent === this.event._eid) return
|
||||||
|
// Unfocus previous event if any.
|
||||||
|
else if (focusedEvent) {
|
||||||
|
const event = this.view.events.find(e => e._eid === focusedEvent)
|
||||||
|
if (event) event.focused = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel delete on previous event if any.
|
||||||
|
this.vuecal.cancelDelete()
|
||||||
|
|
||||||
|
this.vuecal.emitWithEvent('event-focus', this.event)
|
||||||
|
|
||||||
|
focusAnEvent._eid = this.event._eid
|
||||||
|
this.event.focused = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
eventDimensions () {
|
||||||
|
const { startTimeMinutes, endTimeMinutes } = this.segment || this.event
|
||||||
|
|
||||||
|
// Top of event.
|
||||||
|
let minutesFromTop = startTimeMinutes - this.vuecal.timeFrom
|
||||||
|
const top = Math.max(Math.round(minutesFromTop * this.vuecal.timeCellHeight / this.vuecal.timeStep), 0)
|
||||||
|
|
||||||
|
// Bottom of event.
|
||||||
|
minutesFromTop = Math.min(endTimeMinutes, this.vuecal.timeTo) - this.vuecal.timeFrom
|
||||||
|
const bottom = Math.round(minutesFromTop * this.vuecal.timeCellHeight / this.vuecal.timeStep)
|
||||||
|
|
||||||
|
const height = Math.max(bottom - top, 5) // Min height is 5px.
|
||||||
|
|
||||||
|
return { top, height }
|
||||||
|
},
|
||||||
|
|
||||||
|
eventStyles () {
|
||||||
|
if (this.event.allDay || !this.vuecal.time || !this.event.endTimeMinutes || this.view.id === 'month' || this.allDay) return {}
|
||||||
|
let width = 100 / Math.min(this.overlaps.length + 1, this.overlapsStreak)
|
||||||
|
let left = (100 / (this.overlaps.length + 1)) * this.eventPosition
|
||||||
|
|
||||||
|
if (this.vuecal.minEventWidth && width < this.vuecal.minEventWidth) {
|
||||||
|
width = this.vuecal.minEventWidth
|
||||||
|
left = ((100 - this.vuecal.minEventWidth) / this.overlaps.length) * this.eventPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
const { top, height } = this.eventDimensions
|
||||||
|
|
||||||
|
return {
|
||||||
|
top: `${top}px`,
|
||||||
|
height: `${height}px`,
|
||||||
|
width: `${width}%`,
|
||||||
|
left: (this.event.left && `${this.event.left}px`) || `${left}%`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Don't rely on global variables otherwise whenever it would change all the events would be redrawn.
|
||||||
|
eventClasses () {
|
||||||
|
const { isFirstDay, isLastDay } = this.segment || {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
[this.event.class]: !!this.event.class,
|
||||||
|
'vuecal__event--focus': this.event.focused,
|
||||||
|
'vuecal__event--resizing': this.event.resizing,
|
||||||
|
'vuecal__event--background': this.event.background,
|
||||||
|
'vuecal__event--deletable': this.event.deleting,
|
||||||
|
'vuecal__event--all-day': this.event.allDay,
|
||||||
|
// Only apply the dragging class on the event copy that is being dragged.
|
||||||
|
'vuecal__event--dragging': !this.event.draggingStatic && this.event.dragging,
|
||||||
|
// Only apply the static class on the event original that remains static while a copy is being dragged.
|
||||||
|
// Sometimes when dragging fast the static class would get stuck and events stays invisible,
|
||||||
|
// So if dragging is false disable the static class as well.
|
||||||
|
'vuecal__event--static': this.event.dragging && this.event.draggingStatic,
|
||||||
|
// Multiple days events.
|
||||||
|
'vuecal__event--multiple-days': !!this.segment,
|
||||||
|
'event-start': this.segment && isFirstDay && !isLastDay,
|
||||||
|
'event-middle': this.segment && !isFirstDay && !isLastDay,
|
||||||
|
'event-end': this.segment && isLastDay && !isFirstDay
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// When multiple-day events, a segment is a portion of event spanning on 1 day.
|
||||||
|
segment () {
|
||||||
|
return (this.event.segments && this.event.segments[this.cellFormattedDate]) || null
|
||||||
|
},
|
||||||
|
draggable () {
|
||||||
|
const { draggable, background, daysCount } = this.event
|
||||||
|
return this.vuecal.editEvents.drag && draggable && !background && daysCount === 1
|
||||||
|
},
|
||||||
|
resizable () {
|
||||||
|
const { editEvents, time } = this.vuecal
|
||||||
|
return (editEvents.resize && this.event.resizable && time && !this.allDay &&
|
||||||
|
(!this.segment || (this.segment && this.segment.isLastDay)) && this.view.id !== 'month')
|
||||||
|
},
|
||||||
|
// Drag & drop module.
|
||||||
|
dnd () {
|
||||||
|
return this.modules.dnd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.vuecal__event {
|
||||||
|
color: #666;
|
||||||
|
background-color: rgba(248, 248, 248, 0.8);
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
transition: box-shadow 0.3s, left 0.3s, width 0.3s;
|
||||||
|
overflow: hidden;// For sliding delete button.
|
||||||
|
|
||||||
|
// If nothing is shown inside, still make the event visible.
|
||||||
|
.vuecal--no-time & {min-height: 8px;}
|
||||||
|
|
||||||
|
.vuecal:not(.vuecal--dragging-event) &:hover {z-index: 2;}
|
||||||
|
|
||||||
|
// Reactivate user selection in events.
|
||||||
|
.vuecal__cell & * {user-select: text;}
|
||||||
|
|
||||||
|
.vuecal--view-with-time &:not(&--all-day) {position: absolute;}
|
||||||
|
|
||||||
|
.vuecal--view-with-time .vuecal__bg &--all-day {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 0;
|
||||||
|
opacity: 0.6;
|
||||||
|
width: auto;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuecal--view-with-time .vuecal__all-day &--all-day {
|
||||||
|
position: relative;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--background {z-index: 0;}
|
||||||
|
&--focus, &:focus {box-shadow: 1px 1px 6px rgba(0,0,0,0.2);z-index: 3;outline: none;}
|
||||||
|
|
||||||
|
&.vuecal__event--dragging {opacity: 0.7;}
|
||||||
|
&.vuecal__event--static {opacity: 0;transition: opacity 0.1s;}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Firefox sets a half opacity already, so don't dim the element being dragged.
|
||||||
|
@-moz-document url-prefix() {
|
||||||
|
.vuecal__event.vuecal__event--dragging {opacity: 1;}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuecal__event-resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1em;
|
||||||
|
background-color: rgba(255, 255, 255, 0.3);
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(110%);
|
||||||
|
transition: 0.3s;
|
||||||
|
cursor: ns-resize;
|
||||||
|
|
||||||
|
.vuecal__event:hover &,
|
||||||
|
.vuecal__event:focus &,
|
||||||
|
.vuecal__event--focus &,
|
||||||
|
.vuecal__event--resizing & {opacity: 1;transform: translateY(0);}
|
||||||
|
.vuecal__event--dragging & {display: none;}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuecal__event-delete {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 1.4em;
|
||||||
|
line-height: 1.4em;
|
||||||
|
background-color: rgba(221, 51, 51, 0.85);
|
||||||
|
color: #fff;
|
||||||
|
z-index: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transform: translateY(-110%);
|
||||||
|
transition: 0.3s;
|
||||||
|
.vuecal__event & {user-select: none;}
|
||||||
|
|
||||||
|
.vuecal--full-height-delete & {
|
||||||
|
height: auto;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
width: 1.7em;
|
||||||
|
height: 1.8em;
|
||||||
|
display: block;
|
||||||
|
background-image: url('data:image/svg+xml;utf8,<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M12 1.5a10.5 10.5 0 100 21 10.5 10.5 0 000-21zm5 14.1c.2 0 .2.2.2.2l-.1.3-1 1-.3.1h-.2L12 13.5l-3.5 3.6h-.3-.3l-1-1v-.4-.2l3.6-3.6-3.6-3.5A.4.4 0 017 8l1-1 .3-.2c.1 0 .2 0 .2.2l3.6 3.5L15.6 7l.2-.2c.1 0 .2 0 .3.2l1 1v.5L13.5 12z" fill="%23fff" opacity=".9"/></svg>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.vuecal__event--deletable & {transform: translateY(0);z-index: 1;}
|
||||||
|
.vuecal__event--deletable.vuecal__event--dragging & {
|
||||||
|
opacity: 0;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuecal--month-view .vuecal__event-title {
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuecal--short-events .vuecal__event-title {
|
||||||
|
text-align: left;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding: 0 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuecal__event-title,
|
||||||
|
.vuecal__event-content {
|
||||||
|
hyphens: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuecal__event-title--edit {
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
text-align: center;
|
||||||
|
transition: 0.3s;
|
||||||
|
color: inherit;
|
||||||
|
background-image: url('data:image/svg+xml;utf8,<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M442 150l-39 39-80-80 39-39q6-6 15-6t15 6l50 50q6 6 6 15t-6 15zM64 368l236-236 80 80-236 236H64v-80z" fill="%23000" opacity=".4"/></svg>');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 120% 0.15em;
|
||||||
|
background-size: 0.4em;
|
||||||
|
outline: none;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:hover, &:focus {
|
||||||
|
border-color: rgba(0, 0, 0, 0.4);
|
||||||
|
background-position: 99% 0.15em;
|
||||||
|
background-size: 1.2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
<template lang="pug">
|
||||||
|
.vuecal__header
|
||||||
|
.vuecal__flex.vuecal__menu(
|
||||||
|
v-if="!options.hideViewSelector"
|
||||||
|
role="tablist"
|
||||||
|
aria-label="Calendar views navigation")
|
||||||
|
template(v-for="(v, id) in viewProps.views" :key="id")
|
||||||
|
button.vuecal__view-btn(
|
||||||
|
v-if="v.enabled"
|
||||||
|
type="button"
|
||||||
|
:class="{ 'vuecal__view-btn--active': view.id === id, 'vuecal__view-btn--highlighted': highlightedControl === id }"
|
||||||
|
@dragenter="editEvents.drag && dnd && dnd.viewSelectorDragEnter($event, id, $data)"
|
||||||
|
@dragleave="editEvents.drag && dnd && dnd.viewSelectorDragLeave($event, id, $data)"
|
||||||
|
@click="switchView(id, null, true)"
|
||||||
|
:aria-label="`${v.label} view`") {{ v.label }}
|
||||||
|
.vuecal__title-bar(v-if="!options.hideTitleBar")
|
||||||
|
button.vuecal__arrow.vuecal__arrow--prev(
|
||||||
|
type="button"
|
||||||
|
:class="{ 'vuecal__arrow--highlighted': highlightedControl === 'previous' }"
|
||||||
|
@click="previous"
|
||||||
|
@dragenter="editEvents.drag && dnd && dnd.viewSelectorDragEnter($event, 'previous', $data)"
|
||||||
|
@dragleave="editEvents.drag && dnd && dnd.viewSelectorDragLeave($event, 'previous', $data)"
|
||||||
|
:aria-label="`Previous ${view.id}`")
|
||||||
|
slot(name="arrow-prev")
|
||||||
|
.vuecal__flex.vuecal__title(grow)
|
||||||
|
transition(:name="options.transitions ? `slide-fade--${transitionDirection}` : ''")
|
||||||
|
component(
|
||||||
|
:type="!!broaderView && 'button'"
|
||||||
|
:is="broaderView ? 'button' : 'span'"
|
||||||
|
:key="`${view.id}${view.startDate.toString()}`"
|
||||||
|
@click="!!broaderView && switchToBroaderView()"
|
||||||
|
:aria-label="!!broaderView && `Go to ${broaderView} view`")
|
||||||
|
slot(name="title")
|
||||||
|
button.vuecal__today-btn(
|
||||||
|
v-if="options.todayButton"
|
||||||
|
type="button"
|
||||||
|
:class="{ 'vuecal__today-btn--highlighted': highlightedControl === 'today' }"
|
||||||
|
@click="goToToday"
|
||||||
|
@dragenter="editEvents.drag && dnd && dnd.viewSelectorDragEnter($event, 'today', $data)"
|
||||||
|
@dragleave="editEvents.drag && dnd && dnd.viewSelectorDragLeave($event, 'today', $data)"
|
||||||
|
aria-label="Today")
|
||||||
|
slot(name="today-button")
|
||||||
|
button.vuecal__arrow.vuecal__arrow--next(
|
||||||
|
type="button"
|
||||||
|
:class="{ 'vuecal__arrow--highlighted': highlightedControl === 'next' }"
|
||||||
|
@click="next"
|
||||||
|
@dragenter="editEvents.drag && dnd && dnd.viewSelectorDragEnter($event, 'next', $data)"
|
||||||
|
@dragleave="editEvents.drag && dnd && dnd.viewSelectorDragLeave($event, 'next', $data)"
|
||||||
|
:aria-label="`Next ${view.id}`")
|
||||||
|
slot(name="arrow-next")
|
||||||
|
weekdays-headings(
|
||||||
|
v-if="viewProps.weekDaysInHeader"
|
||||||
|
:week-days="weekDays"
|
||||||
|
:transition-direction="transitionDirection"
|
||||||
|
:switch-to-narrower-view="switchToNarrowerView")
|
||||||
|
template(#weekday-heading="{ heading, view }" v-if="$slots['weekday-heading']")
|
||||||
|
slot(name="weekday-heading" :heading="heading" :view="view")
|
||||||
|
template(#split-label="{ split }" v-if="$slots['split-label']")
|
||||||
|
slot(name="split-label" :split="split" :view="view")
|
||||||
|
|
||||||
|
//- Sticky split-days headers on day view only.
|
||||||
|
transition(:name="`slide-fade--${transitionDirection}`")
|
||||||
|
.vuecal__flex.vuecal__split-days-headers(v-if="showDaySplits")
|
||||||
|
.day-split-header(v-for="(split, i) in daySplits" :key="i" :class="split.class || false")
|
||||||
|
slot(name="split-label" :split="split" :view="view.id") {{ split.label }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import WeekdaysHeadings from './weekdays-headings.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
inject: ['vuecal', 'previous', 'next', 'switchView', 'updateSelectedDate', 'modules', 'view'],
|
||||||
|
components: { WeekdaysHeadings },
|
||||||
|
props: {
|
||||||
|
// Vuecal main component options (props).
|
||||||
|
options: { type: Object, default: () => ({}) },
|
||||||
|
editEvents: { type: Object, required: true },
|
||||||
|
hasSplits: { type: [Boolean, Number], default: false },
|
||||||
|
daySplits: { type: Array, default: () => [] },
|
||||||
|
viewProps: { type: Object, default: () => ({}) },
|
||||||
|
weekDays: { type: Array, default: () => [] },
|
||||||
|
switchToNarrowerView: { type: Function, default: () => {} }
|
||||||
|
},
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
highlightedControl: null
|
||||||
|
}),
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
goToToday () {
|
||||||
|
// Last midnight.
|
||||||
|
this.updateSelectedDate(new Date(new Date().setHours(0, 0, 0, 0)))
|
||||||
|
},
|
||||||
|
|
||||||
|
switchToBroaderView () {
|
||||||
|
this.transitionDirection = 'left'
|
||||||
|
|
||||||
|
if (this.broaderView) this.switchView(this.broaderView)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
transitionDirection: {
|
||||||
|
get () {
|
||||||
|
return this.vuecal.transitionDirection
|
||||||
|
},
|
||||||
|
set (direction) {
|
||||||
|
this.vuecal.transitionDirection = direction
|
||||||
|
}
|
||||||
|
},
|
||||||
|
broaderView () {
|
||||||
|
const { enabledViews } = this.vuecal
|
||||||
|
return enabledViews[enabledViews.indexOf(this.view.id) - 1]
|
||||||
|
},
|
||||||
|
showDaySplits () {
|
||||||
|
return this.view.id === 'day' && this.hasSplits && this.options.stickySplitLabels && !this.options.minSplitWidth
|
||||||
|
},
|
||||||
|
// Drag & drop module.
|
||||||
|
dnd () {
|
||||||
|
return this.modules.dnd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.vuecal {
|
||||||
|
&__header button {
|
||||||
|
outline: none;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__menu {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
list-style-type: none;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__view-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0.3em 1em;
|
||||||
|
height: 2.2em;
|
||||||
|
font-size: 1.3em;
|
||||||
|
border-bottom: 0 solid currentColor;
|
||||||
|
cursor: pointer;
|
||||||
|
color: inherit;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: 0.2s;
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
border-bottom-width: 2px;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title-bar {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 1.4em;
|
||||||
|
line-height: 1.3;
|
||||||
|
min-height: 2em;
|
||||||
|
|
||||||
|
.vuecal--xsmall & {font-size: 1.3em;}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
position: relative;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
button.slide-fade--left-leave-active,
|
||||||
|
button.slide-fade--right-leave-active {width: 100%;}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__today-btn {
|
||||||
|
position: relative;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
font-size: 0.8em;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
span.default {
|
||||||
|
font-size: 0.8em;
|
||||||
|
padding: 3px 6px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__arrow {
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
// Non-breakable spaces are added around the default angle icon to make it larger.
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&--prev {margin-left: 0.6em;}
|
||||||
|
&--next {margin-right: 0.6em;}
|
||||||
|
|
||||||
|
i.angle {
|
||||||
|
display: inline-block;
|
||||||
|
border: solid currentColor;
|
||||||
|
border-width: 0 2px 2px 0;
|
||||||
|
padding: 0.25em;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--prev i.angle {border-width: 2px 0 0 2px;}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pulse header buttons when dragging over with an event.
|
||||||
|
&__arrow--highlighted,
|
||||||
|
&__today-btn--highlighted,
|
||||||
|
&__view-btn--highlighted {
|
||||||
|
position: relative;
|
||||||
|
background-color: rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
// Prevent event bubbling on hover and move.
|
||||||
|
* {pointer-events: none;}
|
||||||
|
|
||||||
|
&:before, &:after {
|
||||||
|
content: '';
|
||||||
|
background-color: inherit;
|
||||||
|
animation: sonar 0.8s infinite ease-out;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
width: 3em;
|
||||||
|
height: 3em;
|
||||||
|
border-radius: 3em;
|
||||||
|
margin-top: -1.5em;
|
||||||
|
margin-left: -1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
animation-duration: 1.5s;
|
||||||
|
animation-delay: 0.1s;
|
||||||
|
width: 2.6em;
|
||||||
|
height: 2.6em;
|
||||||
|
border-radius: 2.6em;
|
||||||
|
margin-top: -1.3em;
|
||||||
|
margin-left: -1.3em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animations.
|
||||||
|
//==================================//
|
||||||
|
@keyframes sonar {
|
||||||
|
0%, 20% {opacity: 1;}
|
||||||
|
100% {
|
||||||
|
transform: scale(2.5);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Media queries.
|
||||||
|
//==================================//
|
||||||
|
@media screen and (max-width: 450px) {
|
||||||
|
.vuecal__title {font-size: 0.9em;}
|
||||||
|
.vuecal__view-btn {padding-left: 0.6em;padding-right: 0.6em;}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 350px) {
|
||||||
|
.vuecal__view-btn {font-size: 1.1em;}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["الإثنين", "الثلاثاء", "الأربعاء", "الخميس", "الجمعة", "السبت", "الأحد"],
|
||||||
|
"months": ["يناير" , "فبراير" , "مارس" , "أبريل" , "مايو" , "يونيو" , "يوليو" , "أغسطس" , "سبتمبر" , "أكتوبر" , "نوفمبر" , " ديسمبر"],
|
||||||
|
"years": "سنوات",
|
||||||
|
"year": "سنة",
|
||||||
|
"month": "شهر",
|
||||||
|
"week": "أسبوع",
|
||||||
|
"day": "يوم",
|
||||||
|
"today": "اليوم",
|
||||||
|
"noEvent": "لا حدث",
|
||||||
|
"allDay": "طوال اليوم",
|
||||||
|
"deleteEvent": "حذف",
|
||||||
|
"createEvent": "إنشاء حدث",
|
||||||
|
"dateFormat": "dddd D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Понеделник", "Вторник", "Сряда", "Четвъртък", "Петък", "Събота", "Неделя"],
|
||||||
|
"months": ["Януари", "Февруари", "Март", "Април", "Май", "Юни", "Юли", "Август", "Септември", "Октомври", "Ноември", "Декември"],
|
||||||
|
"years": "Години",
|
||||||
|
"year": "Година",
|
||||||
|
"month": "Месец",
|
||||||
|
"week": "Седмица",
|
||||||
|
"day": "Ден",
|
||||||
|
"today": "Днес",
|
||||||
|
"noEvent": "Няма събития",
|
||||||
|
"allDay": "Цял ден",
|
||||||
|
"deleteEvent": "Изтрий",
|
||||||
|
"createEvent": "Създай събитие",
|
||||||
|
"dateFormat": "dddd D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["সোম", "মঙ্গল", "বুধ", "বৃহস্পতি", "শুক্র" , "শনি" , "রবি"],
|
||||||
|
"months": ["জানুয়ারি" , "ফেব্ুয়ারী" , "মার্চ", "এপ্রিল" , "মে" , "জুন" , "জুলাই" , "অগাস্ট" , "সেপ্টেম্বর" , "অক্টোবর", "নভেম্বর" , "ডিসেম্বর"],
|
||||||
|
"years": "বছর",
|
||||||
|
"year": "বছর",
|
||||||
|
"month": "মাস",
|
||||||
|
"week": "সপ্তাহ",
|
||||||
|
"day": "দিন",
|
||||||
|
"today": "আজ",
|
||||||
|
"noEvent": "কার্যসূচী",
|
||||||
|
"allDay": "সারাদিন",
|
||||||
|
"deleteEvent": "মুছুন",
|
||||||
|
"createEvent": "কার্যসূচী তৈরি করুন",
|
||||||
|
"dateFormat": "dddd D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Ponedjeljak", "Utorak", "Srijeda", "Četvrtak", "Petak", "Subota", "Nedjelja"],
|
||||||
|
"months": ["Januar", "Februar", "Mart", "April", "Maj", "Jun", "Jul", "Avgust", "Septembar", "Oktobar", "Novembar", "Decembar"],
|
||||||
|
"years": "Godine",
|
||||||
|
"year": "Godina",
|
||||||
|
"month": "Mjesec",
|
||||||
|
"week": "Sedmica",
|
||||||
|
"day": "Dan",
|
||||||
|
"today": "Danas",
|
||||||
|
"noEvent": "Nema događaja",
|
||||||
|
"allDay": "Cijeli dan",
|
||||||
|
"deleteEvent": "Obriši",
|
||||||
|
"createEvent": "Kreiraj događaj",
|
||||||
|
"dateFormat": "dddd D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Dilluns", "Dimarts", "Dimecres", "Dijous", "Divendres", "Dissabte", "Diumenge"],
|
||||||
|
"weekDaysShort": ["Dl", "Dt", "Dc", "Dj", "Dv", "Ds", "Dg"],
|
||||||
|
"months": ["Gener", "Febrer", "Març", "Abril", "Maig", "Juny", "Juliol", "Agost", "Setembre", "Octubre", "Novembre", "Desembre"],
|
||||||
|
"years": "Anys",
|
||||||
|
"year": "Any",
|
||||||
|
"month": "Mes",
|
||||||
|
"week": "Setmana",
|
||||||
|
"day": "Dia",
|
||||||
|
"today": "Avui",
|
||||||
|
"noEvent": "No hi ha esdeveniments",
|
||||||
|
"allDay": "Tot el dia",
|
||||||
|
"deleteEvent": "Eliminar",
|
||||||
|
"createEvent": "Crear un esdeveniment",
|
||||||
|
"dateFormat": "dddd D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Pondělí", "Úterý", "Středa", "Čtvrtek", "Pátek", "Sobota", "Neděle"],
|
||||||
|
"months": ["Leden", "Únor", "Březen", "Duben", "Květen", "Červen", "Červenec", "Srpen", "Září", "Říjen", "Listopad", "Prosinec"],
|
||||||
|
"years": "Roky",
|
||||||
|
"year": "Rok",
|
||||||
|
"month": "Měsíc",
|
||||||
|
"week": "Týden",
|
||||||
|
"day": "Den",
|
||||||
|
"today": "Dnes",
|
||||||
|
"noEvent": "Bez událostí",
|
||||||
|
"allDay": "Celý den",
|
||||||
|
"deleteEvent": "Odstranit",
|
||||||
|
"createEvent": "Vytvořit událost",
|
||||||
|
"dateFormat": "dddd D. MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Mandag", "Tirsdag", "Onsdag", "Torsdag", "Fredag", "Lørdag", "Søndag"],
|
||||||
|
"months": ["Januar", "Februar", "Marts", "April", "Maj", "Juni", "Juli", "August", "September", "Oktober", "November", "December"],
|
||||||
|
"years": "År (flertal)",
|
||||||
|
"year": "År",
|
||||||
|
"month": "Måned",
|
||||||
|
"week": "Uge",
|
||||||
|
"day": "Dag",
|
||||||
|
"today": "I dag",
|
||||||
|
"noEvent": "Ingen begivenhed",
|
||||||
|
"allDay": "Hele dagen",
|
||||||
|
"deleteEvent": "Slet",
|
||||||
|
"createEvent": "Opret et event",
|
||||||
|
"dateFormat": "dddd D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"],
|
||||||
|
"months": ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"],
|
||||||
|
"years": "Jahre",
|
||||||
|
"year": "Jahr",
|
||||||
|
"month": "Monat",
|
||||||
|
"week": "Woche",
|
||||||
|
"day": "Tag",
|
||||||
|
"today": "Heute",
|
||||||
|
"noEvent": "Keine Events",
|
||||||
|
"allDay": "Ganztägig",
|
||||||
|
"deleteEvent": "Löschen",
|
||||||
|
"createEvent": "Event erstellen",
|
||||||
|
"dateFormat": "dddd D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Δευτέρα", "Τρίτη", "Τετάρτη", "Πέμπτη", "Παρασκευή", "Σάββατο", "Κυριακή"],
|
||||||
|
"months": ["Ιανουάριος", "Φεβρουάριος", "Μάρτιος", "Απρίλιος", "Μάιος", "Ιούνιος", "Ιούλιος", "Αύγουστος", "Σεπτέμβριος", "Οκτώβριος", "Νοέμβριος", "Δεκέμβριος"],
|
||||||
|
"monthsGenitive": ["Ιανουαρίου", "Φεβρουαρίου", "Μαρτίου", "Απριλίου", "Μαΐου", "Ιουνίου", "Ιουλίου", "Αυγούστου", "Σεπτεμβρίου", "Οκτωβρίου", "Νοεμβρίου", "Δεκεμβρίου"],
|
||||||
|
"years": "Έτη",
|
||||||
|
"year": "Έτος",
|
||||||
|
"month": "Μήνα",
|
||||||
|
"week": "Εβδομάδα",
|
||||||
|
"day": "Ημέρα",
|
||||||
|
"today": "Σήμερα",
|
||||||
|
"noEvent": "Κανένα συμβάν",
|
||||||
|
"allDay": "Ημερήσιο συμβάν",
|
||||||
|
"deleteEvent": "Διαγραφή",
|
||||||
|
"createEvent": "Δημιουργία συμβάντος",
|
||||||
|
"dateFormat": "dddd D MMMMG YYYY",
|
||||||
|
"am": "π.μ.",
|
||||||
|
"pm": "μ.μ."
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
|
||||||
|
"months": ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
|
||||||
|
"years": "Years",
|
||||||
|
"year": "Year",
|
||||||
|
"month": "Month",
|
||||||
|
"week": "Week",
|
||||||
|
"day": "Day",
|
||||||
|
"today": "Today",
|
||||||
|
"noEvent": "No Event",
|
||||||
|
"allDay": "All day",
|
||||||
|
"deleteEvent": "Delete",
|
||||||
|
"createEvent": "Create an event",
|
||||||
|
"dateFormat": "dddd MMMM D{S}, YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado", "Domingo"],
|
||||||
|
"months": ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"],
|
||||||
|
"years": "Años",
|
||||||
|
"year": "Año",
|
||||||
|
"month": "Mes",
|
||||||
|
"week": "Semana",
|
||||||
|
"day": "Día",
|
||||||
|
"today": "Hoy",
|
||||||
|
"noEvent": "No hay evento",
|
||||||
|
"allDay": "Todo el día",
|
||||||
|
"deleteEvent": "Borrar",
|
||||||
|
"createEvent": "Crear un evento",
|
||||||
|
"dateFormat": "dddd D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Esmaspäev", "Teisipäev", "Kolmapäev", "Neljapäev", "Reede", "Laupäev", "Pühapäev"],
|
||||||
|
"months": ["Jaanuar", "Veebruar", "Märts", "Aprill", "Mai", "Juuni", "Juuli", "August", "September", "Oktoober", "November", "Detsember"],
|
||||||
|
"years": "Aastad",
|
||||||
|
"year": "Aasta",
|
||||||
|
"month": "Kuu",
|
||||||
|
"week": "Nädal",
|
||||||
|
"day": "Päev",
|
||||||
|
"today": "Täna",
|
||||||
|
"noEvent": "Sündmus puudub",
|
||||||
|
"allDay": "Terve päev",
|
||||||
|
"deleteEvent": "Kustuta",
|
||||||
|
"createEvent": "Loo sündmus",
|
||||||
|
"dateFormat": "dddd D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["دوشنبه", "سه شنبه", "چهار شنبه", "پنج شنبه", "جمعه", "شنبه", "یک شنبه"],
|
||||||
|
"months": ["ژانویه", "فوریه", "مارس", "آوریل", "می", "ژوئن", "ژوئیه", "اوت", "سپتامبر", "اکتبر", "نوامبر", "دسامبر"],
|
||||||
|
"years": "سالها",
|
||||||
|
"year": "سال",
|
||||||
|
"month": "ماه",
|
||||||
|
"week": "هفته",
|
||||||
|
"day": "روز",
|
||||||
|
"today": "امروز",
|
||||||
|
"noEvent": "رویدادی نیست",
|
||||||
|
"allDay": "تمام روز",
|
||||||
|
"deleteEvent": "حذف",
|
||||||
|
"createEvent": "ایجاد یک رویداد",
|
||||||
|
"dateFormat": "dddd D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche"],
|
||||||
|
"months": ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"],
|
||||||
|
"years": "Années",
|
||||||
|
"year": "Année",
|
||||||
|
"month": "Mois",
|
||||||
|
"week": "Semaine",
|
||||||
|
"day": "Jour",
|
||||||
|
"today": "Aujourd'hui",
|
||||||
|
"noEvent": "Aucun événement",
|
||||||
|
"allDay": "Jour entier",
|
||||||
|
"deleteEvent": "Supprimer",
|
||||||
|
"createEvent": "Créer un événement",
|
||||||
|
"dateFormat": "dddd D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["שני", "שלישי", "רביעי", "חמישי", "שישי", "שבת", "ראשון"],
|
||||||
|
"months": ["ינואר", "פברואר", "מרץ", "אפריל", "מאי", "יוני", "יולי", "אוגוסט", "ספטמבר", "אוקטובר", "נובמבר", "דצמבר"],
|
||||||
|
"years": "שנים",
|
||||||
|
"year": "שנה",
|
||||||
|
"month": "חודש",
|
||||||
|
"week": "שבוע",
|
||||||
|
"day": "יום",
|
||||||
|
"today": "היום",
|
||||||
|
"noEvent": "אין אירועים",
|
||||||
|
"allDay": "כל היום",
|
||||||
|
"deleteEvent": "מחיקה",
|
||||||
|
"createEvent": "צור אירוע",
|
||||||
|
"dateFormat": "dddd D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Ponedjeljak", "Utorak", "Srijeda", "Četvrtak", "Petak", "Subota", "Nedjelja"],
|
||||||
|
"months": ["Siječanj", "Veljača", "Ožujak", "Travanj", "Svibanj", "Lipanj", "Srpanj", "Kolovoz", "Rujan", "Listopad", "Studeni", "Prosinac"],
|
||||||
|
"years": "Godine",
|
||||||
|
"year": "Godina",
|
||||||
|
"month": "Mjesec",
|
||||||
|
"week": "Tjedan",
|
||||||
|
"day": "Dan",
|
||||||
|
"today": "Današnji dan",
|
||||||
|
"noEvent": "Nema događaja",
|
||||||
|
"allDay": "Cijeli dan",
|
||||||
|
"deleteEvent": "Obriši",
|
||||||
|
"createEvent": "Kreiraj događaj",
|
||||||
|
"dateFormat": "dddd D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Hétfo", "Kedd", "Szerda", "Csütörtök", "Péntek", "Szombat", "Vasárnap"],
|
||||||
|
"months": ["Január", "Február", "Március", "Április", "Május", "Június", "Július", "Augusztus", "Szeptember", "Október", "November", "December"],
|
||||||
|
"years": "Évek",
|
||||||
|
"year": "Év",
|
||||||
|
"month": "Hónap",
|
||||||
|
"week": "Hét",
|
||||||
|
"day": "Nap",
|
||||||
|
"today": "Mai nap",
|
||||||
|
"noEvent": "Nincs esemény",
|
||||||
|
"allDay": "Egész nap",
|
||||||
|
"deleteEvent": "Esemény törlese",
|
||||||
|
"createEvent": "Esemény létrehozása",
|
||||||
|
"dateFormat": "dddd D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu", "Minggu"],
|
||||||
|
"months": ["Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember"],
|
||||||
|
"years": "Tahunan",
|
||||||
|
"year": "Tahun",
|
||||||
|
"month": "Bulan",
|
||||||
|
"week": "Minggu",
|
||||||
|
"day": "Hari",
|
||||||
|
"today": "Hari Ini",
|
||||||
|
"noEvent": "Tidak Ada Kegiatan",
|
||||||
|
"allDay": "Sepanjang Hari",
|
||||||
|
"deleteEvent": "Hapus",
|
||||||
|
"createEvent": "Tambah Kegiatan",
|
||||||
|
"dateFormat": "dddd, D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Mánudagur", "Þriðjudagur", "Miðvikudagur", "Fimmtudagur", "Föstudagur", "Laugardagur", "Sunnudagur"],
|
||||||
|
"months": ["Janúar", "Febrúar", "Mars", "Apríl", "Maí", "Júní", "Júlí", "Ágúst", "September", "Október", "Nóvember", "Desember"],
|
||||||
|
"years": "Ár",
|
||||||
|
"year": "Ár",
|
||||||
|
"month": "Mánuður",
|
||||||
|
"week": "Vika",
|
||||||
|
"day": "Dagur",
|
||||||
|
"today": "Í dag",
|
||||||
|
"noEvent": "Enginn atburður",
|
||||||
|
"allDay": "Allan daginn",
|
||||||
|
"deleteEvent": "Eyða",
|
||||||
|
"createEvent": "Búðu til viðburð",
|
||||||
|
"dateFormat": "dddd D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Lunedì", "Martedì", "Mercoledì", "Giovedì", "Venerdì", "Sabato", "Domenica"],
|
||||||
|
"months": ["Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno", "Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre"],
|
||||||
|
"years": "Anni",
|
||||||
|
"year": "Anno",
|
||||||
|
"month": "Mese",
|
||||||
|
"week": "Settimana",
|
||||||
|
"day": "Giorno",
|
||||||
|
"today": "Oggi",
|
||||||
|
"noEvent": "Nessun evento",
|
||||||
|
"allDay": "Tutto il giorno",
|
||||||
|
"deleteEvent": "Cancella",
|
||||||
|
"createEvent": "Crea evento",
|
||||||
|
"dateFormat": "dddd D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["月", "火", "水", "木", "金", "土", "日"],
|
||||||
|
"months": ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"],
|
||||||
|
"years": "年",
|
||||||
|
"year": "今年",
|
||||||
|
"month": "月",
|
||||||
|
"week": "週",
|
||||||
|
"day": "日",
|
||||||
|
"today": "今日",
|
||||||
|
"noEvent": "イベントなし",
|
||||||
|
"allDay": "終日",
|
||||||
|
"deleteEvent": "削除",
|
||||||
|
"createEvent": "イベント作成",
|
||||||
|
"dateFormat": "YYYY年 MMMM D日 (dddd)"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["ორშაბათი", "სამშაბათი", "ოთხშაბათი", "ხუთშაბათი", "პარასკევი", "შაბათი", "კვირა"],
|
||||||
|
"months": ["იანვარი", "თებერვალი", "მარტი", "აპრილი", "მაისი", "ივნისი", "ივლისი", "აგვისტო", "სექტემბერი", "ოქტომბერი", "ნოემბერი", "დეკემბერი"],
|
||||||
|
"years": "წლები",
|
||||||
|
"year": "წელი",
|
||||||
|
"month": "თვე",
|
||||||
|
"week": "კვირა",
|
||||||
|
"day": "დღე",
|
||||||
|
"today": "დღეს",
|
||||||
|
"noEvent": "ღონისძიება არ არის",
|
||||||
|
"allDay": "მთელი დღე",
|
||||||
|
"deleteEvent": "წაშლა",
|
||||||
|
"createEvent": "შექმენით ღონისძიება",
|
||||||
|
"dateFormat": "dddd D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["월", "화", "수", "목", "금", "토", "일"],
|
||||||
|
"months": ["1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월"],
|
||||||
|
"years": "년도",
|
||||||
|
"year": "연간",
|
||||||
|
"month": "월간",
|
||||||
|
"week": "주간",
|
||||||
|
"day": "일간",
|
||||||
|
"today": "오늘",
|
||||||
|
"noEvent": "일정 없음",
|
||||||
|
"allDay": "하루 종일",
|
||||||
|
"deleteEvent": "삭제",
|
||||||
|
"createEvent": "일정 추가",
|
||||||
|
"dateFormat": "YYYY년 MMMM D일 dddd요일"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Pirmadienis", "Antradienis", "Trečiadienis", "Ketvirtadienis", "Penktadienis", "Šeštadienis", "Sekmadienis"],
|
||||||
|
"months": ["Sausis", "Vasaris", "Kovas", "Balandis", "Gegužė", "Birželis", "Liepa", "Rugpjūtis", "Rugsėjis", "Spalis", "Lapkritis", "Gruodis"],
|
||||||
|
"years": "Metų pasirinkimas",
|
||||||
|
"year": "Metai",
|
||||||
|
"month": "Mėnesis",
|
||||||
|
"week": "Savaitė",
|
||||||
|
"day": "Diena",
|
||||||
|
"today": "Šiandien",
|
||||||
|
"noEvent": "Jokių įvykių",
|
||||||
|
"allDay": "Visa diena",
|
||||||
|
"deleteEvent": "Ištrinti",
|
||||||
|
"createEvent": "Sukurti įvykį",
|
||||||
|
"dateFormat": "dddd, D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Даваа", "Мягмар", "Лхавга", "Пүрэв", "Баасан", "Бямба", "Ням"],
|
||||||
|
"months": ["1-р сар", "2-р сар", "3-р сар", "4-р сар", "5-р сар", "6-р сар", "7-р сар", "8-р сар", "9-р сар", "10-р сар", "11-р сар", "12-р сар"],
|
||||||
|
"years": "Жилүүд",
|
||||||
|
"year": "Жил",
|
||||||
|
"month": "Сар",
|
||||||
|
"week": "Долоо хоног",
|
||||||
|
"day": "Өдөр",
|
||||||
|
"today": "Өнөөдөр",
|
||||||
|
"noEvent": "Тэмдэглэлгүй",
|
||||||
|
"allDay": "Бүх өдөр",
|
||||||
|
"deleteEvent": "Устгах",
|
||||||
|
"createEvent": "Шинэ тэмдэглэл",
|
||||||
|
"dateFormat": "dddd D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Maandag", "Dinsdag", "Woensdag", "Donderdag", "Vrijdag", "Zaterdag", "Zondag"],
|
||||||
|
"months": ["Januari", "Februari", "Maart", "April", "Mei", "Juni", "Juli", "Augustus", "September", "Oktober", "November", "December"],
|
||||||
|
"years": "Jaren",
|
||||||
|
"year": "Jaar",
|
||||||
|
"month": "Maand",
|
||||||
|
"week": "Week",
|
||||||
|
"day": "Dag",
|
||||||
|
"today": "Vandaag",
|
||||||
|
"noEvent": "Geen afspraken",
|
||||||
|
"allDay": "Hele dag",
|
||||||
|
"deleteEvent": "Verwijderen",
|
||||||
|
"createEvent": "Nieuwe afspraak aanmaken",
|
||||||
|
"dateFormat": "dddd D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Mandag", "Tirsdag", "Onsdag", "Torsdag", "Fredag", "Lørdag", "Søndag"],
|
||||||
|
"months": ["Januar", "Februar", "Mars", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Desember"],
|
||||||
|
"years": "Velg år",
|
||||||
|
"year": "År",
|
||||||
|
"month": "Måned",
|
||||||
|
"week": "Uke",
|
||||||
|
"day": "Dag",
|
||||||
|
"today": "Idag",
|
||||||
|
"noEvent": "Ingen hendelse",
|
||||||
|
"allDay": "Hele dagen",
|
||||||
|
"deleteEvent": "Ta bort",
|
||||||
|
"createEvent": "Ny hendelse",
|
||||||
|
"dateFormat": "dddd, D. MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Poniedziałek", "Wtorek", "Środa", "Czwartek", "Piątek", "Sobota", "Niedziela"],
|
||||||
|
"months": ["Styczeń", "Luty", "Marzec", "Kwiecień", "Maj", "Czerwiec", "Lipiec", "Sierpień", "Wrzesień", "Październik", "Listopad", "Grudzień"],
|
||||||
|
"years": "Lata",
|
||||||
|
"year": "Rok",
|
||||||
|
"month": "Miesiąc",
|
||||||
|
"week": "Tydzień",
|
||||||
|
"day": "Dzień",
|
||||||
|
"today": "Dzisiaj",
|
||||||
|
"noEvent": "Brak wydarzeń",
|
||||||
|
"allDay": "Cały dzień",
|
||||||
|
"deleteEvent": "Usuń",
|
||||||
|
"createEvent": "Utwórz wydarzenie",
|
||||||
|
"dateFormat": "dddd, D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Segunda-feira", "Terça-feira", "Quarta-feira", "Quinta-feira", "Sexta-feira", "Sábado", "Domingo"],
|
||||||
|
"months": ["Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho", "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro"],
|
||||||
|
"years": "Anos",
|
||||||
|
"year": "Ano",
|
||||||
|
"month": "Mês",
|
||||||
|
"week": "Semana",
|
||||||
|
"day": "Dia",
|
||||||
|
"today": "Hoje",
|
||||||
|
"noEvent": "Sem eventos",
|
||||||
|
"allDay": "Dia inteiro",
|
||||||
|
"deleteEvent": "Remover",
|
||||||
|
"createEvent": "Criar um evento",
|
||||||
|
"dateFormat": "dddd D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Luni", "Marți", "Miercuri", "Joi", "Vineri", "Sâmbăta", "Duminică"],
|
||||||
|
"months": ["Ianuarie", "Februarie", "Martie", "Aprilie", "Mai", "Iunie", "Iulie", "August", "Septembrie", "Octombrie", "Noiembrie", "Decembrie"],
|
||||||
|
"years": "Ani",
|
||||||
|
"year": "An",
|
||||||
|
"month": "Lună",
|
||||||
|
"week": "Săptămână",
|
||||||
|
"day": "Zi",
|
||||||
|
"today": "Azi",
|
||||||
|
"noEvent": "Nici o interacțiune",
|
||||||
|
"allDay": "Toată ziua",
|
||||||
|
"deleteEvent": "Șterge",
|
||||||
|
"createEvent": "Adaugă un eveniment",
|
||||||
|
"dateFormat": "dddd D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"],
|
||||||
|
"weekDaysShort": ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"],
|
||||||
|
"months": ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"],
|
||||||
|
"years": "Годы",
|
||||||
|
"year": "Год",
|
||||||
|
"month": "Месяц",
|
||||||
|
"week": "Неделя",
|
||||||
|
"day": "День",
|
||||||
|
"today": "Сегодня",
|
||||||
|
"noEvent": "Нет событий",
|
||||||
|
"allDay": "Весь день",
|
||||||
|
"deleteEvent": "Удалить",
|
||||||
|
"createEvent": "Создать событие",
|
||||||
|
"dateFormat": "dddd D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Pondelok", "Utorok", "Streda", "Štvrtok", "Piatok", "Sobota", "Nedeľa"],
|
||||||
|
"months": ["Január", "Február", "Marec", "Apríl", "Máj", "Jún", "Júl", "August", "September", "Október", "November", "December"],
|
||||||
|
"years": "Roky",
|
||||||
|
"year": "Rok",
|
||||||
|
"month": "Mesiac",
|
||||||
|
"week": "Týždeň",
|
||||||
|
"day": "Deň",
|
||||||
|
"today": "Dnes",
|
||||||
|
"noEvent": "Bez udalosti",
|
||||||
|
"allDay": "Celý deň",
|
||||||
|
"deleteEvent": "Odstrániť",
|
||||||
|
"createEvent": "Vytvoriť udalosť",
|
||||||
|
"dateFormat": "dddd D. MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Ponedeljek", "Torek", "Sreda", "Četrtek", "Petek", "Sobota", "Nedelja"],
|
||||||
|
"months": ["Januar", "Februar", "Marec", "April", "Maj", "Junij", "Julij", "Avgust", "September", "Oktober", "November", "December"],
|
||||||
|
"years": "Leta",
|
||||||
|
"year": "Leto",
|
||||||
|
"month": "Mesec",
|
||||||
|
"week": "Teden",
|
||||||
|
"day": "Dan",
|
||||||
|
"today": "Danes",
|
||||||
|
"noEvent": "Ni dogodkov",
|
||||||
|
"allDay": "Cel dan",
|
||||||
|
"deleteEvent": "Odstrani",
|
||||||
|
"createEvent": "Ustvari dogodek",
|
||||||
|
"dateFormat": "dddd MMMM D, YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["E Hënë","E Martë","E Mërkurë","E Enjte","E Premte","E Shtunë","E Diel"],
|
||||||
|
"weekDaysShort": ["Hë","Ma","Mr","Enj","Pr","Sh","D"],
|
||||||
|
"months": ["Janar","Shkurt","Mars","Prill","Maj","Qershor","Korrik","Gusht","Shtator","Tetor","Nëntor","Dhjetor"],
|
||||||
|
"years": "Vitet",
|
||||||
|
"year": "Viti",
|
||||||
|
"month": "Muaji",
|
||||||
|
"week": "Java",
|
||||||
|
"day": "Dita",
|
||||||
|
"today": "Sot",
|
||||||
|
"noEvent": "Nuk ka event",
|
||||||
|
"allDay": "Tërë ditën",
|
||||||
|
"deleteEvent": "Fshijë",
|
||||||
|
"createEvent": "Krijo një event",
|
||||||
|
"dateFormat": "dddd D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Ponedeljak", "Utorak", "Sreda", "Četvrtak", "Petak", "Subota", "Nedelja"],
|
||||||
|
"months": ["Januar", "Februar", "Mart", "April", "Maj", "Jun", "Jul", "Avgust", "Septembar", "Oktobar", "Novembar", "Decembar"],
|
||||||
|
"years": "Godine",
|
||||||
|
"year": "Godina",
|
||||||
|
"month": "Mesec",
|
||||||
|
"week": "Sedmica",
|
||||||
|
"day": "Dan",
|
||||||
|
"today": "Danas",
|
||||||
|
"noEvent": "Nema događaja",
|
||||||
|
"allDay": "Celi dan",
|
||||||
|
"deleteEvent": "Obriši",
|
||||||
|
"createEvent": "Kreiraj događaj",
|
||||||
|
"dateFormat": "dddd D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag"],
|
||||||
|
"months": ["Januari", "Februari", "Mars", "April", "Maj", "Juni", "Juli", "Augusti", "September", "Oktober", "November", "December"],
|
||||||
|
"years": "År",
|
||||||
|
"year": "År",
|
||||||
|
"month": "Månad",
|
||||||
|
"week": "Vecka",
|
||||||
|
"day": "Dag",
|
||||||
|
"today": "Idag",
|
||||||
|
"noEvent": "Ingen händelse",
|
||||||
|
"allDay": "Heldag",
|
||||||
|
"deleteEvent": "Ta bort",
|
||||||
|
"createEvent": "Skapa händelse",
|
||||||
|
"dateFormat": "dddd den D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Pazartesi", "Salı", "Çarşamba", "Perşembe", "Cuma", "Cumartesi", "Pazar"],
|
||||||
|
"months": ["Ocak", "Şubat", "Mart", "Nisan", "Mayıs", "Haziran", "Temmuz", "Ağustos", "Eylül", "Ekim", "Kasım", "Aralık"],
|
||||||
|
"years": "Yıllar",
|
||||||
|
"year": "Yıl",
|
||||||
|
"month": "Ay",
|
||||||
|
"week": "Hafta",
|
||||||
|
"day": "Gün",
|
||||||
|
"today": "Bugün",
|
||||||
|
"noEvent": "Etkinlik Yok",
|
||||||
|
"allDay": "Tüm gün",
|
||||||
|
"deleteEvent": "Sil",
|
||||||
|
"createEvent": "Etkinlik ekle",
|
||||||
|
"dateFormat": "dddd D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Понеділок", "Вівторок", "Середа", "Четвер", "П'ятниця", "Субота", "Неділя"],
|
||||||
|
"months": ["Січень", "Лютий", "Березень", "Квітень", "Травень", "Червень", "Липень", "Серпень", "Вересень", "Жовтень", "Листопад", "Грудень"],
|
||||||
|
"weekDaysShort": ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Нд"],
|
||||||
|
"years": "Роки",
|
||||||
|
"year": "Рік",
|
||||||
|
"month": "Місяць",
|
||||||
|
"week": "Тиждень",
|
||||||
|
"day": "День",
|
||||||
|
"today": "Сьогодні",
|
||||||
|
"noEvent": "Немає подій",
|
||||||
|
"allDay": "Весь день",
|
||||||
|
"deleteEvent": "Видалити",
|
||||||
|
"createEvent": "Створити подію",
|
||||||
|
"dateFormat": "dddd D MMMM YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["Thứ hai", "Thứ ba", "Thứ tư", "Thứ năm", "Thứ sáu", "Thứ bảy", "Chủ nhật"],
|
||||||
|
"weekDaysShort": ["T2", "T3", "T4", "T5", "T6", "T7", "CN"],
|
||||||
|
"months": ["Tháng 1", "Tháng 2", "Tháng 3", "Tháng 4", "Tháng 5", "Tháng 6", "Tháng 7", "Tháng 8", "Tháng 9", "Tháng 10", "Tháng 11", "Tháng 12"],
|
||||||
|
"years": "Năm",
|
||||||
|
"year": "Năm nay",
|
||||||
|
"month": "Tháng",
|
||||||
|
"week": "Tuần",
|
||||||
|
"day": "Ngày",
|
||||||
|
"today": "Hôm nay",
|
||||||
|
"noEvent": "NKhông có Event",
|
||||||
|
"allDay": "Cả ngày",
|
||||||
|
"deleteEvent": "Xóa",
|
||||||
|
"createEvent": "Tạo event",
|
||||||
|
"dateFormat": "dddd MMMM D YYYY"
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"],
|
||||||
|
"weekDaysShort": ["一", "二", "三", "四", "五", "六", "日"],
|
||||||
|
"months": ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
|
||||||
|
"years": "年",
|
||||||
|
"year": "本年",
|
||||||
|
"month": "月",
|
||||||
|
"week": "周",
|
||||||
|
"day": "日",
|
||||||
|
"today": "今日",
|
||||||
|
"noEvent": "暂无活动",
|
||||||
|
"allDay": "整天",
|
||||||
|
"deleteEvent": "删除",
|
||||||
|
"createEvent": "新建活动",
|
||||||
|
"dateFormat": "YYYY MMMM D dddd"
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"weekDays": ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"],
|
||||||
|
"weekDaysShort": ["一", "二", "三", "四", "五", "六", "日"],
|
||||||
|
"months": ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
|
||||||
|
"years": "年",
|
||||||
|
"year": "本年",
|
||||||
|
"month": "月",
|
||||||
|
"week": "周",
|
||||||
|
"day": "日",
|
||||||
|
"today": "今日",
|
||||||
|
"noEvent": "暫無活動",
|
||||||
|
"allDay": "整天",
|
||||||
|
"deleteEvent": "刪除",
|
||||||
|
"createEvent": "新建活動",
|
||||||
|
"dateFormat": "YYYY MMMM D dddd"
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { App } from 'vue'
|
||||||
|
import VueCal from './index.vue'
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
install: (app: App) => {
|
||||||
|
|
||||||
|
app.component('Vue-cal', VueCal)
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,344 @@
|
|||||||
|
/**
|
||||||
|
* Events drag and drop module.
|
||||||
|
*
|
||||||
|
* Note: Waiting for VS Code to support JavaScript private fields.
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Class_fields
|
||||||
|
* Meantime keep `_` for private.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const holdOverTimeout = 800 // How long we should hold over an element before it reacts.
|
||||||
|
let changeViewTimeout = null
|
||||||
|
let pressPrevOrNextInterval = null
|
||||||
|
let viewBeforeDrag = { id: null, date: null } // To go back if cancelling.
|
||||||
|
let viewChanged = false
|
||||||
|
let cancelViewChange = true
|
||||||
|
let dragOverCell = { el: null, cell: null, timeout: null }
|
||||||
|
const dragging = {
|
||||||
|
_eid: null,
|
||||||
|
fromVueCal: null,
|
||||||
|
toVueCal: null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DragAndDrop = class {
|
||||||
|
_vuecal
|
||||||
|
|
||||||
|
constructor (vuecal) {
|
||||||
|
this._vuecal = vuecal
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When click and drag an event the cursor can be anywhere in the event,
|
||||||
|
* when dropping the event, we need to subtract the cursor position in the event.
|
||||||
|
*
|
||||||
|
* @param {Object} e The associated DOM event.
|
||||||
|
*/
|
||||||
|
_getEventStart (e) {
|
||||||
|
const { timeStep, timeCellHeight, timeFrom, utils } = this._vuecal
|
||||||
|
let { y } = utils.cell.getPosition(e)
|
||||||
|
y -= e.dataTransfer.getData('cursor-grab-at') * 1
|
||||||
|
return Math.round(y * timeStep / parseInt(timeCellHeight) + timeFrom)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On drop, update the event start and end date directly into the event.
|
||||||
|
*
|
||||||
|
* @param {Object} e The associated DOM event.
|
||||||
|
* @param {Object} event The event being dragged.
|
||||||
|
* @param {Object} transferData The transfer data from the HTML5 dragging event.
|
||||||
|
* @param {Date} cellDate The hovered cell starting date.
|
||||||
|
*/
|
||||||
|
_updateEventStartEnd (e, event, transferData, cellDate) {
|
||||||
|
// If no duration calculate it from event.endTimeMinutes - event.startTimeMinutes
|
||||||
|
// before we modify the start and end.
|
||||||
|
const eventDuration = transferData.duration * 1 || (event.endTimeMinutes - event.startTimeMinutes)
|
||||||
|
|
||||||
|
// Force the start of the event at previous midnight minimum.
|
||||||
|
let startTimeMinutes = Math.max(this._getEventStart(e), 0)
|
||||||
|
|
||||||
|
// On drop, snap to time every X minutes if the option is on.
|
||||||
|
if (this._vuecal.snapToTime) {
|
||||||
|
const plusHalfSnapTime = (startTimeMinutes + this._vuecal.snapToTime / 2)
|
||||||
|
startTimeMinutes = plusHalfSnapTime - (plusHalfSnapTime % this._vuecal.snapToTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
event.startTimeMinutes = startTimeMinutes
|
||||||
|
event.start = new Date(new Date(cellDate).setMinutes(startTimeMinutes))
|
||||||
|
// Force the end of the event at next midnight maximum.
|
||||||
|
event.endTimeMinutes = Math.min(startTimeMinutes + eventDuration, 24 * 60)
|
||||||
|
event.end = new Date(new Date(cellDate).setMinutes(event.endTimeMinutes))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On event drag start, only possible if editableEvent is true.
|
||||||
|
* /!\ This is using the native HTML5 drag & drop, not supported on touch devices.
|
||||||
|
*
|
||||||
|
* @param {Object} e The associated DOM event.
|
||||||
|
* @param {Object} event The event being dragged.
|
||||||
|
*/
|
||||||
|
eventDragStart (e, event) {
|
||||||
|
// Cancel the drag if event has draggable set to false and trying to drag a text selection.
|
||||||
|
if (e.target.nodeType === 3) return e.preventDefault()
|
||||||
|
|
||||||
|
e.dataTransfer.dropEffect = 'move'
|
||||||
|
// Transfer the event's data to the receiver (when successfully drag & dropping out of Vue Cal).
|
||||||
|
// Notice: in Firefox the drag is prevented if there is no dataTransfer.setData().
|
||||||
|
e.dataTransfer.setData('event', JSON.stringify(event))
|
||||||
|
// When click and drag an event the cursor can be anywhere in the event,
|
||||||
|
// when later dropping the event, we need to subtract the cursor position in the event.
|
||||||
|
e.dataTransfer.setData('cursor-grab-at', e.offsetY) // In pixels.
|
||||||
|
|
||||||
|
const { clickHoldAnEvent } = this._vuecal.domEvents
|
||||||
|
// Cancel any delete on dragStart (if held for too long). Don't drag an event with a visible delete button.
|
||||||
|
setTimeout(() => {
|
||||||
|
clickHoldAnEvent._eid = null
|
||||||
|
clearTimeout(clickHoldAnEvent.timeoutId)
|
||||||
|
event.deleting = false
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
this._vuecal.domEvents.dragAnEvent._eid = event._eid // Only for vuecal dragging-event class.
|
||||||
|
dragging._eid = event._eid
|
||||||
|
dragging.fromVueCal = this._vuecal._.uid
|
||||||
|
event.dragging = true
|
||||||
|
// Controls the CSS class of the static event that remains while a copy is being dragged.
|
||||||
|
// Thanks to this class, the event being dragged can have a different style.
|
||||||
|
setTimeout(() => (event.draggingStatic = true), 0)
|
||||||
|
|
||||||
|
viewChanged = false
|
||||||
|
viewBeforeDrag = { id: this._vuecal.view.id, date: this._vuecal.view.startDate }
|
||||||
|
|
||||||
|
cancelViewChange = true // Re-init the cancel view: should cancel unless a cell received the event.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On event drag end, when releasing the event.
|
||||||
|
*
|
||||||
|
* @param {Object} event The event being dragged.
|
||||||
|
*/
|
||||||
|
eventDragEnd (event) {
|
||||||
|
this._vuecal.domEvents.dragAnEvent._eid = null
|
||||||
|
dragging._eid = null
|
||||||
|
event.dragging = false
|
||||||
|
event.draggingStatic = false
|
||||||
|
|
||||||
|
// If an event is dragged from a Vue Cal instance and dropped in a different one, remove the
|
||||||
|
// event from the first one.
|
||||||
|
const { fromVueCal, toVueCal } = dragging
|
||||||
|
if (toVueCal && fromVueCal !== toVueCal) this._vuecal.utils.event.deleteAnEvent(event)
|
||||||
|
dragging.fromVueCal = null
|
||||||
|
dragging.toVueCal = null
|
||||||
|
|
||||||
|
// When dropping the event, cancel view change if no cell received the event (in cellDragDrop).
|
||||||
|
if (viewChanged && cancelViewChange && viewBeforeDrag.id) this._vuecal.switchView(viewBeforeDrag.id, viewBeforeDrag.date, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On cell/split enter with a dragging event.
|
||||||
|
* Highlight the cell, and if on `years`, `year`, `month` view,
|
||||||
|
* set a timer to go deeper on drag hold over this cell.
|
||||||
|
*
|
||||||
|
* @param {Object} e The associated DOM event.
|
||||||
|
* @param {Object} cell The cell component's $data.
|
||||||
|
* @param {Date} cellDate The hovered cell starting date.
|
||||||
|
*/
|
||||||
|
cellDragEnter (e, cell, cellDate) {
|
||||||
|
const target = e.currentTarget
|
||||||
|
|
||||||
|
// Cancel dragEnter event if hovering a child.
|
||||||
|
if (e.currentTarget.contains(e.relatedTarget)) return
|
||||||
|
if (target === dragOverCell.el || !target.className.includes('vuecal__cell-content')) return false
|
||||||
|
|
||||||
|
// Un-highlight the previous cell.
|
||||||
|
if (dragOverCell.el) dragOverCell.cell.highlighted = false
|
||||||
|
|
||||||
|
dragOverCell = { el: target, cell, timeout: clearTimeout(dragOverCell.timeout) }
|
||||||
|
cell.highlighted = true
|
||||||
|
|
||||||
|
// On `years`, `year` & `month` views, go to narrower view on drag and hold.
|
||||||
|
if (['years', 'year', 'month'].includes(this._vuecal.view.id)) {
|
||||||
|
dragOverCell.timeout = setTimeout(() => this._vuecal.switchToNarrowerView(cellDate), 2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On cell/split drag over, highlight the cell being hovered,
|
||||||
|
* Useful when starting to drag event on the same cell/split it's in.
|
||||||
|
* Warning: This is fired repeatedly as long as you stay over this cell/split.
|
||||||
|
*
|
||||||
|
* @param {Object} e The associated DOM event.
|
||||||
|
* @param {Object} cell The cell component's $data.
|
||||||
|
* @param {Date} cellDate The hovered cell starting date.
|
||||||
|
* @param {Number|String} split The optional split being hovered if any.
|
||||||
|
*/
|
||||||
|
cellDragOver (e, cell, cellDate, split) {
|
||||||
|
e.preventDefault()
|
||||||
|
cell.highlighted = true
|
||||||
|
if (split || split === 0) cell.highlightedSplit = split
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When event drag leaves a cell/split.
|
||||||
|
* Remove the cell/split highlighted state.
|
||||||
|
* Warning: cell dragleave event happens AFTER another cell dragenter!
|
||||||
|
*
|
||||||
|
* @param {Object} e The associated DOM event.
|
||||||
|
* @param {Object} cell The cell component's $data.
|
||||||
|
*/
|
||||||
|
cellDragLeave (e, cell) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (e.currentTarget.contains(e.relatedTarget)) return
|
||||||
|
|
||||||
|
cell.highlightedSplit = false
|
||||||
|
|
||||||
|
// Only cancel the timer if leaving the current cell to no other one.
|
||||||
|
// If leaving this cell to enter another, a cancel is done in cellDragEnter,
|
||||||
|
// and a new timer is started.
|
||||||
|
if (dragOverCell.cell === cell) {
|
||||||
|
clearTimeout(dragOverCell.timeout)
|
||||||
|
dragOverCell = { el: null, cell: null, timeout: null }
|
||||||
|
cell.highlighted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On successful event drop into a cell/split.
|
||||||
|
* Change the event start and end time and remove the event dragging state
|
||||||
|
* and cell/split highlighted state.
|
||||||
|
*
|
||||||
|
* @param {Object} e The associated DOM event.
|
||||||
|
* @param {Object} cell The cell component's $data.
|
||||||
|
* @param {Date} cellDate The hovered cell starting date.
|
||||||
|
* @param {Number|String} split The optional split being dropped into, if any.
|
||||||
|
*/
|
||||||
|
cellDragDrop (e, cell, cellDate, split) {
|
||||||
|
// Needed to prevent navigation to the text set in dataTransfer from eventDragStart().
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
clearTimeout(dragOverCell.timeout)
|
||||||
|
dragOverCell = { el: null, cell: null, timeout: null }
|
||||||
|
|
||||||
|
const transferData = JSON.parse(e.dataTransfer.getData('event') || '{}')
|
||||||
|
let event, addToView
|
||||||
|
|
||||||
|
// If the event is not coming from this Vue Cal it means that we are accepting a new event.
|
||||||
|
// So create the event in this Vue Cal.
|
||||||
|
if (dragging.fromVueCal !== this._vuecal._.uid) {
|
||||||
|
// Removing the _eid is mandatory! It prevents the event to be duplicated when drag and
|
||||||
|
// dropping to another calendar then back to the original place.
|
||||||
|
const { _eid, start, end, duration, ...cleanTransferData } = transferData
|
||||||
|
// Note: createAnEvent adds the event to the view.
|
||||||
|
event = this._vuecal.utils.event.createAnEvent(cellDate, duration, { ...cleanTransferData, split })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Find the dragged event from its _eid in the view or mutableEvents array.
|
||||||
|
// If dragging the event to another day, the event is not in the view array but in the
|
||||||
|
// mutableEvents one.
|
||||||
|
event = this._vuecal.view.events.find(evt => evt._eid === dragging._eid)
|
||||||
|
|
||||||
|
if (!event) {
|
||||||
|
event = this._vuecal.mutableEvents.find(evt => evt._eid === dragging._eid)
|
||||||
|
addToView = !!event
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case where events are fetched from the backend and removed from the array when not in the view.
|
||||||
|
// So it won't be found in the mutableEvents array.
|
||||||
|
if (!event) {
|
||||||
|
const duration = transferData.endTimeMinutes - transferData.startTimeMinutes
|
||||||
|
// Pass exactly the same event as it was before the view change (same _eid as well) except dates.
|
||||||
|
const { start, end, ...cleanTransferData } = transferData
|
||||||
|
event = this._vuecal.utils.event.createAnEvent(cellDate, duration, { ...cleanTransferData, split })
|
||||||
|
// Note: createAnEvent adds the event to the view.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { start: oldDate, split: oldSplit } = event
|
||||||
|
this._updateEventStartEnd(e, event, transferData, cellDate)
|
||||||
|
|
||||||
|
// Only add the event to view after the start and end are modified otherwise
|
||||||
|
// it would be filtered out from the function if not in range.
|
||||||
|
if (addToView) this._vuecal.addEventsToView([event])
|
||||||
|
|
||||||
|
event.dragging = false
|
||||||
|
if (split || split === 0) event.split = split
|
||||||
|
|
||||||
|
cell.highlighted = false
|
||||||
|
cell.highlightedSplit = null
|
||||||
|
cancelViewChange = false
|
||||||
|
dragging.toVueCal = this._vuecal._.uid
|
||||||
|
|
||||||
|
// Emit `event-drop` & `event-change` events and return the updated event.
|
||||||
|
const params = {
|
||||||
|
event: this._vuecal.cleanupEvent(event),
|
||||||
|
oldDate,
|
||||||
|
newDate: event.start,
|
||||||
|
...((split || split === 0) && { oldSplit, newSplit: split }),
|
||||||
|
originalEvent: this._vuecal.cleanupEvent(transferData),
|
||||||
|
external: !dragging.fromVueCal // If external event, not coming from any Vue Cal.
|
||||||
|
}
|
||||||
|
this._vuecal.$emit('event-drop', params)
|
||||||
|
this._vuecal.$emit('event-change', { event: params.event, originalEvent: params.originalEvent })
|
||||||
|
|
||||||
|
// Sometimes the event dragend does not trigger (?!), so manually trigger it if it didn't.
|
||||||
|
setTimeout(() => {
|
||||||
|
if (dragging._eid) this.eventDragEnd(event)
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On drag enter on a view button or on today, prev & next buttons.
|
||||||
|
* Sets a highlighted state on the hovered button, and go to requested view.
|
||||||
|
*
|
||||||
|
* @param {Object} e The associated DOM event.
|
||||||
|
* @param {String} id The id of the header element being hovered. One of:
|
||||||
|
* previous, next, today, years, year, month, week, day.
|
||||||
|
* @param {Object} headerData The header component's $data.
|
||||||
|
*/
|
||||||
|
viewSelectorDragEnter (e, id, headerData) {
|
||||||
|
if (e.currentTarget.contains(e.relatedTarget)) return
|
||||||
|
|
||||||
|
headerData.highlightedControl = id
|
||||||
|
clearTimeout(changeViewTimeout)
|
||||||
|
changeViewTimeout = setTimeout(() => {
|
||||||
|
if (['previous', 'next'].includes(id)) {
|
||||||
|
this._vuecal[id]()
|
||||||
|
// Keep pressing on previous or next button until user goes away.
|
||||||
|
clearInterval(pressPrevOrNextInterval)
|
||||||
|
pressPrevOrNextInterval = setInterval(this._vuecal[id], holdOverTimeout)
|
||||||
|
}
|
||||||
|
else if (id === 'today') {
|
||||||
|
clearInterval(pressPrevOrNextInterval)
|
||||||
|
let viewId
|
||||||
|
if (this._vuecal.view.id.includes('year')) {
|
||||||
|
// If hovering today from a year or years view go to narrower view from month view.
|
||||||
|
viewId = this._vuecal.enabledViews.filter(view => !view.includes('year'))[0]
|
||||||
|
}
|
||||||
|
this._vuecal.switchView(viewId || this._vuecal.view.id, new Date(new Date().setHours(0, 0, 0, 0)), true)
|
||||||
|
}
|
||||||
|
else this._vuecal.switchView(id, null, true)
|
||||||
|
viewChanged = true
|
||||||
|
}, holdOverTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On drag leave on a view button or on today, prev & next buttons.
|
||||||
|
* Removes the highlighted state on the hovered button, and cancel the timer to
|
||||||
|
* go to the requested view.
|
||||||
|
*
|
||||||
|
* @param {Object} e The associated DOM event.
|
||||||
|
* @param {String} id The id of the header element being hovered. One of:
|
||||||
|
* previous, next, today, years, year, month, week, day.
|
||||||
|
* @param {Object} headerData The header component's $data.
|
||||||
|
*/
|
||||||
|
viewSelectorDragLeave (e, id, headerData) {
|
||||||
|
if (e.currentTarget.contains(e.relatedTarget)) return
|
||||||
|
|
||||||
|
// Only cancel the timer if leaving the current nav button to no other one.
|
||||||
|
// If leaving this nav button to enter another, a cancel is done in viewSelectorDragEnter,
|
||||||
|
// and a new timer is started.
|
||||||
|
if (headerData.highlightedControl === id) {
|
||||||
|
headerData.highlightedControl = null
|
||||||
|
if (changeViewTimeout) changeViewTimeout = clearTimeout(changeViewTimeout)
|
||||||
|
if (pressPrevOrNextInterval) pressPrevOrNextInterval = clearInterval(pressPrevOrNextInterval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
|
||||||
|
// Global structure.
|
||||||
|
// --------------------------------------------------------
|
||||||
|
* {margin: 0;padding: 0;}
|
||||||
|
|
||||||
|
html {scroll-behavior: smooth;}
|
||||||
|
|
||||||
|
#app {overflow-x: hidden;}
|
||||||
|
|
||||||
|
.w-app {
|
||||||
|
font: 15px 'Avenir', Helvetica, Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
background-color: #fff !important;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-top: 8em;
|
||||||
|
padding-top: 12em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: $page-max-width;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared classes & styles.
|
||||||
|
// --------------------------------------------------------
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: $primary;
|
||||||
|
|
||||||
|
&[name] {
|
||||||
|
position: relative;
|
||||||
|
top: -4em;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollable {
|
||||||
|
height: 250px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
padding-right: 1.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-style: italic;
|
||||||
|
margin: 3em 0 3.5em;
|
||||||
|
|
||||||
|
.w-icon {
|
||||||
|
font-size: 1.2em;
|
||||||
|
|
||||||
|
&.heart {transition: 1s ease-out;cursor: pointer;}
|
||||||
|
&.heart:hover {animation: pulse 1.8s ease-out infinite;}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 20%, 30%, 35%, 45%, 100% {transform: scale(1);}
|
||||||
|
25%, 40% {transform: scale(1.3);}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Media queries.
|
||||||
|
// --------------------------------------------------------
|
||||||
|
@media screen and (max-width: ($page-max-width + (2 * $container-padding-md))) {
|
||||||
|
:root {--container-padding: #{$container-padding-md};}
|
||||||
|
.page-container {
|
||||||
|
padding-left: var(--container-padding);
|
||||||
|
padding-right: var(--container-padding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: (500 + (2 * $container-padding-sm))) {
|
||||||
|
:root {--container-padding: #{$container-padding-sm};}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
$primary: #42b983;
|
||||||
|
$page-max-width: 1200px;
|
||||||
|
$container-padding-md: 20px;
|
||||||
|
$container-padding-sm: 12px;
|
||||||
|
|
||||||
|
@mixin backdrop-blur($blur: 4px, $fallback-color: rgba(255, 255, 255, 0.9), $blur-color: rgba(255, 255, 255, 0.6)) {
|
||||||
|
background-color: $fallback-color; // Slightly transparent fallback by default.
|
||||||
|
|
||||||
|
// If backdrop support: very transparent and blurred.
|
||||||
|
@supports ((-webkit-backdrop-filter: none) or (backdrop-filter: none)) {
|
||||||
|
& {
|
||||||
|
background-color: $blur-color;
|
||||||
|
backdrop-filter: blur($blur);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
@import './variables';
|
||||||
|
@import './base';
|
||||||
@@ -0,0 +1,323 @@
|
|||||||
|
$time-column-width: 3em;
|
||||||
|
$week-numbers-width: 3em;
|
||||||
|
$time-column-width-12: 4em; // twelve-hour clock shows am/pm.
|
||||||
|
$weekdays-headings-height: 2.8em;
|
||||||
|
$weekdays-headings-height-with-splits: 3.4em;
|
||||||
|
$splits-headers-height: 1.5em;
|
||||||
|
|
||||||
|
.vuecal {
|
||||||
|
height: 100%;
|
||||||
|
box-shadow: 0 0 0 1px inset rgba(0, 0, 0, 0.08);
|
||||||
|
|
||||||
|
// Disable user selection everywhere except in events.
|
||||||
|
* {user-select: none;}
|
||||||
|
// On touch device, disable everywhere except the event title if editable.
|
||||||
|
&--has-touch *:not(.vuecal__event-title--edit) {
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// General.
|
||||||
|
//==================================//
|
||||||
|
.clickable {cursor: pointer;}
|
||||||
|
|
||||||
|
&--resizing-event, &--drag-creating-event {cursor: ns-resize;}
|
||||||
|
&--dragging-event {
|
||||||
|
cursor: move;
|
||||||
|
cursor: -webkit-grabbing;
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragging-helper {
|
||||||
|
position: absolute;
|
||||||
|
width: 60px;
|
||||||
|
height: 40px;
|
||||||
|
background: rgba(138, 190, 230, 0.8);
|
||||||
|
border: 1px solid #61a9e0;
|
||||||
|
z-index: 10;
|
||||||
|
// transition: 0.2s transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--xsmall {font-size: 0.9em;}
|
||||||
|
|
||||||
|
&__flex {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
&[column] {
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[grow] {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[wrap] {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calendar header.
|
||||||
|
//==================================//
|
||||||
|
&__split-days-headers {
|
||||||
|
&.slide-fade--right-leave-active {display: none;}
|
||||||
|
|
||||||
|
.vuecal--week-numbers.vuecal--month-view & {margin-left: $week-numbers-width;}
|
||||||
|
.vuecal--day-view:not(.vuecal--overflow-x) & {margin-left: $time-column-width;height: 2.2em;}
|
||||||
|
.vuecal--day-view.vuecal--twelve-hour:not(.vuecal--overflow-x) & {margin-left: $time-column-width-12;}
|
||||||
|
|
||||||
|
.day-split-header {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0; // Evenly distribute.
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuecal--day-view.vuecal--overflow-x.vuecal--sticky-split-labels .day-split-header {height: $splits-headers-height;}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calendar body.
|
||||||
|
//==================================//
|
||||||
|
&__body {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__all-day {
|
||||||
|
min-height: 1.7em;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&-text {
|
||||||
|
width: $time-column-width;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #999;
|
||||||
|
padding-right: 2px;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
hyphens: auto;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 0.85em;
|
||||||
|
text-align: right;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.vuecal--twelve-hour & > span {width: $time-column-width-12;}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__bg {
|
||||||
|
overflow: auto;
|
||||||
|
overflow-x: hidden; // Prevent horizontal scroll bar while transitioning.
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
min-height: 60px;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
// margin-bottom: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--no-time &__bg {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__week-numbers {
|
||||||
|
width: $week-numbers-width;
|
||||||
|
flex-shrink: 0 !important;
|
||||||
|
|
||||||
|
.vuecal__week-number-cell {
|
||||||
|
opacity: 0.4;
|
||||||
|
font-size: 0.9em;
|
||||||
|
align-items: center;
|
||||||
|
justify-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__scrollbar-check {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
overflow: scroll;
|
||||||
|
visibility: hidden;
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
|
& div {height: 120%;}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__time-column {
|
||||||
|
width: $time-column-width;
|
||||||
|
height: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.vuecal--twelve-hour & {
|
||||||
|
width: $time-column-width-12;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuecal--overflow-x.vuecal--week-view & {
|
||||||
|
margin-top: $weekdays-headings-height;
|
||||||
|
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuecal--overflow-x.vuecal--week-view.vuecal--sticky-split-labels & {
|
||||||
|
margin-top: $weekdays-headings-height-with-splits;
|
||||||
|
}
|
||||||
|
.vuecal--overflow-x.vuecal--day-view.vuecal--sticky-split-labels & {margin-top: $splits-headers-height;}
|
||||||
|
|
||||||
|
.vuecal__time-cell {
|
||||||
|
color: #999;
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 2px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuecal__time-cell-line:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
border-top: 1px solid rgba(196, 196, 196, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calendar cells.
|
||||||
|
//==================================//
|
||||||
|
&__cells {
|
||||||
|
margin: 0 1px 1px 0;
|
||||||
|
.vuecal--overflow-x.vuecal--day-view & {margin: 0;}
|
||||||
|
|
||||||
|
.vuecal--events-on-month-view.vuecal--short-events & {width: 99.9%;}
|
||||||
|
|
||||||
|
.vuecal--overflow-x.vuecal--day-view &,
|
||||||
|
.vuecal--overflow-x.vuecal--week-view & {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transitions.
|
||||||
|
//==================================//
|
||||||
|
.slide-fade--left-enter-active, .slide-fade--left-leave-active,
|
||||||
|
.slide-fade--right-enter-active, .slide-fade--right-leave-active {
|
||||||
|
transition: 0.25s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade--left-enter-from,
|
||||||
|
.slide-fade--right-leave-to {
|
||||||
|
transform: translateX(-15px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade--left-leave-to,
|
||||||
|
.slide-fade--right-enter-from {
|
||||||
|
transform: translateX(15px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade--left-leave-active,
|
||||||
|
.slide-fade--right-leave-active {position: absolute !important;height: 100%;}
|
||||||
|
.vuecal__title-bar .slide-fade--left-leave-active,
|
||||||
|
.vuecal__title-bar .slide-fade--right-leave-active {left: 0;right: 0;height: auto;}
|
||||||
|
.vuecal__heading .slide-fade--left-leave-active,
|
||||||
|
.vuecal__heading .slide-fade--right-leave-active {display: flex;align-items: center;}
|
||||||
|
|
||||||
|
// Themes.
|
||||||
|
//==================================//
|
||||||
|
.vuecal--green-theme {
|
||||||
|
.vuecal__menu, .vuecal__cell-events-count {background-color: #42b983;color: #fff;}
|
||||||
|
.vuecal__title-bar {background-color: #e4f5ef;}
|
||||||
|
.vuecal__cell--today, .vuecal__cell--current {background-color: rgba(240, 240, 255, 0.4);}
|
||||||
|
&:not(.vuecal--day-view) .vuecal__cell--selected {background-color: rgba(235, 255, 245, 0.4);}
|
||||||
|
.vuecal__cell--selected:before {border-color: rgba(66, 185, 131, 0.5);}
|
||||||
|
.vuecal__cell--highlighted:not(.vuecal__cell--has-splits),
|
||||||
|
.vuecal__cell-split--highlighted {background-color: rgba(195, 255, 225, 0.5);}
|
||||||
|
.vuecal__arrow--highlighted,
|
||||||
|
.vuecal__today-btn--highlighted,
|
||||||
|
.vuecal__view-btn--highlighted {background-color: rgba(136, 236, 191, 0.25);}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuecal--blue-theme {
|
||||||
|
.vuecal__menu, .vuecal__cell-events-count {background-color: rgba(66, 163, 185, 0.8);color: #fff;}
|
||||||
|
.vuecal__title-bar {background-color: rgba(0, 165, 188, 0.3);}
|
||||||
|
.vuecal__cell--today, .vuecal__cell--current {background-color: rgba(240, 240, 255, 0.4);}
|
||||||
|
&:not(.vuecal--day-view) .vuecal__cell--selected {background-color: rgba(235, 253, 255, 0.4);}
|
||||||
|
.vuecal__cell--selected:before {border-color: rgba(115, 191, 204, 0.5);}
|
||||||
|
.vuecal__cell--highlighted:not(.vuecal__cell--has-splits),
|
||||||
|
.vuecal__cell-split--highlighted {background-color: rgba(0, 165, 188, 0.06);}
|
||||||
|
.vuecal__arrow--highlighted,
|
||||||
|
.vuecal__today-btn--highlighted,
|
||||||
|
.vuecal__view-btn--highlighted {background-color: rgba(66, 163, 185, 0.2);}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rounded cells.
|
||||||
|
.vuecal--rounded-theme {
|
||||||
|
.vuecal__weekdays-headings {border: none;}
|
||||||
|
.vuecal__cell, .vuecal__cell:before {background: none;border: none;}
|
||||||
|
.vuecal__cell--out-of-scope {opacity: 0.4;}
|
||||||
|
.vuecal__cell-content {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
flex-grow: 0;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 30px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
&.vuecal--day-view .vuecal__cell-content {width: auto;background: none;}
|
||||||
|
&.vuecal--year-view .vuecal__cell {width: 33.33%;}
|
||||||
|
&.vuecal--year-view .vuecal__cell-content {width: 85px;}
|
||||||
|
&.vuecal--years-view .vuecal__cell-content {width: 52px;}
|
||||||
|
.vuecal__cell {background-color: transparent !important;}
|
||||||
|
|
||||||
|
&.vuecal--green-theme {
|
||||||
|
&:not(.vuecal--day-view) .vuecal__cell-content {background-color: #f1faf7;}
|
||||||
|
&:not(.vuecal--day-view) .vuecal__cell--today .vuecal__cell-content {background-color: #42b983;color: #fff;}
|
||||||
|
.vuecal--day-view .vuecal__cell--today:before {background-color: rgba(66, 185, 131, 0.05);}
|
||||||
|
&:not(.vuecal--day-view) .vuecal__cell--selected .vuecal__cell-content {border-color: #42b983;}
|
||||||
|
.vuecal__cell--highlighted:not(.vuecal__cell--has-splits),
|
||||||
|
.vuecal__cell-split--highlighted {background-color: rgba(195, 255, 225, 0.5);}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.vuecal--blue-theme {
|
||||||
|
&:not(.vuecal--day-view) .vuecal__cell-content {background-color: rgba(100, 182, 255, 0.2);}
|
||||||
|
&:not(.vuecal--day-view) .vuecal__cell--today .vuecal__cell-content {background-color: #8fb7e4;color: #fff;}
|
||||||
|
.vuecal--day-view .vuecal__cell--today:before {background-color: rgba(143, 183, 228, 0.1);}
|
||||||
|
&:not(.vuecal--day-view) .vuecal__cell--selected .vuecal__cell-content {border-color: #61a9e0;}
|
||||||
|
.vuecal__cell--highlighted:not(.vuecal__cell--has-splits),
|
||||||
|
.vuecal__cell-split--highlighted {background-color: rgba(0, 165, 188, 0.06);}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuecal--date-picker {
|
||||||
|
.vuecal__title-bar {font-size: 1.2em;}
|
||||||
|
.vuecal__heading {height: 2.2em;font-weight: 500;opacity: 0.4;}
|
||||||
|
.vuecal__weekdays-headings {border: none;}
|
||||||
|
.vuecal__body {margin-left: 1px;}
|
||||||
|
.vuecal__cell, .vuecal__cell:before {background: none;border: none;}
|
||||||
|
.vuecal__cell-content {
|
||||||
|
height: 26px;
|
||||||
|
flex-grow: 0;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 25px;
|
||||||
|
transition: background-color 0.2s cubic-bezier(0.39, 0.58, 0.57, 1);
|
||||||
|
}
|
||||||
|
&.vuecal--years-view .vuecal__cell-content {flex: 0;padding: 0 4px;height: 24px;}
|
||||||
|
&.vuecal--year-view .vuecal__cell-content {flex: 0;padding: 0 15px;}
|
||||||
|
&.vuecal--month-view .vuecal__cell-content {width: 26px;}
|
||||||
|
|
||||||
|
&:not(.vuecal--day-view) .vuecal__cell-content:hover {background-color: rgba(0, 0, 0, 0.1);}
|
||||||
|
&:not(.vuecal--day-view) .vuecal__cell--selected .vuecal__cell-content {background-color: #42b982;color: #fff;}
|
||||||
|
&:not(.vuecal--day-view) .vuecal__cell--current .vuecal__cell-content,
|
||||||
|
&:not(.vuecal--day-view) .vuecal__cell--today .vuecal__cell-content {border-color: #42b982;}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* Cell Utils.
|
||||||
|
*
|
||||||
|
* Waiting for VS Code to support JavaScript private fields.
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Class_fields
|
||||||
|
* Meantime keep `_` for private.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const minutesInADay = 24 * 60
|
||||||
|
|
||||||
|
export default class CellUtils {
|
||||||
|
_vuecal = null
|
||||||
|
|
||||||
|
constructor (vuecal) {
|
||||||
|
this._vuecal = vuecal
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a cell and go to narrower view on double click or single click according to vuecalProps option.
|
||||||
|
*
|
||||||
|
* @param {Boolean} force Force switching to narrower view.
|
||||||
|
* @param {Date} date The selected cell date at the exact time where it was clicked (through cursor coords).
|
||||||
|
* @param {Integer} split The selected cell split if any.
|
||||||
|
*/
|
||||||
|
selectCell = (force = false, date, split) => {
|
||||||
|
// Cell-click event returns a date and time at cursor position.
|
||||||
|
this._vuecal.$emit('cell-click', split ? { date, split } : date)
|
||||||
|
|
||||||
|
// Switch to narrower view.
|
||||||
|
if (this._vuecal.clickToNavigate || force) this._vuecal.switchToNarrowerView()
|
||||||
|
|
||||||
|
// Handle double click manually for touch devices.
|
||||||
|
else if (this._vuecal.dblclickToNavigate && 'ontouchstart' in window) {
|
||||||
|
this._vuecal.domEvents.dblTapACell.taps++
|
||||||
|
|
||||||
|
setTimeout(() => (this._vuecal.domEvents.dblTapACell.taps = 0), this._vuecal.domEvents.dblTapACell.timeout)
|
||||||
|
|
||||||
|
if (this._vuecal.domEvents.dblTapACell.taps >= 2) {
|
||||||
|
this._vuecal.domEvents.dblTapACell.taps = 0
|
||||||
|
this._vuecal.switchToNarrowerView()
|
||||||
|
this._vuecal.$emit('cell-dblclick', split ? { date, split } : date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a cell and go to narrower view on enter.
|
||||||
|
*
|
||||||
|
* @param {Boolean} force Force switching to narrower view.
|
||||||
|
* @param {Date} date The selected cell date at the exact time where it was clicked (through cursor coords).
|
||||||
|
* @param {Integer} split The selected cell split if any.
|
||||||
|
*/
|
||||||
|
keyPressEnterCell = (date, split) => {
|
||||||
|
// Cell-key-press-enter event returns a date and time at cursor position.
|
||||||
|
this._vuecal.$emit('cell-keypress-enter', split ? { date, split } : date)
|
||||||
|
|
||||||
|
// Switch to narrower view.
|
||||||
|
this._vuecal.switchToNarrowerView()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the coordinates of the mouse cursor from the cells wrapper referential (`ref="cells"`).
|
||||||
|
*
|
||||||
|
* @todo Cache bounding box & update it on resize.
|
||||||
|
* @param {Object} e the native DOM event object.
|
||||||
|
* @return {Object} containing { x: {Number}, y: {Number} }
|
||||||
|
*/
|
||||||
|
getPosition = e => {
|
||||||
|
const { left, top } = this._vuecal.cellsEl.getBoundingClientRect()
|
||||||
|
const { clientX, clientY } = 'ontouchstart' in window && e.touches ? e.touches[0] : e
|
||||||
|
return { x: clientX - left, y: clientY - top }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of minutes from the top to the mouse cursor.
|
||||||
|
* Returns a constrained time between 0 and 24 * 60.
|
||||||
|
*
|
||||||
|
* @param {Object} e the native DOM event object.
|
||||||
|
* @return {Object} containing { minutes: {Number}, cursorCoords: { x: {Number}, y: {Number} } }
|
||||||
|
*/
|
||||||
|
minutesAtCursor = e => {
|
||||||
|
let minutes = 0
|
||||||
|
let cursorCoords = { x: 0, y: 0 }
|
||||||
|
const { timeStep, timeCellHeight, timeFrom } = this._vuecal.$props
|
||||||
|
|
||||||
|
if (typeof e === 'number') minutes = e
|
||||||
|
else if (typeof e === 'object') {
|
||||||
|
cursorCoords = this.getPosition(e)
|
||||||
|
minutes = Math.round(cursorCoords.y * timeStep / parseInt(timeCellHeight) + timeFrom)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { minutes: Math.max(Math.min(minutes, minutesInADay), 0), cursorCoords }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,376 @@
|
|||||||
|
/**
|
||||||
|
* Date Utils & prototypes.
|
||||||
|
*
|
||||||
|
* Waiting for VS Code to support JavaScript private fields.
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Class_fields
|
||||||
|
* Meantime keep `_` for private.
|
||||||
|
*/
|
||||||
|
|
||||||
|
let now, todayDate, todayF, self
|
||||||
|
let _dateObject = {}
|
||||||
|
let _timeObject = {}
|
||||||
|
|
||||||
|
export default class DateUtils {
|
||||||
|
texts = {}
|
||||||
|
|
||||||
|
constructor (texts, noPrototypes = false) {
|
||||||
|
self = this // For use in Date prototypes.
|
||||||
|
this._texts = texts
|
||||||
|
|
||||||
|
// Add prototypes ASAP - only once.
|
||||||
|
if (!noPrototypes && Date && !Date.prototype.addDays) this._initDatePrototypes()
|
||||||
|
|
||||||
|
// @todo: This would be nicer, but how to set Date.noPrototypes ASAP only if user wants?
|
||||||
|
// if (Date.noPrototypes) delete Date.noPrototypes
|
||||||
|
// else this._initDatePrototypes()
|
||||||
|
}
|
||||||
|
|
||||||
|
_initDatePrototypes () {
|
||||||
|
/* eslint-disable no-extend-native */
|
||||||
|
Date.prototype.addDays = function (days) { return self.addDays(this, days) }
|
||||||
|
Date.prototype.subtractDays = function (days) { return self.subtractDays(this, days) }
|
||||||
|
Date.prototype.addHours = function (hours) { return self.addHours(this, hours) }
|
||||||
|
Date.prototype.subtractHours = function (hours) { return self.subtractHours(this, hours) }
|
||||||
|
Date.prototype.addMinutes = function (minutes) { return self.addMinutes(this, minutes) }
|
||||||
|
Date.prototype.subtractMinutes = function (minutes) { return self.subtractMinutes(this, minutes) }
|
||||||
|
Date.prototype.getWeek = function () { return self.getWeek(this) }
|
||||||
|
Date.prototype.isToday = function () { return self.isToday(this) }
|
||||||
|
Date.prototype.isLeapYear = function () { return self.isLeapYear(this) }
|
||||||
|
Date.prototype.format = function (format = 'YYYY-MM-DD') { return self.formatDate(this, format) }
|
||||||
|
Date.prototype.formatTime = function (format = 'HH:mm') { return self.formatTime(this, format) }
|
||||||
|
/* eslint-enable no-extend-native */
|
||||||
|
}
|
||||||
|
|
||||||
|
removePrototypes () {
|
||||||
|
delete Date.prototype.addDays
|
||||||
|
delete Date.prototype.subtractDays
|
||||||
|
delete Date.prototype.addHours
|
||||||
|
delete Date.prototype.subtractHours
|
||||||
|
delete Date.prototype.addMinutes
|
||||||
|
delete Date.prototype.subtractMinutes
|
||||||
|
delete Date.prototype.getWeek
|
||||||
|
delete Date.prototype.isToday
|
||||||
|
delete Date.prototype.isLeapYear
|
||||||
|
delete Date.prototype.format
|
||||||
|
delete Date.prototype.formatTime
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTexts (texts) {
|
||||||
|
this._texts = texts
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache Today's date (to a maximum) for better isToday() performances. Formatted without leading 0.
|
||||||
|
// We still need to update Today's date when Today changes without page refresh.
|
||||||
|
_todayFormatted () {
|
||||||
|
if (todayDate !== (new Date()).getDate()) {
|
||||||
|
now = new Date()
|
||||||
|
todayDate = now.getDate()
|
||||||
|
todayF = `${now.getFullYear()}-${now.getMonth()}-${now.getDate()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return todayF
|
||||||
|
}
|
||||||
|
|
||||||
|
// UTILITIES.
|
||||||
|
// ====================================================================
|
||||||
|
addDays (date, days) {
|
||||||
|
const d = new Date(date.valueOf())
|
||||||
|
d.setDate(d.getDate() + days)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
subtractDays (date, days) {
|
||||||
|
const d = new Date(date.valueOf())
|
||||||
|
d.setDate(d.getDate() - days)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
addHours (date, hours) {
|
||||||
|
const d = new Date(date.valueOf())
|
||||||
|
d.setHours(d.getHours() + hours)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
subtractHours (date, hours) {
|
||||||
|
const d = new Date(date.valueOf())
|
||||||
|
d.setHours(d.getHours() - hours)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
addMinutes (date, minutes) {
|
||||||
|
const d = new Date(date.valueOf())
|
||||||
|
d.setMinutes(d.getMinutes() + minutes)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
subtractMinutes (date, minutes) {
|
||||||
|
const d = new Date(date.valueOf())
|
||||||
|
d.setMinutes(d.getMinutes() - minutes)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
getWeek (date) {
|
||||||
|
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()))
|
||||||
|
const dayNum = d.getUTCDay() || 7
|
||||||
|
d.setUTCDate(d.getUTCDate() + 4 - dayNum)
|
||||||
|
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1))
|
||||||
|
return Math.ceil((((d - yearStart) / 86400000) + 1) / 7)
|
||||||
|
}
|
||||||
|
|
||||||
|
isToday (date) {
|
||||||
|
return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}` === this._todayFormatted()
|
||||||
|
}
|
||||||
|
|
||||||
|
isLeapYear (date) {
|
||||||
|
const year = date.getFullYear()
|
||||||
|
return !(year % 400) || (year % 100 && !(year % 4))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns today if it's FirstDayOfWeek (Monday or Sunday) or previous FirstDayOfWeek otherwise.
|
||||||
|
getPreviousFirstDayOfWeek (date = null, weekStartsOnSunday) {
|
||||||
|
const prevFirstDayOfWeek = (date && new Date(date.valueOf())) || new Date()
|
||||||
|
const dayModifier = weekStartsOnSunday ? 7 : 6
|
||||||
|
prevFirstDayOfWeek.setDate(prevFirstDayOfWeek.getDate() - (prevFirstDayOfWeek.getDay() + dayModifier) % 7)
|
||||||
|
return prevFirstDayOfWeek
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a string to a Javascript Date object. If a Date object is passed, return it as is.
|
||||||
|
*
|
||||||
|
* @param {String | Date} date the string to convert to Date.
|
||||||
|
* @return {Date} the equivalent Javascript Date object.
|
||||||
|
*/
|
||||||
|
stringToDate (date) {
|
||||||
|
if (date instanceof Date) return date
|
||||||
|
// Regexp way is less performant: https://jsperf.com/string-to-date-regexp-vs-new-date
|
||||||
|
// const [, y, m, d, h = 0, min = 0] = date.match(/(\d{4})-(\d{2})-(\d{2})(?: (\d{2}):(\d{2}))?/)
|
||||||
|
// return new Date(y, parseInt(m) - 1, d, h, min)
|
||||||
|
|
||||||
|
if (date.length === 10) date += ' 00:00'
|
||||||
|
return new Date(date.replace(/-/g, '/')) // replace '-' with '/' for Safari.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simply takes a Date and returns the associated time in minutes (sum of hours + minutes).
|
||||||
|
*
|
||||||
|
* @param {Date} date the JavaScript Date to extract minutes from.
|
||||||
|
* @return {Number} the number of minutes (total of hours plus minutes).
|
||||||
|
*/
|
||||||
|
dateToMinutes = date => date.getHours() * 60 + date.getMinutes()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count the number of days this date range spans onto.
|
||||||
|
* E.g. countDays(2019-11-02 18:00, 2019-11-03 02:00) = 2
|
||||||
|
*
|
||||||
|
* @param {String | Date} start the start date
|
||||||
|
* @param {String | Date} end the end date
|
||||||
|
* @return {Integer} The number of days this date range involves
|
||||||
|
*/
|
||||||
|
countDays (start, end) {
|
||||||
|
// replace '-' with '/' for Safari.
|
||||||
|
if (typeof start === 'string') start = start.replace(/-/g, '/')
|
||||||
|
if (typeof end === 'string') end = end.replace(/-/g, '/')
|
||||||
|
|
||||||
|
// Set start & end at midnight then compare the delta. Don't modify the original dates.
|
||||||
|
start = (new Date(start)).setHours(0, 0, 0, 0)
|
||||||
|
// Set end at midnight plus 1 sec, so Math.ceil will round it up to a full day.
|
||||||
|
end = (new Date(end)).setHours(0, 0, 1, 0)
|
||||||
|
|
||||||
|
// Remove the potential daylight saving delta.
|
||||||
|
const timezoneDiffMs = (new Date(end).getTimezoneOffset() - new Date(start).getTimezoneOffset()) * 60 * 1000
|
||||||
|
return Math.ceil((end - start - timezoneDiffMs) / (24 * 3600 * 1000))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take 2 dates and check if within the same time step (useful in overlapping events).
|
||||||
|
*
|
||||||
|
* @return {Boolean} `true` if their time is included in the same time step,
|
||||||
|
* this means these 2 dates are very close.
|
||||||
|
*/
|
||||||
|
datesInSameTimeStep (date1, date2, timeStep) {
|
||||||
|
return Math.abs(date1.getTime() - date2.getTime()) <= timeStep * 60 * 1000
|
||||||
|
}
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
|
// FORMATTERS.
|
||||||
|
// ====================================================================
|
||||||
|
/**
|
||||||
|
* Formats a date/time to the given format and returns the formatted string.
|
||||||
|
*
|
||||||
|
* @param {Date} date a JavaScript Date object to format.
|
||||||
|
* @param {String} format the wanted format.
|
||||||
|
* @param {Object} texts Optional: the localized texts object to override the vue-cal one in this._texts.
|
||||||
|
* This becomes useful when showing multiple instances with different languages,
|
||||||
|
* like in the documentation page.
|
||||||
|
* @return {String} the formatted date.
|
||||||
|
*/
|
||||||
|
formatDate (date, format = 'YYYY-MM-DD', texts = null) {
|
||||||
|
if (!texts) texts = this._texts
|
||||||
|
if (!format) format = 'YYYY-MM-DD' // Allows passing null for default format.
|
||||||
|
if (format === 'YYYY-MM-DD') return this.formatDateLite(date)
|
||||||
|
|
||||||
|
// Reinit the date and time object on each function call.
|
||||||
|
_dateObject = {}
|
||||||
|
_timeObject = {}
|
||||||
|
|
||||||
|
// Each keyword is a function to load the dateObject or timeObject on demand: no wasted resource.
|
||||||
|
const dateObj = {
|
||||||
|
YYYY: () => this._hydrateDateObject(date, texts).YYYY,
|
||||||
|
YY: () => this._hydrateDateObject(date, texts).YY(),
|
||||||
|
M: () => this._hydrateDateObject(date, texts).M,
|
||||||
|
MM: () => this._hydrateDateObject(date, texts).MM(),
|
||||||
|
MMM: () => this._hydrateDateObject(date, texts).MMM(),
|
||||||
|
MMMM: () => this._hydrateDateObject(date, texts).MMMM(),
|
||||||
|
MMMMG: () => this._hydrateDateObject(date, texts).MMMMG(),
|
||||||
|
D: () => this._hydrateDateObject(date, texts).D,
|
||||||
|
DD: () => this._hydrateDateObject(date, texts).DD(),
|
||||||
|
S: () => this._hydrateDateObject(date, texts).S(),
|
||||||
|
d: () => this._hydrateDateObject(date, texts).d,
|
||||||
|
dd: () => this._hydrateDateObject(date, texts).dd(),
|
||||||
|
ddd: () => this._hydrateDateObject(date, texts).ddd(),
|
||||||
|
dddd: () => this._hydrateDateObject(date, texts).dddd(),
|
||||||
|
HH: () => this._hydrateTimeObject(date, texts).HH,
|
||||||
|
H: () => this._hydrateTimeObject(date, texts).H,
|
||||||
|
hh: () => this._hydrateTimeObject(date, texts).hh,
|
||||||
|
h: () => this._hydrateTimeObject(date, texts).h,
|
||||||
|
am: () => this._hydrateTimeObject(date, texts).am,
|
||||||
|
AM: () => this._hydrateTimeObject(date, texts).AM,
|
||||||
|
mm: () => this._hydrateTimeObject(date, texts).mm,
|
||||||
|
m: () => this._hydrateTimeObject(date, texts).m
|
||||||
|
}
|
||||||
|
|
||||||
|
return format.replace(/(\{[a-zA-Z]+\}|[a-zA-Z]+)/g, (m, contents) => {
|
||||||
|
const result = dateObj[contents.replace(/\{|\}/g, '')]
|
||||||
|
return result !== undefined ? result() : contents
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// More performant function to convert a Date to `YYYY-MM-DD` formatted string only.
|
||||||
|
formatDateLite (date) {
|
||||||
|
const m = date.getMonth() + 1
|
||||||
|
const d = date.getDate()
|
||||||
|
return `${date.getFullYear()}-${m < 10 ? '0' : ''}${m}-${d < 10 ? '0' : ''}${d}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a time (from Date or number of mins) to the given format and returns the formatted string.
|
||||||
|
*
|
||||||
|
* @param {Date | Number} date a JavaScript Date object or a time in minutes.
|
||||||
|
* @param {String} format the wanted format.
|
||||||
|
* @param {Object} texts Optional: the localized texts object to override the vue-cal one in this._texts.
|
||||||
|
* This becomes useful when showing multiple instances with different languages,
|
||||||
|
* like in the documentation page.
|
||||||
|
* @param {Boolean} round if time is 23:59:59, rounds up to 24:00 for formatting only.
|
||||||
|
* @return {String} the formatted time.
|
||||||
|
*/
|
||||||
|
formatTime (date, format = 'HH:mm', texts = null, round = false) {
|
||||||
|
let shouldRound = false
|
||||||
|
if (round) {
|
||||||
|
const [h, m, s] = [date.getHours(), date.getMinutes(), date.getSeconds()]
|
||||||
|
if ((h + m + s) === (23 + 59 + 59)) shouldRound = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (date instanceof Date && format === 'HH:mm') return shouldRound ? '24:00' : this.formatTimeLite(date)
|
||||||
|
|
||||||
|
_timeObject = {} // Reinit the time object on each function call.
|
||||||
|
if (!texts) texts = this._texts
|
||||||
|
const timeObj = this._hydrateTimeObject(date, texts)
|
||||||
|
|
||||||
|
const formatted = format.replace(/(\{[a-zA-Z]+\}|[a-zA-Z]+)/g, (m, contents) => {
|
||||||
|
const result = timeObj[contents.replace(/\{|\}/g, '')]
|
||||||
|
return result !== undefined ? result : contents
|
||||||
|
})
|
||||||
|
|
||||||
|
// Round 23:59:59 to 24:00. For 12-hour format there is nothing to replace: as both are 12am.
|
||||||
|
// Also don't return `24:00` straight away as the user format may be different.
|
||||||
|
return shouldRound ? formatted.replace('23:59', '24:00') : formatted
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a time to 'HH:mm' from a Date and returns the formatted string.
|
||||||
|
*
|
||||||
|
* @param {Date} date a JavaScript Date object to format.
|
||||||
|
* @return {String} the formatted time.
|
||||||
|
*/
|
||||||
|
formatTimeLite (date) {
|
||||||
|
const h = date.getHours()
|
||||||
|
const m = date.getMinutes()
|
||||||
|
return `${(h < 10 ? '0' : '') + h}:${(m < 10 ? '0' : '') + m}`
|
||||||
|
}
|
||||||
|
|
||||||
|
_nth (d) {
|
||||||
|
if (d > 3 && d < 21) return 'th'
|
||||||
|
switch (d % 10) {
|
||||||
|
case 1: return 'st'
|
||||||
|
case 2: return 'nd'
|
||||||
|
case 3: return 'rd'
|
||||||
|
default: return 'th'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_hydrateDateObject (date, texts) {
|
||||||
|
if (_dateObject.D) return _dateObject
|
||||||
|
|
||||||
|
const YYYY = date.getFullYear()
|
||||||
|
const M = date.getMonth() + 1
|
||||||
|
const D = date.getDate()
|
||||||
|
const day = date.getDay() // Day of the week.
|
||||||
|
const dayNumber = (day - 1 + 7) % 7 // Day of the week. 0 to 6 with 6 = Sunday.
|
||||||
|
// Some of this props are functions, to only calculate on demand.
|
||||||
|
_dateObject = {
|
||||||
|
// Year.
|
||||||
|
YYYY, // 2019.
|
||||||
|
YY: () => YYYY.toString().substring(2), // 19.
|
||||||
|
|
||||||
|
// Month.
|
||||||
|
M, // 1 to 12.
|
||||||
|
MM: () => (M < 10 ? '0' : '') + M, // 01 to 12.
|
||||||
|
MMM: () => texts.months[M - 1].substring(0, 3), // Jan to Dec.
|
||||||
|
MMMM: () => texts.months[M - 1], // January to December.
|
||||||
|
MMMMG: () => (texts.monthsGenitive || texts.months)[M - 1], // January to December in genitive form (Greek...)
|
||||||
|
|
||||||
|
// Day.
|
||||||
|
D, // 1 to 31.
|
||||||
|
DD: () => (D < 10 ? '0' : '') + D, // 01 to 31.
|
||||||
|
S: () => this._nth(D), // st, nd, rd, th.
|
||||||
|
|
||||||
|
// Day of the week.
|
||||||
|
d: dayNumber + 1, // 1 to 7 with 7 = Sunday.
|
||||||
|
dd: () => texts.weekDays[dayNumber][0], // M to S.
|
||||||
|
ddd: () => texts.weekDays[dayNumber].substr(0, 3), // Mon to Sun.
|
||||||
|
dddd: () => texts.weekDays[dayNumber] // Monday to Sunday.
|
||||||
|
}
|
||||||
|
|
||||||
|
return _dateObject
|
||||||
|
}
|
||||||
|
|
||||||
|
_hydrateTimeObject (date, texts) {
|
||||||
|
if (_timeObject.am) return _timeObject
|
||||||
|
|
||||||
|
let H, m
|
||||||
|
if (date instanceof Date) {
|
||||||
|
H = date.getHours()
|
||||||
|
m = date.getMinutes()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
H = Math.floor(date / 60)
|
||||||
|
m = Math.floor(date % 60)
|
||||||
|
}
|
||||||
|
|
||||||
|
const h = H % 12 ? H % 12 : 12
|
||||||
|
const am = (texts || { am: 'am', pm: 'pm' })[H === 24 || H < 12 ? 'am' : 'pm']
|
||||||
|
_timeObject = {
|
||||||
|
H,
|
||||||
|
h,
|
||||||
|
HH: (H < 10 ? '0' : '') + H,
|
||||||
|
hh: (h < 10 ? '0' : '') + h,
|
||||||
|
am,
|
||||||
|
AM: am.toUpperCase(),
|
||||||
|
m,
|
||||||
|
mm: (m < 10 ? '0' : '') + m
|
||||||
|
}
|
||||||
|
|
||||||
|
return _timeObject
|
||||||
|
}
|
||||||
|
// ====================================================================
|
||||||
|
}
|
||||||
@@ -0,0 +1,426 @@
|
|||||||
|
/**
|
||||||
|
* Events Utils.
|
||||||
|
*
|
||||||
|
* Waiting for VS Code to support JavaScript private fields.
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Class_fields
|
||||||
|
* Meantime keep `_` for private.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const defaultEventDuration = 2 // In hours.
|
||||||
|
const minutesInADay = 24 * 60 // Don't do the maths every time.
|
||||||
|
|
||||||
|
let ud
|
||||||
|
|
||||||
|
// Event overlaps: only for the current view, recreated on view change.
|
||||||
|
let _cellOverlaps, _comparisonArray
|
||||||
|
|
||||||
|
// This is an approximation, it will not work with DLS time.
|
||||||
|
// const approxDayMilliseconds = minutesInADay * 60 * 1000
|
||||||
|
// This is an approximate minimum we can get in a year. Purposely stay below 365 but close.
|
||||||
|
// const minYearMilliseconds = 364 * approxDayMilliseconds // Don't do the maths every time.
|
||||||
|
|
||||||
|
export default class EventUtils {
|
||||||
|
_vuecal = null
|
||||||
|
|
||||||
|
eventDefaults = {
|
||||||
|
_eid: null,
|
||||||
|
start: '', // Externally given formatted date & time or Date object.
|
||||||
|
startTimeMinutes: 0,
|
||||||
|
end: '', // Externally given formatted date & time or Date object.
|
||||||
|
endTimeMinutes: 0,
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
background: false,
|
||||||
|
allDay: false,
|
||||||
|
segments: null,
|
||||||
|
repeat: null,
|
||||||
|
daysCount: 1,
|
||||||
|
deletable: true,
|
||||||
|
deleting: false,
|
||||||
|
titleEditable: true,
|
||||||
|
resizable: true,
|
||||||
|
resizing: false,
|
||||||
|
draggable: true,
|
||||||
|
dragging: false,
|
||||||
|
draggingStatic: false, // Controls the CSS class of the static clone while dragging.
|
||||||
|
focused: false,
|
||||||
|
class: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor (vuecal, dateUtils) {
|
||||||
|
this._vuecal = vuecal
|
||||||
|
ud = dateUtils
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an event at the given date and time, and allow overriding
|
||||||
|
* event attributes through the eventOptions object.
|
||||||
|
*
|
||||||
|
* @param {Date | String} dateTime The date and time of the new event start.
|
||||||
|
* @param {Number} duration the event duration in minutes.
|
||||||
|
* @param {Object} eventOptions some options to override the `eventDefaults` - optional.
|
||||||
|
*/
|
||||||
|
createAnEvent (dateTime, duration, eventOptions) {
|
||||||
|
if (typeof dateTime === 'string') dateTime = ud.stringToDate(dateTime)
|
||||||
|
if (!(dateTime instanceof Date)) return false
|
||||||
|
|
||||||
|
const startTimeMinutes = ud.dateToMinutes(dateTime)
|
||||||
|
duration = duration * 1 || defaultEventDuration * 60
|
||||||
|
const endTimeMinutes = startTimeMinutes + duration
|
||||||
|
const end = ud.addMinutes(new Date(dateTime), duration)
|
||||||
|
|
||||||
|
// Automatically add the required endTimeMinutes when passing an end.
|
||||||
|
if (eventOptions.end) {
|
||||||
|
if (typeof eventOptions.end === 'string') eventOptions.end = ud.stringToDate(eventOptions.end)
|
||||||
|
eventOptions.endTimeMinutes = ud.dateToMinutes(eventOptions.end)
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = {
|
||||||
|
...this.eventDefaults,
|
||||||
|
_eid: `${this._vuecal._.uid}_${this._vuecal.eventIdIncrement++}`,
|
||||||
|
start: dateTime,
|
||||||
|
startTimeMinutes,
|
||||||
|
end,
|
||||||
|
endTimeMinutes,
|
||||||
|
segments: null,
|
||||||
|
...eventOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the onEventCreate() function is given as a parameter to vue-cal:
|
||||||
|
// 1. give it access to the created event & the deleteAnEvent() function.
|
||||||
|
// 2. Prevent creation of the event if this function returns false.
|
||||||
|
if (typeof this._vuecal.onEventCreate === 'function') {
|
||||||
|
if (!this._vuecal.onEventCreate(event, () => this.deleteAnEvent(event))) return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if event is a multiple day event and update days count.
|
||||||
|
if (event.startDateF !== event.endDateF) {
|
||||||
|
event.daysCount = ud.countDays(event.start, event.end)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event to the mutableEvents array.
|
||||||
|
this._vuecal.mutableEvents.push(event)
|
||||||
|
|
||||||
|
// Add the new event to the current view.
|
||||||
|
// The event may have been edited on the fly to become a multiple-day event,
|
||||||
|
// the method addEventsToView makes sure the segments are created.
|
||||||
|
this._vuecal.addEventsToView([event])
|
||||||
|
|
||||||
|
this._vuecal.emitWithEvent('event-create', event)
|
||||||
|
this._vuecal.$emit('event-change', { event: this._vuecal.cleanupEvent(event), originalEvent: null })
|
||||||
|
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an event segment (= day) to a multiple-day event.
|
||||||
|
*
|
||||||
|
* @param {Object} e the multiple-day event to add segment in.
|
||||||
|
*/
|
||||||
|
addEventSegment (e) {
|
||||||
|
// If event was previously single-day, event.segments = null,
|
||||||
|
// so first create the first segment (first day).
|
||||||
|
if (!e.segments) {
|
||||||
|
e.segments = {}
|
||||||
|
e.segments[ud.formatDateLite(e.start)] = {
|
||||||
|
start: e.start,
|
||||||
|
startTimeMinutes: e.startTimeMinutes,
|
||||||
|
endTimeMinutes: minutesInADay,
|
||||||
|
isFirstDay: true,
|
||||||
|
isLastDay: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify the last segment - which will not stay the last one after this function.
|
||||||
|
const previousSegment = e.segments[ud.formatDateLite(e.end)]
|
||||||
|
// previousSegment might not exist when dragging too fast, prevent errors.
|
||||||
|
if (previousSegment) {
|
||||||
|
previousSegment.isLastDay = false
|
||||||
|
previousSegment.endTimeMinutes = minutesInADay
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// @todo: when moving fast might lose the previousSegment.
|
||||||
|
// Trying to update it would then result in an error, but do nothing would create a visual bug.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the new last segment.
|
||||||
|
const start = ud.addDays(e.end, 1)
|
||||||
|
const formattedDate = ud.formatDateLite(start)
|
||||||
|
start.setHours(0, 0, 0, 0)
|
||||||
|
e.segments[formattedDate] = {
|
||||||
|
start,
|
||||||
|
startTimeMinutes: 0,
|
||||||
|
endTimeMinutes: e.endTimeMinutes,
|
||||||
|
isFirstDay: false,
|
||||||
|
isLastDay: true
|
||||||
|
}
|
||||||
|
|
||||||
|
e.end = ud.addMinutes(start, e.endTimeMinutes)
|
||||||
|
e.daysCount = Object.keys(e.segments).length
|
||||||
|
|
||||||
|
return formattedDate
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an event segment (= day) from a multiple-day event.
|
||||||
|
*
|
||||||
|
* @param {Object} e the multiple-day event to remove segments from.
|
||||||
|
*/
|
||||||
|
removeEventSegment (e) {
|
||||||
|
let segmentsCount = Object.keys(e.segments).length
|
||||||
|
if (segmentsCount <= 1) return ud.formatDateLite(e.end)
|
||||||
|
|
||||||
|
// Remove the last segment.
|
||||||
|
delete e.segments[ud.formatDateLite(e.end)]
|
||||||
|
segmentsCount--
|
||||||
|
|
||||||
|
const end = ud.subtractDays(e.end, 1)
|
||||||
|
const formattedDate = ud.formatDateLite(end)
|
||||||
|
const previousSegment = e.segments[formattedDate]
|
||||||
|
|
||||||
|
// If no more segments, reset the segments attribute to null.
|
||||||
|
if (!segmentsCount) e.segments = null
|
||||||
|
|
||||||
|
// previousSegment might not exist when dragging too fast, prevent errors.
|
||||||
|
else if (previousSegment) {
|
||||||
|
// Modify the new last segment.
|
||||||
|
previousSegment.isLastDay = true
|
||||||
|
previousSegment.endTimeMinutes = e.endTimeMinutes
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// @todo: when moving fast might lose the previousSegment.
|
||||||
|
// Trying to update it would then result in an error, but do nothing would create a visual bug.
|
||||||
|
}
|
||||||
|
|
||||||
|
e.daysCount = segmentsCount || 1
|
||||||
|
e.end = end
|
||||||
|
|
||||||
|
return formattedDate
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create 1 segment per day of the given event, but only within the current view.
|
||||||
|
* (It won't create segments for all the days in view that are not in the event!)
|
||||||
|
*
|
||||||
|
* An event segment is a piece of event per day that contains more day-specific data.
|
||||||
|
*
|
||||||
|
* @param {Object} e the multiple-day event to create segments for.
|
||||||
|
* @param {Date} viewStartDate the starting date of the view.
|
||||||
|
* @param {Date} viewEndDate the ending date of the view.
|
||||||
|
*/
|
||||||
|
createEventSegments (e, viewStartDate, viewEndDate) {
|
||||||
|
const viewStartTimestamp = viewStartDate.getTime()
|
||||||
|
const viewEndTimestamp = viewEndDate.getTime()
|
||||||
|
let eventStart = e.start.getTime()
|
||||||
|
let eventEnd = e.end.getTime()
|
||||||
|
let repeatedEventStartFound = false
|
||||||
|
let timestamp, end, eventStartAtMidnight
|
||||||
|
|
||||||
|
// @todo: I don't think we still need that:
|
||||||
|
// Removing 1 sec when ending at 00:00, so that we don't create a segment for nothing on last day.
|
||||||
|
if (!e.end.getHours() && !e.end.getMinutes()) eventEnd -= 1000
|
||||||
|
|
||||||
|
e.segments = {}
|
||||||
|
|
||||||
|
// The goal is to create 1 segment per day in the event, but only within the current view.
|
||||||
|
if (!e.repeat) { // Simple case first.
|
||||||
|
timestamp = Math.max(viewStartTimestamp, eventStart)
|
||||||
|
end = Math.min(viewEndTimestamp, eventEnd)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Start at the beginning of the range, and end at soonest between `repeat.until` if any or range end.
|
||||||
|
// This range will most likely be too large (e.g. whole week) and we need to narrow it
|
||||||
|
// down in the while loop below.
|
||||||
|
// We must not create unused segments, it would break the render or result in weird behaviors.
|
||||||
|
timestamp = viewStartTimestamp
|
||||||
|
end = Math.min(
|
||||||
|
viewEndTimestamp,
|
||||||
|
e.repeat.until ? ud.stringToDate(e.repeat.until).getTime() : viewEndTimestamp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
while (timestamp <= end) {
|
||||||
|
let createSegment = false
|
||||||
|
// Be careful not to simply add 24 hours!
|
||||||
|
// In case of DLS, that would cause the event to never end and browser to hang.
|
||||||
|
// So use `addDays(1)` instead.
|
||||||
|
const nextMidnight = ud.addDays(new Date(timestamp), 1).setHours(0, 0, 0, 0)
|
||||||
|
let isFirstDay, isLastDay, start, formattedDate
|
||||||
|
|
||||||
|
if (e.repeat) {
|
||||||
|
const tmpDate = new Date(timestamp)
|
||||||
|
const tmpDateFormatted = ud.formatDateLite(tmpDate)
|
||||||
|
// If the current day in loop is a known date of the repeated event (in `e.occurrences`),
|
||||||
|
// then we found the first day of the event repetition, now update the `eventStart` and
|
||||||
|
// the end of the loop at current day + event days count.
|
||||||
|
if (repeatedEventStartFound || (e.occurrences && e.occurrences[tmpDateFormatted])) {
|
||||||
|
if (!repeatedEventStartFound) {
|
||||||
|
eventStart = e.occurrences[tmpDateFormatted].start
|
||||||
|
eventStartAtMidnight = new Date(eventStart).setHours(0, 0, 0, 0)
|
||||||
|
eventEnd = e.occurrences[tmpDateFormatted].end
|
||||||
|
}
|
||||||
|
repeatedEventStartFound = true
|
||||||
|
createSegment = true
|
||||||
|
}
|
||||||
|
|
||||||
|
isFirstDay = timestamp === eventStartAtMidnight
|
||||||
|
isLastDay = tmpDateFormatted === ud.formatDateLite(new Date(eventEnd))
|
||||||
|
start = new Date(isFirstDay ? eventStart : timestamp)
|
||||||
|
formattedDate = ud.formatDateLite(start)
|
||||||
|
// We want to find any potential other repetition of event in same range.
|
||||||
|
if (isLastDay) repeatedEventStartFound = false
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
createSegment = true
|
||||||
|
isFirstDay = timestamp === eventStart
|
||||||
|
isLastDay = end === eventEnd && nextMidnight > end
|
||||||
|
start = isFirstDay ? e.start : new Date(timestamp)
|
||||||
|
formattedDate = ud.formatDateLite(isFirstDay ? e.start : start)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createSegment) {
|
||||||
|
e.segments[formattedDate] = {
|
||||||
|
start,
|
||||||
|
startTimeMinutes: isFirstDay ? e.startTimeMinutes : 0,
|
||||||
|
endTimeMinutes: isLastDay ? e.endTimeMinutes : minutesInADay,
|
||||||
|
isFirstDay,
|
||||||
|
isLastDay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamp = nextMidnight
|
||||||
|
}
|
||||||
|
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an event.
|
||||||
|
*
|
||||||
|
* @param {Object} event the calendar event to delete.
|
||||||
|
*/
|
||||||
|
deleteAnEvent (event) {
|
||||||
|
this._vuecal.emitWithEvent('event-delete', event)
|
||||||
|
|
||||||
|
// Delete the event globally.
|
||||||
|
this._vuecal.mutableEvents = this._vuecal.mutableEvents.filter(e => e._eid !== event._eid)
|
||||||
|
// Delete the event from the current view.
|
||||||
|
// checkCellOverlappingEvents() will be re-run automatically from the cell computed events.
|
||||||
|
this._vuecal.view.events = this._vuecal.view.events.filter(e => e._eid !== event._eid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EVENT OVERLAPS.
|
||||||
|
// ===================================================================
|
||||||
|
// Will recalculate all the overlaps of the current cell OR split.
|
||||||
|
// cellEvents will contain only the current split events if in a split.
|
||||||
|
checkCellOverlappingEvents (cellEvents, options) {
|
||||||
|
_comparisonArray = cellEvents.slice(0)
|
||||||
|
_cellOverlaps = {}
|
||||||
|
|
||||||
|
// Can't filter background events before calling this function otherwise
|
||||||
|
// when an event is changed to background it would not update its previous overlaps.
|
||||||
|
cellEvents.forEach(e => {
|
||||||
|
// For performance, never compare the current event in the next loops.
|
||||||
|
// The array is smaller and smaller as we loop.
|
||||||
|
_comparisonArray.shift()
|
||||||
|
|
||||||
|
if (!_cellOverlaps[e._eid]) _cellOverlaps[e._eid] = { overlaps: [], start: e.start, position: 0 }
|
||||||
|
_cellOverlaps[e._eid].position = 0
|
||||||
|
|
||||||
|
_comparisonArray.forEach(e2 => {
|
||||||
|
if (!_cellOverlaps[e2._eid]) _cellOverlaps[e2._eid] = { overlaps: [], start: e2.start, position: 0 }
|
||||||
|
|
||||||
|
const eventIsInRange = this.eventInRange(e2, e.start, e.end)
|
||||||
|
const eventsInSameTimeStep = options.overlapsPerTimeStep ? ud.datesInSameTimeStep(e.start, e2.start, options.timeStep) : 1
|
||||||
|
// Add to the overlaps array if overlapping.
|
||||||
|
if (!e.background && !e.allDay && !e2.background && !e2.allDay && eventIsInRange && eventsInSameTimeStep) {
|
||||||
|
_cellOverlaps[e._eid].overlaps.push(e2._eid)
|
||||||
|
_cellOverlaps[e._eid].overlaps = [...new Set(_cellOverlaps[e._eid].overlaps)] // Dedupe, most performant way.
|
||||||
|
|
||||||
|
_cellOverlaps[e2._eid].overlaps.push(e._eid)
|
||||||
|
_cellOverlaps[e2._eid].overlaps = [...new Set(_cellOverlaps[e2._eid].overlaps)] // Dedupe, most performant way.
|
||||||
|
_cellOverlaps[e2._eid].position++
|
||||||
|
}
|
||||||
|
// Remove from the overlaps array if not overlapping or if 1 of the 2 events is background or all-day long.
|
||||||
|
else {
|
||||||
|
let pos1, pos2
|
||||||
|
if ((pos1 = (_cellOverlaps[e._eid] || { overlaps: [] }).overlaps.indexOf(e2._eid)) > -1) _cellOverlaps[e._eid].overlaps.splice(pos1, 1)
|
||||||
|
if ((pos2 = (_cellOverlaps[e2._eid] || { overlaps: [] }).overlaps.indexOf(e._eid)) > -1) _cellOverlaps[e2._eid].overlaps.splice(pos2, 1)
|
||||||
|
_cellOverlaps[e2._eid].position--
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Overlaps streak is the longest horizontal set of simultaneous events.
|
||||||
|
// This is determining the width of events in a streak.
|
||||||
|
// e.g. 3 overlapping events in a cell:
|
||||||
|
// ___ ___
|
||||||
|
// | 1 | |_2_| 1 overlaps 2 & 3; 2 & 3 don't overlap;
|
||||||
|
// | | ___ => streak = 2; each width = 50% not 33%.
|
||||||
|
// |___| |_3_|
|
||||||
|
let longestStreak = 0
|
||||||
|
for (const id in _cellOverlaps) {
|
||||||
|
const item = _cellOverlaps[id]
|
||||||
|
|
||||||
|
// Calculate the position of each event in current streak (determines the CSS left property).
|
||||||
|
const overlapsRow = item.overlaps.map(id2 => ({ id: id2, start: _cellOverlaps[id2].start }))
|
||||||
|
overlapsRow.push({ id, start: item.start })
|
||||||
|
overlapsRow.sort((a, b) => a.start < b.start ? -1 : (a.start > b.start ? 1 : (a.id > b.id ? -1 : 1)))
|
||||||
|
item.position = overlapsRow.findIndex(e => e.id === id)
|
||||||
|
|
||||||
|
longestStreak = Math.max(this.getOverlapsStreak(item, _cellOverlaps), longestStreak)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [_cellOverlaps, longestStreak]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overlaps streak is the longest horizontal set of simultaneous events.
|
||||||
|
* This is determining the width of each events in this streak.
|
||||||
|
* E.g. 3 overlapping events [1, 2, 3]; 1 overlaps 2 & 3; 2 & 3 don't overlap;
|
||||||
|
* => streak = 2; each width = 50% not 33%.
|
||||||
|
*
|
||||||
|
* @param {Object} event The current event we are checking among all the events of the current cell.
|
||||||
|
* @param {Object} cellOverlaps An indexed array of all the events overlaps for the current cell.
|
||||||
|
* @return {Number} The number of simultaneous event for this event.
|
||||||
|
*/
|
||||||
|
getOverlapsStreak (event, cellOverlaps = {}) {
|
||||||
|
let streak = event.overlaps.length + 1
|
||||||
|
let removeFromStreak = []
|
||||||
|
event.overlaps.forEach(id => {
|
||||||
|
if (!removeFromStreak.includes(id)) {
|
||||||
|
const overlapsWithoutSelf = event.overlaps.filter(id2 => id2 !== id)
|
||||||
|
overlapsWithoutSelf.forEach(id3 => {
|
||||||
|
if (!cellOverlaps[id3].overlaps.includes(id)) removeFromStreak.push(id3)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
removeFromStreak = [...new Set(removeFromStreak)] // Dedupe, most performant way.
|
||||||
|
streak -= removeFromStreak.length
|
||||||
|
return streak
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells whether an event is in a given date range, even partially.
|
||||||
|
*
|
||||||
|
* @param {Object} event The event to test.
|
||||||
|
* @param {Date} start The start of range date object.
|
||||||
|
* @param {Date} end The end of range date object.
|
||||||
|
* @return {Boolean} true if in range, even partially.
|
||||||
|
*/
|
||||||
|
eventInRange (event, start, end) {
|
||||||
|
// Check if all-day or timeless event (if date but no time there won't be a `:` in event.start).
|
||||||
|
if (event.allDay || !this._vuecal.time) {
|
||||||
|
// Get the date and discard the time if any, then check it's within the date range.
|
||||||
|
const startTimestamp = new Date(event.start).setHours(0, 0, 0, 0)
|
||||||
|
const endTimestamp = new Date(event.end).setHours(23, 59, 0, 0)
|
||||||
|
return (endTimestamp >= new Date(start).setHours(0, 0, 0, 0) &&
|
||||||
|
startTimestamp <= new Date(end).setHours(0, 0, 0, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTimestamp = event.start.getTime()
|
||||||
|
const endTimestamp = event.end.getTime()
|
||||||
|
return startTimestamp < end.getTime() && endTimestamp > start.getTime()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
<template lang="pug">
|
||||||
|
//- weekdays-headings are on week view only.
|
||||||
|
.vuecal__flex.vuecal__weekdays-headings
|
||||||
|
template(v-for="(heading, i) in headings" :key="i")
|
||||||
|
.vuecal__flex.vuecal__heading(
|
||||||
|
v-if="!heading.hide"
|
||||||
|
:class="{ today: heading.today, clickable: cellHeadingsClickable }"
|
||||||
|
:style="weekdayCellStyles"
|
||||||
|
@click="view.id === 'week' && selectCell(heading.date, $event)"
|
||||||
|
@dblclick="view.id === 'week' && vuecal.dblclickToNavigate && switchToNarrowerView()")
|
||||||
|
transition(:name="`slide-fade--${transitionDirection}`" :appear="vuecal.transitions")
|
||||||
|
.vuecal__flex(column :key="vuecal.transitions ? `${i}-${heading.dayOfMonth}` : false")
|
||||||
|
.vuecal__flex.weekday-label(grow)
|
||||||
|
slot(name="weekday-heading" :heading="cleanupHeading(heading)" :view="view")
|
||||||
|
//- For small/xsmall option. 3 media queries also truncate weekdays.
|
||||||
|
span.full {{ heading.full }}
|
||||||
|
span.small {{ heading.small }}
|
||||||
|
span.xsmall {{ heading.xsmall }}
|
||||||
|
span(v-if="heading.dayOfMonth") {{ heading.dayOfMonth }}
|
||||||
|
.vuecal__flex.vuecal__split-days-headers(
|
||||||
|
v-if="vuecal.hasSplits && vuecal.stickySplitLabels"
|
||||||
|
grow)
|
||||||
|
.day-split-header(v-for="(split, i) in vuecal.daySplits" :key="i" :class="split.class || false")
|
||||||
|
slot(name="split-label" :split="split" :view="view") {{ split.label }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
inject: ['vuecal', 'utils', 'view'],
|
||||||
|
props: {
|
||||||
|
transitionDirection: { type: String, default: 'right' },
|
||||||
|
weekDays: { type: Array, default: () => [] },
|
||||||
|
switchToNarrowerView: { type: Function, default: () => {} }
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
selectCell (date, DOMEvent) {
|
||||||
|
if (date.getTime() !== this.view.selectedDate.getTime()) {
|
||||||
|
this.view.selectedDate = date
|
||||||
|
}
|
||||||
|
this.utils.cell.selectCell(false, date, DOMEvent)
|
||||||
|
},
|
||||||
|
cleanupHeading: heading => ({
|
||||||
|
label: heading.full,
|
||||||
|
date: heading.date,
|
||||||
|
...(heading.today ? { today: heading.today } : {})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
headings () {
|
||||||
|
if (!['month', 'week'].includes(this.view.id)) return []
|
||||||
|
|
||||||
|
let todayFound = false
|
||||||
|
const headings = this.weekDays.map((cell, i) => {
|
||||||
|
const date = this.utils.date.addDays(this.view.startDate, this.vuecal.startWeekOnSunday ? i - 1 : i)
|
||||||
|
|
||||||
|
return {
|
||||||
|
hide: cell.hide,
|
||||||
|
full: cell.label,
|
||||||
|
// If defined in i18n file, weekDaysShort overrides default truncation of
|
||||||
|
// week days when does not fit on screen or with small/xsmall options.
|
||||||
|
small: cell.short || cell.label.substr(0, 3),
|
||||||
|
xsmall: cell.short || cell.label.substr(0, 1),
|
||||||
|
|
||||||
|
// Only for week view.
|
||||||
|
...(this.view.id === 'week' ? {
|
||||||
|
dayOfMonth: date.getDate(),
|
||||||
|
date,
|
||||||
|
today: !todayFound && this.utils.date.isToday(date) && !todayFound++
|
||||||
|
} : {})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return headings
|
||||||
|
},
|
||||||
|
cellWidth () {
|
||||||
|
return 100 / (7 - this.weekDays.reduce((total, day) => total + day.hide, 0))
|
||||||
|
},
|
||||||
|
weekdayCellStyles () {
|
||||||
|
return {
|
||||||
|
...(this.vuecal.hideWeekdays.length ? { width: `${this.cellWidth}%` } : {})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cellHeadingsClickable () {
|
||||||
|
return this.view.id === 'week' && (this.vuecal.clickToNavigate || this.vuecal.dblclickToNavigate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
$time-column-width: 3em;
|
||||||
|
$time-column-width-12: 4em; // twelve-hour clock shows am/pm.
|
||||||
|
$week-numbers-width: 3em;
|
||||||
|
$weekdays-headings-height: 2.8em;
|
||||||
|
$weekdays-headings-height-with-splits: 3.4em;
|
||||||
|
|
||||||
|
.vuecal {
|
||||||
|
&__weekdays-headings {
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
|
||||||
|
.vuecal--week-numbers & {padding-left: $week-numbers-width;}
|
||||||
|
.vuecal--view-with-time & {padding-left: $time-column-width;}
|
||||||
|
|
||||||
|
.vuecal--view-with-time.vuecal--twelve-hour & {
|
||||||
|
font-size: 0.9em;
|
||||||
|
padding-left: $time-column-width-12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuecal--overflow-x.vuecal--view-with-time & {padding-left: 0;}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__heading {
|
||||||
|
width: 100%;
|
||||||
|
height: $weekdays-headings-height;
|
||||||
|
font-weight: 400;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
> .vuecal__flex {width: 100%;height: 100%;align-items: initial !important;} // For Vue transition.
|
||||||
|
|
||||||
|
.vuecal--sticky-split-labels & {height: $weekdays-headings-height-with-splits;}
|
||||||
|
|
||||||
|
.vuecal--month-view &, .vuecal--week-view &, .vuecal--day-view & {width: 14.2857%;}
|
||||||
|
.vuecal--hide-weekends.vuecal--month-view &,
|
||||||
|
.vuecal--hide-weekends.vuecal--week-view &,
|
||||||
|
.vuecal--hide-weekends.vuecal--day-view & {width: 20%;}
|
||||||
|
.vuecal--years-view & {width: 20%;}
|
||||||
|
.vuecal--year-view & {width: 33.33%;}
|
||||||
|
|
||||||
|
.weekday-label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuecal--small & .small, .vuecal--xsmall & .xsmall {display: block;}
|
||||||
|
.small, .xsmall,
|
||||||
|
.vuecal--small & .full, .vuecal--small & .xsmall,
|
||||||
|
.vuecal--xsmall & .full, .vuecal--xsmall & .small {display: none;}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuecal__split-days-headers {align-items: center;}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Media queries.
|
||||||
|
//==================================//
|
||||||
|
@media screen and (max-width: 550px) {
|
||||||
|
.vuecal__heading {
|
||||||
|
line-height: 1.2;
|
||||||
|
|
||||||
|
.small,
|
||||||
|
.vuecal--small & .small,
|
||||||
|
.vuecal--xsmall & .xsmall {display: block;}
|
||||||
|
.full, .xsmall,
|
||||||
|
.vuecal--small & .full, .vuecal--small & .xsmall,
|
||||||
|
.vuecal--xsmall & .full, .vuecal--xsmall & .small {display: none;}
|
||||||
|
|
||||||
|
.vuecal--overflow-x & .full,
|
||||||
|
.vuecal--small.vuecal--overflow-x & .small,
|
||||||
|
.vuecal--xsmall.vuecal--overflow-x & .xsmall {display: block;}
|
||||||
|
.vuecal--overflow-x & .small, .vuecal--overflow-x & .xsmall,
|
||||||
|
.vuecal--small.vuecal--overflow-x & .full, .vuecal--small.vuecal--overflow-x & .xsmall,
|
||||||
|
.vuecal--xsmall.vuecal--overflow-x & .full, .vuecal--xsmall.vuecal--overflow-x & .small {display: none;}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 450px) {
|
||||||
|
.vuecal__heading {
|
||||||
|
.xsmall,
|
||||||
|
.vuecal--small & .xsmall,
|
||||||
|
.vuecal--xsmall & .xsmall {display: block;}
|
||||||
|
.full, .small,
|
||||||
|
.vuecal--small & .full, .vuecal--small & .small,
|
||||||
|
.vuecal--xsmall & .full, .vuecal--xsmall & .small {display: none;}
|
||||||
|
|
||||||
|
.vuecal--small.vuecal--overflow-x & .small,
|
||||||
|
.vuecal--xsmall.vuecal--overflow-x & .xsmall {display: block;}
|
||||||
|
.vuecal--small.vuecal--overflow-x & .full, .vuecal--small.vuecal--overflow-x & .xsmall,
|
||||||
|
.vuecal--xsmall.vuecal--overflow-x & .full, .vuecal--xsmall.vuecal--overflow-x & .small {display: none;}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in new issue
Block a user