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