619 lines
18 KiB
JavaScript
Raw Normal View History

2016-03-31 11:05:32 +02:00
/* global Module */
/* Magic Mirror
* Module: Calendar
*
* By Michael Teeuw http://michaelteeuw.nl
* MIT Licensed.
*/
2016-03-31 11:05:32 +02:00
Module.register("calendar", {
2016-03-31 11:05:32 +02:00
// Define module defaults
2016-03-31 11:05:32 +02:00
defaults: {
maximumEntries: 10, // Total Maximum Entries
maximumNumberOfDays: 365,
2016-03-31 11:05:32 +02:00
displaySymbol: true,
defaultSymbol: "calendar", // Fontawesome Symbol see http://fontawesome.io/cheatsheet/
displayRepeatingCountTitle: false,
defaultRepeatingCountTitle: "",
maxTitleLength: 25,
wrapEvents: false, // wrap events to multiple lines breaking at maxTitleLength
2016-03-31 11:05:32 +02:00
fetchInterval: 5 * 60 * 1000, // Update every 5 minutes.
animationSpeed: 2000,
fade: true,
urgency: 7,
timeFormat: "relative",
2016-11-10 17:26:29 +01:00
dateFormat: "MMM Do",
2017-05-02 21:20:35 +02:00
fullDayEventDateFormat: "MMM Do",
2016-09-04 00:05:02 +02:00
getRelative: 6,
2016-03-31 11:05:32 +02:00
fadePoint: 0.25, // Start on 1/4th of the list.
hidePrivate: false,
2018-04-08 14:57:28 +03:00
hideOngoing: false,
2017-01-29 00:59:38 +01:00
colored: false,
coloredSymbolOnly: false,
tableClass: "small",
2016-04-05 14:35:11 -04:00
calendars: [
2016-03-31 11:05:32 +02:00
{
2016-04-05 14:35:11 -04:00
symbol: "calendar",
url: "http://www.calendarlabs.com/templates/ical/US-Holidays.ics",
2016-03-31 11:05:32 +02:00
},
],
titleReplace: {
2016-06-06 12:05:41 +02:00
"De verjaardag van ": "",
"'s birthday": ""
},
broadcastEvents: true,
excludedEvents: []
2016-03-31 11:05:32 +02:00
},
// Define required scripts.
getStyles: function () {
2016-04-05 14:35:11 -04:00
return ["calendar.css", "font-awesome.css"];
2016-03-31 11:05:32 +02:00
},
// Define required scripts.
getScripts: function () {
2016-04-05 14:35:11 -04:00
return ["moment.js"];
2016-03-31 11:05:32 +02:00
},
2016-04-21 01:04:00 +02:00
// Define required translations.
getTranslations: function () {
2017-03-30 22:14:11 +02:00
// The translations for the default modules are defined in the core translation files.
// Therefor we can just return false. Otherwise we should have returned a dictionary.
// If you're trying to build your own module including translations, check out the documentation.
2016-05-11 12:38:41 +02:00
return false;
2016-04-21 01:04:00 +02:00
},
2016-03-31 11:05:32 +02:00
// Override start method.
start: function () {
2016-04-05 14:35:11 -04:00
Log.log("Starting module: " + this.name);
2016-03-31 11:05:32 +02:00
// Set locale.
moment.updateLocale(config.language, this.getLocaleSpecification(config.timeFormat));
2016-03-31 11:05:32 +02:00
for (var c in this.config.calendars) {
var calendar = this.config.calendars[c];
2016-04-05 14:35:11 -04:00
calendar.url = calendar.url.replace("webcal://", "http://");
var calendarConfig = {
maximumEntries: calendar.maximumEntries,
maximumNumberOfDays: calendar.maximumNumberOfDays
};
// we check user and password here for backwards compatibility with old configs
if(calendar.user && calendar.pass) {
Log.warn("Deprecation warning: Please update your calendar authentication configuration.");
Log.warn("https://github.com/MichMich/MagicMirror/tree/v2.1.2/modules/default/calendar#calendar-authentication-options");
calendar.auth = {
user: calendar.user,
pass: calendar.pass
}
}
this.addCalendar(calendar.url, calendar.auth, calendarConfig);
2016-03-31 11:05:32 +02:00
}
this.calendarData = {};
this.loaded = false;
2016-03-31 11:05:32 +02:00
},
// Override socket notification handler.
socketNotificationReceived: function (notification, payload) {
2016-04-05 14:35:11 -04:00
if (notification === "CALENDAR_EVENTS") {
2016-03-31 11:05:32 +02:00
if (this.hasCalendarURL(payload.url)) {
this.calendarData[payload.url] = payload.events;
this.loaded = true;
if (this.config.broadcastEvents) {
this.broadcastEvents();
}
2016-03-31 11:05:32 +02:00
}
2016-04-05 14:35:11 -04:00
} else if (notification === "FETCH_ERROR") {
Log.error("Calendar Error. Could not fetch calendar: " + payload.url);
} else if (notification === "INCORRECT_URL") {
Log.error("Calendar Error. Incorrect url: " + payload.url);
2016-03-31 11:05:32 +02:00
} else {
2016-04-05 14:35:11 -04:00
Log.log("Calendar received an unknown socket notification: " + notification);
2016-03-31 11:05:32 +02:00
}
this.updateDom(this.config.animationSpeed);
},
// Override dom generator.
getDom: function () {
2016-03-31 11:05:32 +02:00
var events = this.createEventList();
var wrapper = document.createElement("table");
wrapper.className = this.config.tableClass;
2016-03-31 11:05:32 +02:00
if (events.length === 0) {
2016-04-21 01:04:00 +02:00
wrapper.innerHTML = (this.loaded) ? this.translate("EMPTY") : this.translate("LOADING");
wrapper.className = this.config.tableClass + " dimmed";
2016-03-31 11:05:32 +02:00
return wrapper;
}
var lastSeenDate = "";
2016-03-31 11:05:32 +02:00
for (var e in events) {
var event = events[e];
var dateAsString = moment(event.startDate, "x").format(this.config.dateFormat);
if(this.config.timeFormat === "dateheaders"){
if(lastSeenDate !== dateAsString){
var dateRow = document.createElement("tr");
dateRow.className = "normal"
var dateCell = document.createElement("td");
dateCell.colSpan = "3";
dateCell.innerHTML = dateAsString;
dateRow.appendChild(dateCell);
wrapper.appendChild(dateRow);
lastSeenDate = dateAsString;
}
}
2016-03-31 11:05:32 +02:00
var eventWrapper = document.createElement("tr");
if (this.config.colored && !this.config.coloredSymbolOnly) {
2017-01-29 00:59:38 +01:00
eventWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
}
2016-03-31 11:05:32 +02:00
eventWrapper.className = "normal";
if (this.config.displaySymbol) {
var symbolWrapper = document.createElement("td");
if (this.config.colored && this.config.coloredSymbolOnly) {
2018-05-05 08:31:58 -05:00
symbolWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
}
2017-03-16 16:57:55 +01:00
symbolWrapper.className = "symbol align-right";
var symbols = this.symbolsForUrl(event.url);
if(typeof symbols === "string") {
2017-03-16 16:57:55 +01:00
symbols = [symbols];
}
for(var i = 0; i < symbols.length; i++) {
2017-03-16 16:57:55 +01:00
var symbol = document.createElement("span");
symbol.className = "fa fa-fw fa-" + symbols[i];
if(i > 0){
2017-03-16 16:57:55 +01:00
symbol.style.paddingLeft = "5px";
}
symbolWrapper.appendChild(symbol);
}
2016-03-31 11:05:32 +02:00
eventWrapper.appendChild(symbolWrapper);
}else if(this.config.timeFormat === "dateheaders"){
var blankCell = document.createElement("td");
blankCell.innerHTML = "&nbsp;&nbsp;&nbsp;"
eventWrapper.appendChild(blankCell);
2016-03-31 11:05:32 +02:00
}
var titleWrapper = document.createElement("td"),
repeatingCountTitle = "";
2016-05-11 12:38:41 +02:00
if (this.config.displayRepeatingCountTitle) {
2016-05-03 11:56:24 +02:00
repeatingCountTitle = this.countTitleForUrl(event.url);
2016-05-11 12:38:41 +02:00
if (repeatingCountTitle !== "") {
var thisYear = new Date(parseInt(event.startDate)).getFullYear(),
yearDiff = thisYear - event.firstYear;
2016-05-11 12:38:41 +02:00
repeatingCountTitle = ", " + yearDiff + ". " + repeatingCountTitle;
}
2016-05-11 12:38:41 +02:00
}
titleWrapper.innerHTML = this.titleTransform(event.title) + repeatingCountTitle;
2017-01-29 00:59:38 +01:00
if (!this.config.colored) {
titleWrapper.className = "title bright";
} else {
titleWrapper.className = "title";
}
if(this.config.timeFormat === "dateheaders"){
if (event.fullDayEvent) {
titleWrapper.colSpan = "2";
titleWrapper.align = "left";
}else{
var timeWrapper = document.createElement("td");
timeWrapper.className = "time light";
timeWrapper.align = "left";
timeWrapper.style.paddingLeft = "2px";
var timeFormatString = "";
switch (config.timeFormat) {
case 12: {
timeFormatString = "h:mm A";
break;
}
case 24: {
timeFormatString = "HH:mm";
break;
}
default: {
timeFormatString = "HH:mm";
break;
}
}
timeWrapper.innerHTML = moment(event.startDate, "x").format(timeFormatString);
eventWrapper.appendChild(timeWrapper);
titleWrapper.align = "right";
}
eventWrapper.appendChild(titleWrapper);
}else{
var timeWrapper = document.createElement("td");
eventWrapper.appendChild(titleWrapper);
//console.log(event.today);
var now = new Date();
// Define second, minute, hour, and day variables
var oneSecond = 1000; // 1,000 milliseconds
var oneMinute = oneSecond * 60;
var oneHour = oneMinute * 60;
var oneDay = oneHour * 24;
if (event.fullDayEvent) {
if (event.today) {
timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
} else if (event.startDate - now < oneDay && event.startDate - now > 0) {
timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
} else if (event.startDate - now < 2 * oneDay && event.startDate - now > 0) {
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
} else {
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
}
2016-04-19 10:34:14 +02:00
} else {
/* Check to see if the user displays absolute or relative dates with their events
* Also check to see if an event is happening within an 'urgency' time frameElement
* For example, if the user set an .urgency of 7 days, those events that fall within that
* time frame will be displayed with 'in xxx' time format or moment.fromNow()
*
* Note: this needs to be put in its own function, as the whole thing repeats again verbatim
*/
if (this.config.timeFormat === "absolute") {
if ((this.config.urgency > 1) && (event.startDate - now < (this.config.urgency * oneDay))) {
// This event falls within the config.urgency period that the user has set
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
} else {
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
}
} else {
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
}
2016-04-19 10:34:14 +02:00
}
2016-04-15 13:13:06 +02:00
} else {
if (event.startDate >= new Date()) {
if (event.startDate - now < 2 * oneDay) {
// This event is within the next 48 hours (2 days)
if (event.startDate - now < this.config.getRelative * oneHour) {
// If event is within 6 hour, display 'in xxx' time format or moment.fromNow()
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
} else {
// Otherwise just say 'Today/Tomorrow at such-n-such time'
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar());
}
} else {
/* Check to see if the user displays absolute or relative dates with their events
* Also check to see if an event is happening within an 'urgency' time frameElement
* For example, if the user set an .urgency of 7 days, those events that fall within that
* time frame will be displayed with 'in xxx' time format or moment.fromNow()
*
* Note: this needs to be put in its own function, as the whole thing repeats again verbatim
*/
if (this.config.timeFormat === "absolute") {
if ((this.config.urgency > 1) && (event.startDate - now < (this.config.urgency * oneDay))) {
// This event falls within the config.urgency period that the user has set
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
} else {
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
}
} else {
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
}
}
} else {
timeWrapper.innerHTML = this.capFirst(
this.translate("RUNNING", {
fallback: this.translate("RUNNING") + " {timeUntilEnd}",
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
})
);
}
2016-04-15 13:13:06 +02:00
}
//timeWrapper.innerHTML += ' - '+ moment(event.startDate,'x').format('lll');
//console.log(event);
timeWrapper.className = "time light";
eventWrapper.appendChild(timeWrapper);
}
2016-03-31 11:05:32 +02:00
wrapper.appendChild(eventWrapper);
// Create fade effect.
if (this.config.fade && this.config.fadePoint < 1) {
if (this.config.fadePoint < 0) {
this.config.fadePoint = 0;
}
var startingPoint = events.length * this.config.fadePoint;
var steps = events.length - startingPoint;
if (e >= startingPoint) {
var currentStep = e - startingPoint;
eventWrapper.style.opacity = 1 - (1 / steps * currentStep);
}
}
}
return wrapper;
},
/**
* This function accepts a number (either 12 or 24) and returns a moment.js LocaleSpecification with the
* corresponding timeformat to be used in the calendar display. If no number is given (or otherwise invalid input)
* it will a localeSpecification object with the system locale time format.
*
* @param {number} timeFormat Specifies either 12 or 24 hour time format
* @returns {moment.LocaleSpecification}
*/
getLocaleSpecification: function(timeFormat) {
switch (timeFormat) {
case 12: {
return { longDateFormat: {LT: "h:mm A"} };
break;
}
case 24: {
return { longDateFormat: {LT: "HH:mm"} };
break;
}
default: {
return { longDateFormat: {LT: moment.localeData().longDateFormat("LT")} };
break;
}
}
},
2016-03-31 11:05:32 +02:00
/* hasCalendarURL(url)
* Check if this config contains the calendar url.
*
* argument url string - Url to look for.
*
* return bool - Has calendar url
*/
hasCalendarURL: function (url) {
2016-03-31 11:05:32 +02:00
for (var c in this.config.calendars) {
var calendar = this.config.calendars[c];
if (calendar.url === url) {
return true;
}
}
return false;
},
/* createEventList()
* Creates the sorted list of all events.
*
* return array - Array with events.
*/
createEventList: function () {
2016-03-31 11:05:32 +02:00
var events = [];
2016-04-05 14:35:11 -04:00
var today = moment().startOf("day");
2018-04-08 14:57:28 +03:00
var now = new Date();
2016-03-31 11:05:32 +02:00
for (var c in this.calendarData) {
var calendar = this.calendarData[c];
for (var e in calendar) {
var event = calendar[e];
if(this.config.hidePrivate) {
if(event.class === "PRIVATE") {
// do not add the current event, skip it
continue;
2018-04-08 14:57:28 +03:00
}
}
if(this.config.hideOngoing) {
if(event.startDate < now) {
2018-04-08 14:57:28 +03:00
continue;
}
}
if(this.listContainsEvent(events,event)){
continue;
}
2016-03-31 11:05:32 +02:00
event.url = c;
2016-04-05 11:20:47 +02:00
event.today = event.startDate >= today && event.startDate < (today + 24 * 60 * 60 * 1000);
2016-03-31 11:05:32 +02:00
events.push(event);
}
}
events.sort(function (a, b) {
2016-03-31 11:05:32 +02:00
return a.startDate - b.startDate;
});
return events.slice(0, this.config.maximumEntries);
2016-03-31 11:05:32 +02:00
},
listContainsEvent: function(eventList, event){
for(let evt of eventList){
if(evt.title === event.title && parseInt(evt.startDate) === parseInt(event.startDate)){
return true;
}
}
return false;
},
2016-03-31 11:05:32 +02:00
/* createEventList(url)
* Requests node helper to add calendar url.
*
* argument url string - Url to add.
*/
addCalendar: function (url, auth, calendarConfig) {
2016-04-05 14:35:11 -04:00
this.sendSocketNotification("ADD_CALENDAR", {
2016-03-31 11:05:32 +02:00
url: url,
excludedEvents: calendarConfig.excludedEvents || this.config.excludedEvents,
maximumEntries: calendarConfig.maximumEntries || this.config.maximumEntries,
maximumNumberOfDays: calendarConfig.maximumNumberOfDays || this.config.maximumNumberOfDays,
fetchInterval: this.config.fetchInterval,
auth: auth
2016-03-31 11:05:32 +02:00
});
},
2017-03-16 16:57:55 +01:00
/* symbolsForUrl(url)
* Retrieves the symbols for a specific url.
*
* argument url string - Url to look for.
*
* return string/array - The Symbols
*/
2017-03-16 16:57:55 +01:00
symbolsForUrl: function (url) {
2017-02-08 00:05:28 +01:00
return this.getCalendarProperty(url, "symbol", this.config.defaultSymbol);
2016-03-31 11:05:32 +02:00
},
2017-01-29 00:59:38 +01:00
/* colorForUrl(url)
* Retrieves the color for a specific url.
*
* argument url string - Url to look for.
*
* return string - The Color
*/
colorForUrl: function (url) {
2017-02-08 00:05:28 +01:00
return this.getCalendarProperty(url, "color", "#fff");
},
2017-02-07 23:51:13 +01:00
/* countTitleForUrl(url)
* Retrieves the name for a specific url.
*
* argument url string - Url to look for.
*
* return string - The Symbol
*/
countTitleForUrl: function (url) {
2017-02-07 23:51:13 +01:00
return this.getCalendarProperty(url, "repeatingCountTitle", this.config.defaultRepeatingCountTitle);
},
2017-02-07 23:51:13 +01:00
/* getCalendarProperty(url, property, defaultValue)
* Helper method to retrieve the property for a specific url.
*
* argument url string - Url to look for.
* argument property string - Property to look for.
* argument defaultValue string - Value if property is not found.
*
* return string - The Property
*/
2017-02-07 23:51:13 +01:00
getCalendarProperty: function (url, property, defaultValue) {
2017-02-08 00:05:28 +01:00
for (var c in this.config.calendars) {
var calendar = this.config.calendars[c];
2017-03-16 16:57:55 +01:00
if (calendar.url === url && calendar.hasOwnProperty(property)) {
2017-02-08 00:05:28 +01:00
return calendar[property];
}
}
return defaultValue;
},
2016-03-31 11:05:32 +02:00
/**
* Shortens a string if it's longer than maxLength and add a ellipsis to the end
*
* @param {string} string Text string to shorten
* @param {number} maxLength The max length of the string
* @param {boolean} wrapEvents Wrap the text after the line has reached maxLength
* @returns {string} The shortened string
*/
shorten: function (string, maxLength, wrapEvents) {
if (typeof string !== "string") {
return "";
}
if (wrapEvents === true) {
var temp = "";
var currentLine = "";
var words = string.split(" ");
for (var i = 0; i < words.length; i++) {
var word = words[i];
if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) { // max - 1 to account for a space
currentLine += (word + " ");
} else {
if (currentLine.length > 0) {
temp += (currentLine + "<br>" + word + " ");
} else {
temp += (word + "<br>");
}
currentLine = "";
}
}
2016-03-31 11:05:32 +02:00
return (temp + currentLine).trim();
} else {
if (maxLength && typeof maxLength === "number" && string.length > maxLength) {
return string.trim().slice(0, maxLength) + "&hellip;";
} else {
return string.trim();
}
}
2016-03-31 11:05:32 +02:00
},
/* capFirst(string)
* Capitalize the first letter of a string
* Return capitalized string
*/
capFirst: function (string) {
return string.charAt(0).toUpperCase() + string.slice(1);
},
2016-03-31 11:05:32 +02:00
/* titleTransform(title)
* Transforms the title of an event for usage.
* Replaces parts of the text as defined in config.titleReplace.
* Shortens title based on config.maxTitleLength and config.wrapEvents
*
* argument title string - The title to transform.
*
* return string - The transformed title.
*/
titleTransform: function (title) {
2016-03-31 11:05:32 +02:00
for (var needle in this.config.titleReplace) {
var replacement = this.config.titleReplace[needle];
var regParts = needle.match(/^\/(.+)\/([gim]*)$/);
if (regParts) {
// the parsed pattern is a regexp.
needle = new RegExp(regParts[1], regParts[2]);
}
2016-03-31 11:05:32 +02:00
title = title.replace(needle, replacement);
}
title = this.shorten(title, this.config.maxTitleLength, this.config.wrapEvents);
2016-03-31 11:05:32 +02:00
return title;
},
/* broadcastEvents()
* Broadcasts the events to all other modules for reuse.
* The all events available in one array, sorted on startdate.
*/
broadcastEvents: function () {
var eventList = [];
for (var url in this.calendarData) {
var calendar = this.calendarData[url];
for (var e in calendar) {
var event = cloneObject(calendar[e]);
event.symbol = this.symbolsForUrl(url);
event.color = this.colorForUrl(url);
delete event.url;
eventList.push(event);
}
}
eventList.sort(function(a,b) {
return a.startDate - b.startDate;
});
this.sendNotification("CALENDAR_EVENTS", eventList);
2016-03-31 11:05:32 +02:00
}
});