@openhab-ui/setup-and-maintenance
Version:
Configuration and maintenance interface for openHAB
1,695 lines (1,539 loc) • 143 kB
JavaScript
import { store } from '../app.js';
const now = new Date();
// Cache today's date for better isDateToday() performances. Formatted without leading 0.
const todayFormatted = `${now.getFullYear()}-${now.getMonth()}-${now.getDate()}`;
// eslint-disable-next-line
Date.prototype.addDays = function (days) {
let date = new Date(this.valueOf());
date.setDate(date.getDate() + days);
return date
};
// eslint-disable-next-line
Date.prototype.subtractDays = function (days) {
let date = new Date(this.valueOf());
date.setDate(date.getDate() - days);
return date
};
// eslint-disable-next-line
Date.prototype.getWeek = function () {
let d = new Date(Date.UTC(this.getFullYear(), this.getMonth(), this.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)
};
const isDateToday = date => {
return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}` === todayFormatted
};
/* export const getDateOfWeek = (w, y) => {
let d = (1 + (w - 1) * 7) // 1st of January + 7 days for each week.
return new Date(y, 0, d)
} */
// Returns today if it's FirstDayOfWeek (Monday or Sunday) or previous FirstDayOfWeek otherwise.
const getPreviousFirstDayOfWeek = (date = null, weekStartsOnSunday) => {
let prevFirstDayOfWeek = (date && new Date(date.valueOf())) || new Date();
let dayModifier = weekStartsOnSunday ? 7 : 6;
prevFirstDayOfWeek.setDate(prevFirstDayOfWeek.getDate() - (prevFirstDayOfWeek.getDay() + dayModifier) % 7);
return prevFirstDayOfWeek
};
/**
* @param {int} The month number, 0 based.
* @param {int} The year, not zero based, required to account for leap years.
* @return {Date[]} List with date objects for each day of the month.
*/
const getDaysInMonth = (month, year) => {
let date = new Date(year, month, 1);
let days = [];
while (date.getMonth() === month) {
days.push(new Date(date));
date.setDate(date.getDate() + 1);
}
return days
};
const 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'
}
};
const formatTime = (time, format = 'HH:mm') => {
const H = Math.floor(time / 60);
const h = H % 12 ? H % 12 : 12;
const am = H < 12 ? 'am' : 'pm';
const m = Math.floor(time % 60);
const timeObj = {
H,
h,
HH: (H < 10 ? '0' : '') + H,
hh: (h < 10 ? '0' : '') + h,
am,
AM: am.toUpperCase(),
m,
mm: (m < 10 ? '0' : '') + m
};
return format.replace(/(\{[a-zA-Z]+\}|[a-zA-Z]+)/g, (m, contents) => timeObj[contents.replace(/\{|\}/g, '')])
};
const formatDate = (date, format = 'yyyy-mm-dd', localizedTexts) => {
const d = date.getDate();
const m = date.getMonth() + 1;
const dateObj = {
D: date.getDay(), // 0 to 6.
DD: localizedTexts.weekDays[(date.getDay() - 1 + 7) % 7][0], // M to S.
DDD: localizedTexts.weekDays[(date.getDay() - 1 + 7) % 7].substr(0, 3), // Mon to Sun.
DDDD: localizedTexts.weekDays[(date.getDay() - 1 + 7) % 7], // Monday to Sunday.
d, // 1 to 31.
dd: (d < 10 ? '0' : '') + d, // 01 to 31.
S: nth(d), // st, nd, rd, th.
m, // 1 to 12.
mm: (m < 10 ? '0' : '') + m, // 01 to 12.
mmm: localizedTexts.months[m - 1].substr(0, 3), // Jan to Dec.
mmmm: localizedTexts.months[m - 1], // January to December.
yyyy: date.getFullYear(), // 2018.
yy: date.getFullYear().toString().substr(2, 4) // 18.
};
return format.replace(/(\{[a-zA-Z]+\}|[a-zA-Z]+)/g, (m, contents) => {
const result = dateObj[contents.replace(/\{|\}/g, '')];
return result !== undefined ? result : contents
})
};
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
var script = {
props: {
cssClass: {
type: String,
default: ""
},
date: {
type: Date,
required: true
},
formattedDate: {
type: String,
default: ""
},
cellid: {
type: Number,
required: true
},
content: {
type: [String, Number],
default: ""
},
splits: {
type: Array,
default: () => []
},
today: {
type: Boolean,
default: false
},
allDayEvents: {
type: Boolean,
default: false
}
},
data: () => ({
splitEvents: {}
}),
methods: {
updateEventPosition(event) {
const src = (event.multipleDays.daysCount && event.multipleDays) || event;
const { startTimeMinutes, endTimeMinutes } = src;
let minutesFromTop = startTimeMinutes - this.timeFrom;
const top = Math.round(
(minutesFromTop * this.timeCellHeight) / this.timeStep
);
minutesFromTop = Math.min(endTimeMinutes, this.timeTo) - this.timeFrom;
const bottom = Math.round(
(minutesFromTop * this.timeCellHeight) / this.timeStep
);
event.top = Math.max(top, 0);
event.height = bottom - event.top;
},
eventStyles(event) {
if (!event.startTime || this.view === "month" || this.allDayEvents)
return {};
const resizeAnEvent = this.domEvents.resizeAnEvent;
return {
top: `${event.top}px`,
height: `${
resizeAnEvent.newHeight && resizeAnEvent.eventId === event.id
? resizeAnEvent.newHeight
: event.height
}px`
};
},
eventClasses(event) {
const overlapping = Object.keys(event.overlapping).length;
const overlapped = Object.keys(event.overlapped).length;
let simultaneous = Object.keys(event.simultaneous).length + 1;
let forceLeft = false;
let deletable =
this.domEvents.clickHoldAnEvent.eventId &&
this.domEvents.dragAnEvent.eventId !== event.id &&
(this.domEvents.clickHoldAnEvent.eventId === event.id ||
event.linked.find(
e => e.id === this.domEvents.clickHoldAnEvent.eventId
));
if (simultaneous >= 3) {
let split3 = simultaneous - 1;
Object.keys(event.simultaneous).forEach(eventId => {
if (
split3 &&
Object.keys(this.events.find(e => e.id === eventId).simultaneous)
.length +
1 <
3
) {
split3--;
}
});
if (!split3) simultaneous = 2;
} else if (simultaneous === 2) {
const otherEvent = this.events.find(
e => e.id === Object.keys(event.simultaneous)[0]
);
if (
otherEvent &&
Object.keys(otherEvent.overlapping).length &&
Object.keys(otherEvent.overlapped).length
) {
forceLeft = true;
}
}
return {
...event.classes,
"vuecal__event--dragging":
this.domEvents.dragAnEvent.eventId &&
this.domEvents.dragAnEvent.eventId !== event.id,
"vuecal__event--transient": event.transient,
"vuecal__event--focus":
this.domEvents.focusAnEvent.eventId === event.id,
"vuecal__event--deletable": deletable,
"vuecal__event--overlapped": overlapped,
"vuecal__event--overlapping": overlapping,
"vuecal__event--split2": simultaneous === 2,
"vuecal__event--split3": simultaneous >= 3,
"vuecal__event--split-middle":
overlapped && overlapping && simultaneous >= 3,
"vuecal__event--split-left": forceLeft,
"vuecal__event--all-day": event.allDay,
"vuecal__event--multiple-days": Object.keys(event.multipleDays).length
};
},
// Will recalculate all the overlappings of the current cell or only of the given split if provided.
checkCellOverlappingEvents(split = 0) {
if (this.events) {
const foregroundEventsList = this.events.filter(
item => !item.background && (split ? item.split === split : 1)
);
if (foregroundEventsList.length) {
// Do the mapping outside of the next loop if not splitted cell.
// If splitted need the whole event object to compare splits.
const foregroundEventsIdList = foregroundEventsList.map(
item => item.id
);
let comparisonArray = {};
this.events.forEach(event => {
if (!event.background) {
let comparisonArrayKeys = Object.keys(comparisonArray);
// Unique comparison of events.
comparisonArray[event.id] = this.splits.length
? foregroundEventsList
.filter(
item =>
item.id !== event.id &&
comparisonArrayKeys.indexOf(item.id) === -1 &&
item.split === event.split
)
.map(item => item.id)
: foregroundEventsIdList.filter(
id =>
id !== event.id && comparisonArrayKeys.indexOf(id) === -1
);
if (comparisonArray[event.id].length)
this.checkOverlappingEvents(event, comparisonArray[event.id]);
}
});
}
}
},
checkOverlappingEvents(event, comparisonArray) {
const src = (event.multipleDays.daysCount && event.multipleDays) || event;
const {
startTimeMinutes: startTimeMinE1,
endTimeMinutes: endTimeMinE1
} = src;
comparisonArray.forEach((event2id, i) => {
let event2 = this.events.find(item => item.id === event2id);
const src2 =
(event2.multipleDays.daysCount && event2.multipleDays) || event2;
const {
startTimeMinutes: startTimeMinE2,
endTimeMinutes: endTimeMinE2
} = src2;
const event1startsFirst = startTimeMinE1 < startTimeMinE2;
const event1overlapsEvent2 =
!event1startsFirst && endTimeMinE2 > startTimeMinE1;
const event2overlapsEvent1 =
event1startsFirst && endTimeMinE1 > startTimeMinE2;
if (event1overlapsEvent2) {
this.$set(event.overlapping, event2.id, true);
this.$set(event2.overlapped, event.id, true);
} else {
delete event.overlapping[event2.id];
delete event2.overlapped[event.id];
}
if (event2overlapsEvent1) {
this.$set(event2.overlapping, event.id, true);
this.$set(event.overlapped, event2.id, true);
} else {
delete event2.overlapping[event.id];
delete event.overlapped[event2.id];
}
// If up to 3 events start at the same time.
if (
startTimeMinE1 === startTimeMinE2 ||
(event1overlapsEvent2 || event2overlapsEvent1)
) {
this.$set(event.simultaneous, event2.id, true);
this.$set(event2.simultaneous, event.id, true);
} else {
delete event.simultaneous[event2.id];
delete event2.simultaneous[event.id];
}
if (this.splits.length) {
this.splitEvents[event.split] = this.events.filter(
e => e.split === event.split
);
}
});
},
onResizeEvent() {
let { eventId, newHeight } = this.$parent.domEvents.resizeAnEvent;
let event = this.events.filter(e => e.id === eventId)[0];
if (event) {
event.height = Math.max(newHeight, 10);
this.updateEndTimeOnResize(event);
if (!event.background)
this.checkCellOverlappingEvents(event.split || 0);
}
},
updateEndTimeOnResize(event) {
const bottom = event.top + event.height;
const endTime =
((bottom / this.timeCellHeight) * this.timeStep + this.timeFrom) / 60;
const hours = parseInt(endTime);
const minutes = parseInt((endTime - hours) * 60);
event.endTimeMinutes = endTime * 60;
event.endTime = `${hours}:${(minutes < 10 ? "0" : "") + minutes}`;
event.end = event.end.split(" ")[0] + ` ${event.endTime}`;
if (event.multipleDays.daysCount) {
event.multipleDays.endTimeMinutes = event.endTimeMinutes;
event.multipleDays.endTime = event.endTime;
event.multipleDays.end = event.end;
event.linked.forEach(e => {
let dayToModify = this.$parent.mutableEvents[e.date];
let eventToModify = dayToModify.find(e2 => e2.id === e.id);
eventToModify.endTimeMinutes = event.endTimeMinutes;
eventToModify.endTime = event.endTime;
eventToModify.end = event.end;
});
}
},
getPosition(e) {
const rect = e.target.getBoundingClientRect();
const { clientX, clientY } =
"ontouchstart" in window && e.touches ? e.touches[0] : e;
return { x: clientX - rect.left, y: clientY - rect.top };
},
// On an event.
onMouseDown(e, event, touch = false) {
// Prevent a double mouse down on touch devices.
if ("ontouchstart" in window && !touch) return false;
let {
clickHoldAnEvent,
resizeAnEvent,
dragAnEvent,
mousedown
} = this.domEvents;
this.domEvents.mousedown = true;
// If the delete button is already out and event is on focus then delete event.
if (
this.domEvents.focusAnEvent.eventId === event.id &&
clickHoldAnEvent.eventId === event.id
) {
return true;
}
// Focus the clicked event.
this.focusEvent(event);
clickHoldAnEvent.eventId = null; // Reinit click hold on each click.
// Don't show delete button if dragging or resizing event.
if (!resizeAnEvent.start && !dragAnEvent.eventId && this.editableEvents) {
clickHoldAnEvent.timeoutId = setTimeout(() => {
clickHoldAnEvent.eventId = event.id;
}, clickHoldAnEvent.timeout);
}
},
onDragStart(e, event) {
let { dragAnEvent, dragging } = this.domEvents;
this.$parent.cursorPosition = e.offsetY;
this.$parent.dragCurrentCell = this.cellid;
dragging = true;
e.dataTransfer.dropEffect = "move";
dragAnEvent = Object.assign(dragAnEvent, {
eventId: event.id,
eventStartDate: event.startDate
});
},
onDragEnd(e, event) {
e.preventDefault();
let elArray = document.elementsFromPoint(e.clientX, e.clientY);
for (let el of elArray) {
if (el.__vue__ && el.__vue__.dragEnd) {
const m =
(el.__vue__.cursorPosition * this.timeStep) / this.timeCellHeight;
el.__vue__.dragEnd(
Math.floor((m + this.timeFrom) / 60),
Math.floor(m + this.timeFrom) % 60
);
break;
}
}
this.domEvents.dragging = false;
this.$parent.cursorPosition = null;
},
onMouseEnter(e, event) {
//e.preventDefault();
this.$parent.emitWithEvent("event-mouse-enter", event);
},
onMouseLeave(e, event) {
//e.preventDefault();
this.$parent.emitWithEvent("event-mouse-leave", event);
},
onContextMenu(e, event) {
e.preventDefault();
return false;
},
onTouchStart(e, event) {
this.onMouseDown(e, event, true);
},
onClick(e, event) {
if (typeof this.$parent.onEventClick === "function")
return this.$parent.onEventClick(event, e);
},
onDblClick(e, event) {
if (typeof this.$parent.onEventDblclick === "function")
return this.$parent.onEventDblclick(event, e);
},
onDragHandleMouseDown(e, event) {
const start =
"ontouchstart" in window && e.touches
? e.touches[0].clientY
: e.clientY;
let { resizeAnEvent, mousedown } = this.domEvents;
this.domEvents.mousedown = true;
resizeAnEvent = Object.assign(resizeAnEvent, {
start,
originalHeight: event.height,
newHeight: event.height,
eventId: event.id,
eventStartDate: event.startDate
});
},
deleteEvent(event, touch = false) {
// Prevent a double mouse down on touch devices.
if ("ontouchstart" in window && !touch) return false;
this.$parent.emitWithEvent("event-delete", event);
// Filtering from $parent.mutableEvents since current cell might only contain all day events or vice-versa.
this.events = this.$parent.mutableEvents[this.formattedDate].filter(
e => e.id !== event.id
);
// If deleting a multiple-day event, delete all the events pieces (days).
if (event.multipleDays.daysCount) {
event.linked.forEach(e => {
let dayToModify = this.$parent.mutableEvents[e.date];
let eventToDelete = dayToModify.find(e2 => e2.id === e.id);
this.$parent.mutableEvents[e.date] = dayToModify.filter(
e2 => e2.id !== e.id
);
if (!e.background) {
// Remove this event from possible other overlapping events of the same cell.
Object.keys(eventToDelete.overlapped).forEach(
id =>
delete dayToModify.find(item => item.id === id).overlapping[
eventToDelete.id
]
);
Object.keys(eventToDelete.overlapping).forEach(
id =>
delete dayToModify.find(item => item.id === id).overlapped[
eventToDelete.id
]
);
Object.keys(eventToDelete.simultaneous).forEach(
id =>
delete dayToModify.find(item => item.id === id).simultaneous[
eventToDelete.id
]
);
}
});
}
if (!event.background) {
// Remove this event from possible other overlapping events of the same cell.
Object.keys(event.overlapped).forEach(
id =>
delete this.events.find(item => item.id === id).overlapping[
event.id
]
);
Object.keys(event.overlapping).forEach(
id =>
delete this.events.find(item => item.id === id).overlapped[event.id]
);
Object.keys(event.simultaneous).forEach(
id =>
delete this.events.find(item => item.id === id).simultaneous[
event.id
]
);
this.checkCellOverlappingEvents(event.split || 0);
}
if (this.splits.length)
this.splitEvents[event.split] = this.events.filter(
e => e.id !== event.id && e.split === event.split
);
},
touchDeleteEvent(event) {
this.deleteEvent(event, true);
},
focusEvent(event) {
this.$parent.emitWithEvent("event-focus", event);
this.domEvents.focusAnEvent.eventId = event.id;
}
},
computed: {
texts() {
return this.$parent.texts;
},
view() {
return this.$parent.view.id;
},
time() {
return this.$parent.time;
},
timeFormat() {
return (
this.$parent.timeFormat ||
(this.$parent["12Hour"] ? "h:mm{am}" : "HH:mm")
);
},
timeCellHeight() {
return parseInt(this.$parent.timeCellHeight);
},
timeFrom() {
return parseInt(this.$parent.timeFrom);
},
timeTo() {
return parseInt(this.$parent.timeTo);
},
timeStep() {
return parseInt(this.$parent.timeStep);
},
showAllDayEvents() {
return this.$parent.showAllDayEvents;
},
eventsOnMonthView() {
return this.$parent.eventsOnMonthView;
},
editableEvents() {
return this.$parent.editableEvents;
},
draggableEvents() {
return this.$parent.draggableEvents;
},
noEventOverlaps() {
this.$nextTick(() => this.checkCellOverlappingEvents());
return this.$parent.noEventOverlaps;
},
transitions() {
return this.$parent.transitions;
},
transitionDirection() {
return this.$parent.transitionDirection;
},
domEvents: {
get() {
if (this.$parent.domEvents.resizeAnEvent.eventId) this.onResizeEvent();
return this.$parent.domEvents;
},
set(object) {
this.$parent.domEvents = object;
}
},
cellStyles() {
return {
minWidth:
this.view === "week" && this.$parent.minCellWidth
? `${this.$parent.minCellWidth}px`
: null,
position: "relative"
};
},
events: {
get() {
const events = this.$parent.mutableEvents[this.formattedDate] || [];
// eslint-disable-next-line
this.splitEvents = [];
events.forEach(event => {
if (event.startTime && !(this.showAllDayEvents && this.allDayEvents))
this.updateEventPosition(event);
// Only for splits.
if (this.splits.length && event.split) {
// eslint-disable-next-line
if (!this.splitEvents[event.split])
this.$set(this.splitEvents, event.split, []);
// eslint-disable-next-line
this.splitEvents[event.split].push(event);
}
});
// NextTick prevents a cyclic redundancy.
this.$nextTick(this.checkCellOverlappingEvents);
return this.showAllDayEvents
? events.filter(e => !!e.allDay === this.allDayEvents)
: events;
},
set(events) {
this.$parent.mutableEvents[this.formattedDate] = events;
}
},
cellSplitEvents() {
let splitsEventIndexes = {};
this.events.forEach((e, i) => {
if (!splitsEventIndexes[e.split || 0])
splitsEventIndexes[e.split || 0] = {};
splitsEventIndexes[e.split || 0][e.id] = i;
});
return splitsEventIndexes;
},
timelineVisible() {
if (
!this.today ||
!this.time ||
this.allDayEvents ||
["week", "day"].indexOf(this.view) === -1
)
return;
const now = new Date();
let startTimeMinutes = now.getHours() * 60 + now.getMinutes();
return startTimeMinutes <= this.timeTo;
},
todaysTimePosition() {
// Make sure to skip the Maths if not relevant.
if (!this.today || !this.time) return;
const now = new Date();
let startTimeMinutes = now.getHours() * 60 + now.getMinutes();
let minutesFromTop = startTimeMinutes - this.timeFrom;
return Math.round((minutesFromTop * this.timeCellHeight) / this.timeStep);
}
}
};
function normalizeComponent(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier
/* server only */
, shadowMode, createInjector, createInjectorSSR, createInjectorShadow) {
if (typeof shadowMode !== 'boolean') {
createInjectorSSR = createInjector;
createInjector = shadowMode;
shadowMode = false;
} // Vue.extend constructor export interop.
var options = typeof script === 'function' ? script.options : script; // render functions
if (template && template.render) {
options.render = template.render;
options.staticRenderFns = template.staticRenderFns;
options._compiled = true; // functional template
if (isFunctionalTemplate) {
options.functional = true;
}
} // scopedId
if (scopeId) {
options._scopeId = scopeId;
}
var hook;
if (moduleIdentifier) {
// server build
hook = function hook(context) {
// 2.3 injection
context = context || // cached call
this.$vnode && this.$vnode.ssrContext || // stateful
this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext; // functional
// 2.2 with runInNewContext: true
if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {
context = __VUE_SSR_CONTEXT__;
} // inject component styles
if (style) {
style.call(this, createInjectorSSR(context));
} // register component module identifier for async chunk inference
if (context && context._registeredComponents) {
context._registeredComponents.add(moduleIdentifier);
}
}; // used by ssr in case component is cached and beforeCreate
// never gets called
options._ssrRegister = hook;
} else if (style) {
hook = shadowMode ? function () {
style.call(this, createInjectorShadow(this.$list.$options.shadowRoot));
} : function (context) {
style.call(this, createInjector(context));
};
}
if (hook) {
if (options.functional) {
// register for functional component in vue file
var originalRender = options.render;
options.render = function renderWithStyleInjection(h, context) {
hook.call(context);
return originalRender(h, context);
};
} else {
// inject component registration as beforeCreate hook
var existing = options.beforeCreate;
options.beforeCreate = existing ? [].concat(existing, hook) : [hook];
}
}
return script;
}
/* script */
const __vue_script__ = script;
/* template */
var __vue_render__ = function () {
var _vm = this;
var _h = _vm.$createElement;
var _c = _vm._self._c || _h;
return _c(
"transition-group",
{
staticClass: "vuecal__cell",
class: ((_obj = {
splitted: _vm.splits.length,
"vuecal__cell--has-events": _vm.events.length
}),
(_obj[_vm.cssClass] = true),
_obj),
style: _vm.cellStyles,
attrs: {
tag: "div",
name: "slide-fade--" + _vm.transitionDirection,
appear: _vm.transitions
}
},
[
_vm._l(_vm.splits.length || 1, function (i) {
return _c(
"div",
{
key: _vm.transitions ? _vm.view + "-" + _vm.content + "-" + i : i,
staticClass: "vuecal__flex vuecal__cell-content",
class:
_vm.splits.length &&
"vuecal__cell-split " + _vm.splits[i - 1].class,
attrs: { column: "column" }
},
[
_vm.splits.length
? _c("div", {
staticClass: "split-label",
domProps: { innerHTML: _vm._s(_vm.splits[i - 1].label) }
})
: _vm._e(),
_vm.content
? _c("div", {
staticClass: "vuecal__cell-date",
domProps: { innerHTML: _vm._s(_vm.content) }
})
: _vm._e(),
!_vm.events.length &&
(["week", "day"].indexOf(_vm.view) > -1 ||
(_vm.view === "month" && _vm.eventsOnMonthView))
? _c(
"div",
{ staticClass: "vuecal__no-event" },
[_vm._t("no-event", [_vm._v(_vm._s(_vm.texts.noEvent))])],
2
)
: _vm._e(),
_vm.events.length &&
(["week", "day"].indexOf(_vm.view) > -1 ||
(_vm.view === "month" && _vm.eventsOnMonthView))
? _c(
"div",
{ staticClass: "vuecal__cell-events" },
_vm._l(
_vm.splits.length ? _vm.splitEvents[i] : _vm.events,
function (event, j) {
return _c(
"div",
{
key: j,
staticClass: "vuecal__event",
class: _vm.eventClasses(event),
style: _vm.eventStyles(event),
attrs: {
draggable: _vm.draggableEvents && !event.fixed
},
on: {
mouseenter: function ($event) {
_vm.onMouseEnter($event, event);
},
mouseleave: function ($event) {
_vm.onMouseLeave($event, event);
},
mousedown: function ($event) {
_vm.onMouseDown($event, event);
},
dragstart: function ($event) {
_vm.onDragStart($event, event);
},
dragend: function ($event) {
_vm.onDragEnd($event, event);
},
contextmenu: function ($event) {
_vm.onContextMenu($event, event);
},
touchstart: function ($event) {
_vm.onTouchStart($event, event);
},
click: function ($event) {
_vm.onClick($event, event);
},
dblclick: function ($event) {
_vm.onDblClick($event, event);
}
}
},
[
_vm.editableEvents
? _c(
"div",
{
staticClass: "vuecal__event-delete",
on: {
mousedown: function ($event) {
$event.stopPropagation();
$event.preventDefault();
_vm.deleteEvent(event);
},
touchstart: function ($event) {
$event.stopPropagation();
$event.preventDefault();
_vm.touchDeleteEvent(event);
}
}
},
[_vm._v(_vm._s(_vm.texts.deleteEvent))]
)
: _vm._e(),
_vm._t("event-renderer", null, {
event: event,
view: _vm.view
}),
_vm.editableEvents &&
_vm.time &&
event.startTime &&
!_vm.allDayEvents &&
!event.multipleDays.start &&
!event.multipleDays.middle &&
_vm.view !== "month"
? _c("div", {
staticClass: "vuecal__event-resize-handle",
on: {
mousedown: function ($event) {
_vm.editableEvents &&
_vm.time &&
_vm.onDragHandleMouseDown($event, event);
},
touchstart: function ($event) {
_vm.editableEvents &&
_vm.time &&
_vm.onDragHandleMouseDown($event, event);
}
}
})
: _vm._e()
],
2
)
}
),
0
)
: _vm._e(),
_vm.view === "month" &&
!_vm.eventsOnMonthView &&
_vm.events.length &&
!_vm.allDayEvents
? _vm._t("events-count-month-view", null, { events: _vm.events })
: _vm._e()
],
2
)
}),
_vm.timelineVisible
? _c("div", {
key: _vm.transitions ? _vm.view + "-now-line" : "now-line",
staticClass: "vuecal__now-line",
style: "top: " + _vm.todaysTimePosition + "px"
})
: _vm._e(),
_vm.$parent.dragCurrentCell == _vm.cellid
? _c(
"div",
{
key: "dragline",
staticClass: "uecal__cursor-line",
style: "top: " + _vm.$parent.cursorPosition + "px"
},
[_c("span", [_vm._v(_vm._s(_vm.texts.noEvent))])]
)
: _vm._e()
],
2
)
var _obj;
};
var __vue_staticRenderFns__ = [];
__vue_render__._withStripped = true;
/* style */
const __vue_inject_styles__ = undefined;
/* scoped */
const __vue_scope_id__ = undefined;
/* module identifier */
const __vue_module_identifier__ = undefined;
/* functional template */
const __vue_is_functional_template__ = false;
/* style inject */
/* style inject SSR */
var Cell = normalizeComponent(
{ render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ },
__vue_inject_styles__,
__vue_script__,
__vue_scope_id__,
__vue_is_functional_template__,
__vue_module_identifier__,
undefined,
undefined
);
//
const VueCal = {
name: "vue-cal",
components: { "vuecal-cell": Cell },
props: {
locale: {
type: String,
default: "en"
},
hideViewSelector: {
type: Boolean,
default: false
},
hideTitleBar: {
type: Boolean,
default: false
},
hideBody: {
type: Boolean,
default: false
},
hideWeekends: {
type: Boolean,
default: false
},
disableViews: {
type: Array,
default: () => []
},
defaultView: {
type: String,
default: "week"
},
showAllDayEvents: {
type: Boolean,
default: false
},
selectedDate: {
type: [String, Date],
default: ""
},
startWeekOnSunday: {
type: Boolean,
default: false
},
small: {
type: Boolean,
default: false
},
xsmall: {
type: Boolean,
default: false
},
clickToNavigate: {
type: Boolean,
default: false
},
dblClickToNavigate: {
type: Boolean,
default: true
},
time: {
type: Boolean,
default: true
},
timeFrom: {
type: Number,
default: 0 // In minutes.
},
timeTo: {
type: Number,
default: 24 * 60 // In minutes.
},
timeStep: {
type: Number,
default: 60 // In minutes.
},
timeCellHeight: {
type: Number,
default: 40 // In pixels.
},
"12Hour": {
type: Boolean,
default: false
},
timeFormat: {
type: String,
default: ""
},
minCellWidth: {
type: Number,
default: 0
},
splitDays: {
type: Array,
default: () => []
},
editableEvents: {
type: Boolean,
default: false
},
draggableEvents: {
type: Boolean,
default: false
},
noEventOverlaps: {
type: Boolean,
default: false
},
eventsOnMonthView: {
type: [Boolean, String],
default: false
},
onEventClick: {
type: Function,
default: () => { }
},
onEventDblclick: {
type: Function,
default: () => { }
},
transitions: {
type: Boolean,
default: true
}
},
data: () => ({
texts: {
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",
deleteEvent: "Delete",
createEvent: "Create an event",
dateFormat: "DDDD mmmm d{S}, yyyy"
},
cursorPosition: null,
dragCurrentCell: null,
ready: false,
now,
view: {
id: "",
title: "",
startDate: null,
selectedDate: null
},
eventIdIncrement: 1,
domEvents: {
mousedown: false,
dragging: false,
resizeAnEvent: {
eventId: null, // Only one at a time.
start: null,
originalHeight: 0,
newHeight: 0
},
dragAnEvent: {
eventId: null, // Only one at a time.
start: null
},
focusAnEvent: {
eventId: null // Only one at a time.
},
clickHoldAnEvent: {
eventId: null, // Only one at a time.
timeout: 1200,
timeoutId: null
},
dblTapACell: {
taps: 0,
timeout: 500
}
},
mutableEvents: {}, // An indexed array of mutable events updated each time given events array changes.
transitionDirection: "right"
}),
methods: {
previous() {
this.transitionDirection = "left";
switch (this.view.id) {
case "years":
this.switchView(
this.view.id,
new Date(this.view.startDate.getFullYear() - 25, 0, 1)
);
break;
case "year":
const firstDayOfYear = new Date(
this.view.startDate.getFullYear() - 1,
1,
1
);
this.switchView(this.view.id, firstDayOfYear);
break;
case "month":
const firstDayOfMonth = new Date(
this.view.startDate.getFullYear(),
this.view.startDate.getMonth() - 1,
1
);
this.switchView(this.view.id, firstDayOfMonth);
break;
case "week":
const firstDayOfPrevWeek = getPreviousFirstDayOfWeek(
this.view.startDate,
this.startWeekOnSunday
).subtractDays(7);
this.switchView(this.view.id, firstDayOfPrevWeek);
break;
case "day":
const day = this.view.startDate.subtractDays(1);
this.switchView(this.view.id, day);
break;
}
},
next() {
this.transitionDirection = "right";
switch (this.view.id) {
case "years":
this.switchView(
this.view.id,
new Date(this.view.startDate.getFullYear() + 25, 0, 1)
);
break;
case "year":
const firstDayOfYear = new Date(
this.view.startDate.getFullYear() + 1,
0,
1
);
this.switchView(this.view.id, firstDayOfYear);
break;
case "month":
const firstDayOfMonth = new Date(
this.view.startDate.getFullYear(),
this.view.startDate.getMonth() + 1,
1
);
this.switchView(this.view.id, firstDayOfMonth);
break;
case "week":
const firstDayOfNextWeek = getPreviousFirstDayOfWeek(
this.view.startDate,
this.startWeekOnSunday
).addDays(7);
this.switchView(this.view.id, firstDayOfNextWeek);
break;
case "day":
const day = this.view.startDate.addDays(1);
this.switchView(this.view.id, day);
break;
}
},
switchToBroaderView() {
this.transitionDirection = "left";
if (this.broaderView) this.switchView(this.broaderView);
},
switchToNarrowerView() {
this.transitionDirection = "right";
let views = Object.keys(this.views);
views = views.slice(views.indexOf(this.view.id) + 1);
const view = views.find(v => this.views[v].enabled);
if (view) this.switchView(view);
},
switchView(view, date = null, fromViewSelector = false) {
if (this.transitions && fromViewSelector) {
const views = Object.keys(this.views);
this.transitionDirection =
views.indexOf(this.view.id) > views.indexOf(view) ? "left" : "right";
}
this.view.events = [];
this.view.id = view;
let dateTmp, endTime, formattedDate, dayEvents;
if (!date) {
date = this.view.selectedDate || this.view.startDate;
if (view === "week")
date = getPreviousFirstDayOfWeek(date, this.startWeekOnSunday);
}
switch (view) {
case "years":
// Always fill first cell with a multiple of 25 years, E.g. year 2000, or 2025.
this.view.startDate = new Date(
Math.floor(date.getFullYear() / 25) * 25 || 2000,
0,
1
);
this.view.endDate = new Date(
this.view.startDate.getFullYear() + 26,
0,
0
);
break;
case "year":
this.view.startDate = new Date(date.getFullYear(), 0, 1);
this.view.endDate = new Date(date.getFullYear() + 1, 0, 0);
break;
case "month":
this.view.startDate = new Date(
date.getFullYear(),
date.getMonth(),
1
);
this.view.endDate = new Date(
date.getFullYear(),
date.getMonth() + 1,
0
);
dateTmp = new Date(this.view.startDate);
endTime = this.view.endDate.getTime();
while (dateTmp.getTime() <= endTime) {
dateTmp = dateTmp.addDays(1);
formattedDate = formatDate(dateTmp, "yyyy-mm-dd", this.texts);
dayEvents = this.mutableEvents[formattedDate] || [];
if (dayEvents.length)
this.view.events.push(
...dayEvents.map(e => this.cleanupEvent(e))
);
}
break;
case "week":
this.view.startDate =
this.hideWeekends && this.startWeekOnSunday
? date.addDays(1)
: date;
this.view.endDate = date.addDays(7);
dateTmp = new Date(date);
for (let i = 0; i < 7; i++) {
formattedDate = formatDate(
dateTmp.addDays(i),
"yyyy-mm-dd",
this.texts
);
dayEvents = this.mutableEvents[formattedDate] || [];
if (dayEvents.length)
this.view.events.push(
...dayEvents.map(e => this.cleanupEvent(e))
);
}
break;
case "day":
this.view.startDate = date;
this.view.endDate = date;
dayEvents =
this.mutableEvents[formatDate(date, "yyyy-mm-dd", this.texts)] ||
[];
if (dayEvents.length)
this.view.events = dayEvents.map(e => this.cleanupEvent(e));
break;
}
const params = {
view,
startDate: this.view.startDate,
endDate: this.view.endDate,
events: this.view.events,
...(view === "week" ? { week: this.view.startDate.getWeek() } : {})
};
this.$emit("view-change", params);
},
findAncestor(el, Class) {
while ((el = el.parentElement) && !el.classList.contains(Class));
return el;
},
isDOMElementAnEvent(el) {
return (
el.classList.contains("vuecal__event") ||
this.findAncestor(el, "vuecal__event")
);
},
onDragEnter(e) {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
},
onCellMouseMove(e, cellid) {
if (e.offsetY < 10) return;
this.cursorPosition = e.offsetY;
this.dragCurrentCell = cellid;
},
onDragOver(e, cell, cellid) {
e.dataTransfer.dropEffect = "move";
if (e.offsetY == -1) {
console.trace();
return;
}
this.dragCurrentCell = cellid;
},
dragEnd(hour, minute) {
let { dragAnEvent } = this.domEvents;
if (dragAnEvent.eventId) {
let event = this.mutableEvents[dragAnEvent.eventStartDate].find(
item => item.id === dragAnEvent.eventId
);
event.transient = true;
let d = this.viewCells[this.dragCurrentCell].date;
d = new Date(d.getFullYear(), d.getMonth(), d.getDate(), hour, minute);
event.start = event.end = d;
if (event) {
this.emitWithEvent("event-change", event);
}
dragAnEvent.eventId = null;
}
},
selectCell(cell) {
if (this.view.selectedDate.toString() !== cell.date.toString()) {
this.view.selectedDate = cell.date;
this.$emit("day-focus", cell.date);
}
// Switch to narrower view.
if (this.clickToNavigate) this.switchToNarrowerView();
// Handle double click manually for touch devices.
else if (this.dblClickToNavigate && "ontouchstart" in window) {
this.domEvents.dblTapACell.taps++;
setTimeout(
() => (this.domEvents.dblTapACell.taps = 0),
this.domEvents.dblTapACell.timeout
);
if (this.domEvents.dblTapACell.taps >= 2) {
this.domEvents.dblTapACell.taps = 0;
this.switchToNarrowerView();
}
}
},
// Event resizing is started in cell component (onMouseDown) but place onMouseMove & onMouseUp
// handlers in the single parent for performance.
onMouseMove(e) {
let { resizeAnEvent, mousedown, dragging } = this.domEvents;
if (mousedown) {
e.preventDefault();
this.domEvents.dragging = true;
const y = "ontouchstart" in window ? e.touches[0].clientY : e.clientY;
resizeAnEvent.newHeight =
resizeAnEvent.originalHeight + (y - resizeAnEvent.start);
}
},
onMouseUp(e) {
let {
focusAnEvent,
resizeAnEvent,
clickHoldAnEvent,
dragAnEvent,
mousedown,
dragging
} = this.domEvents;
// On event resize end, emit event.
if (resizeAnEvent.eventId) {
let event = this.mutableEvents[resizeAnEvent.eventStartDate].find(
item => item.id === resizeAnEvent.eventId
);
if (event) {
this.emitWithEvent("event-change", event);
this.emitWithEvent("event-duration-change", event);
}
}
// If not mouse up on an event, unfocus any event except if just dragged.
if (!this.isDOMElementAnEvent(e.target) && !resizeAnEvent.eventId) {
focusAnEvent.eventId = null; // Cancel event focus.
clickHoldAnEvent.eventId = null; // Hide delete button.
}
// Prevent showing delete button if click and hold was not long enough.
// Click & hold timeout happens in onMouseDown() in cell component.
if (clickHoldAnEvent.timeoutId && !clickHoldAnEvent.eventId) {
clearTimeout(clickHoldAnEvent.timeoutId);
clickHoldAnEvent.timeoutId = null;
}
// Any mouse up must cancel event resizing and event dragging.
resizeAnEvent.eventId = null;
resizeAnEvent.start = null;
resizeAnEvent.originalHeight = null;
resizeAnEvent.newHeight = null;
dragAnEvent.eventId = null;
dragAnEvent.start = null;
this.domEvents.mousedown = false;
this.domEvents.dragging = false;
},
onEventTitleBlur(e, event) {
event.title = e.target.innerHTML;
if (event.linked.daysCount) {
event.linked.forEach(e => {
let dayToModify = this.mutableEvents[e.date];
dayToModify.find(e2 => e2.id === e.id).title = event.title;
});
}
this.emitWithEvent("event-change", event);
this.emitWithEvent("event-title-change", event);
},
// Object of arrays of events indexed by dates.
updateMutableEvents(events) {
// eslint-disable-next-line
this.mutableEvents = {};
// Group events into dates.
events.map(event => {
// Keep the event ids scoped to this calendar instance.
// eslint-disable-next-line
let id = `${this._uid}_${this.eventIdIncrement++}`;
event = Object.assign(
{
id,
startDate:
event.start.getFullYear() +
"-" +
String(event.start.getMonth()).padStart(2, "0") +
"-" +
String(event.start.getDate()).padStart(2, "0"),
startTime: event.start.toLocaleTimeString(),
startTimeMinutes:
event.start.getHours() * 60 + event.start.getMinutes(),
endDate:
event.end.getFullYear() +
"-" +
String(event.end.getMonth()).padStart(2, "0") +
"-" +
String(event.end.getDate()).padStart(2, "0"),
endTime: event.end.toLocaleTimeString(),
endTimeMinutes: event.end.getHours() * 60 + event.end.getMinutes(),
height: 0,
top: 0,
overlapped: {},
overlapping: {},
simultaneous: {},
linked: [], // Linked events.
multipleDays: {},
allDay: false
},
event
);
const multipleDays = event.startDate != event.endDate;
event.classes = {
[event.class]: true,
"vuecal__event--background": event.background,
"vuecal__event--multiple-days": multipleDays,
"event-start": multipleDays
};
// Make array reactive for future events creations & deletions.
if (!(event.startDate in this.mutableEvents))
this.$set(this.mutableEvents, event.startDate, []);
// eslint-disable-next-line
this.mutableEvents[event.startDate].push(event);
if (multipleDays) {
// Create an array of linked events to attach to each event piece (piece = 1 day),
// So that deletion and modification reflects on all the pieces.
let eventPieces = [];
const oneDayInMs = 24 * 60 * 60 * 1000;
const [y1, m1, d1] = startDate.split("-");
const [y2, m2, d2] = endDate.split("-");
startDate = new Date(y1, parseInt(m1) - 1, d1);
endDate = new Date(y2, parseInt(m2) - 1, d2);
const datesDiff = Math.round(
Math.abs((startDate.getTi