/* global Module */ /* Magic Mirror * Module: Calendar * * By Michael Teeuw http://michaelteeuw.nl * MIT Licensed. */ Module.register("calendar", { // Define module defaults defaults: { maximumEntries: 10, // Total Maximum Entries maximumNumberOfDays: 365, displaySymbol: true, defaultSymbol: "calendar", // Fontawesome Symbol see http://fontawesome.io/cheatsheet/ displayRepeatingCountTitle: false, defaultRepeatingCountTitle: "", maxTitleLength: 25, fetchInterval: 5 * 60 * 1000, // Update every 5 minutes. animationSpeed: 2000, fade: true, urgency: 7, timeFormat: "relative", dateFormat: "MMM Do", getRelative: 6, fadePoint: 0.25, // Start on 1/4th of the list. hidePrivate: false, calendars: [ { symbol: "calendar", url: "http://www.calendarlabs.com/templates/ical/US-Holidays.ics", }, ], titleReplace: { "De verjaardag van ": "", "'s birthday": "" }, broadcastEvents: true }, // Define required scripts. getStyles: function () { return ["calendar.css", "font-awesome.css"]; }, // Define required scripts. getScripts: function () { return ["moment.js"]; }, // Define required translations. getTranslations: function () { // The translations for the defaut modules are defined in the core translation files. // Therefor we can just return false. Otherwise we should have returned a dictionairy. // If you're trying to build your own module including translations, check out the documentation. return false; }, // Override start method. start: function () { Log.log("Starting module: " + this.name); // Set locale. moment.locale(config.language); for (var c in this.config.calendars) { var calendar = this.config.calendars[c]; calendar.url = calendar.url.replace("webcal://", "http://"); this.addCalendar(calendar.url, calendar.user, calendar.pass); } this.calendarData = {}; this.loaded = false; }, // Override socket notification handler. socketNotificationReceived: function (notification, payload) { if (notification === "CALENDAR_EVENTS") { if (this.hasCalendarURL(payload.url)) { this.calendarData[payload.url] = payload.events; this.loaded = true; if (this.config.broadcastEvents) { this.broadcastEvents(); } } } 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); } else { Log.log("Calendar received an unknown socket notification: " + notification); } this.updateDom(this.config.animationSpeed); }, // Override dom generator. getDom: function () { var events = this.createEventList(); var wrapper = document.createElement("table"); wrapper.className = "small"; if (events.length === 0) { wrapper.innerHTML = (this.loaded) ? this.translate("EMPTY") : this.translate("LOADING"); wrapper.className = "small dimmed"; return wrapper; } for (var e in events) { var event = events[e]; var eventWrapper = document.createElement("tr"); eventWrapper.className = "normal"; if (this.config.displaySymbol) { var symbolWrapper = document.createElement("td"); symbolWrapper.className = "symbol"; var symbol = document.createElement("span"); symbol.className = "fa fa-" + this.symbolForUrl(event.url); symbolWrapper.appendChild(symbol); eventWrapper.appendChild(symbolWrapper); } var titleWrapper = document.createElement("td"), repeatingCountTitle = ""; if (this.config.displayRepeatingCountTitle) { repeatingCountTitle = this.countTitleForUrl(event.url); if (repeatingCountTitle !== "") { var thisYear = new Date(parseInt(event.startDate)).getFullYear(), yearDiff = thisYear - event.firstYear; repeatingCountTitle = ", " + yearDiff + ". " + repeatingCountTitle; } } titleWrapper.innerHTML = this.titleTransform(event.title) + repeatingCountTitle; titleWrapper.className = "title bright"; eventWrapper.appendChild(titleWrapper); var timeWrapper = document.createElement("td"); //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()); } } 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 { 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")) + " " + moment(event.endDate, "x").fromNow(true); } } //timeWrapper.innerHTML += ' - '+ moment(event.startDate,'x').format('lll'); //console.log(event); timeWrapper.className = "time light"; eventWrapper.appendChild(timeWrapper); 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; }, /* hasCalendarURL(url) * Check if this config contains the calendar url. * * argument url sting - Url to look for. * * return bool - Has calendar url */ hasCalendarURL: function (url) { 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 () { var events = []; var today = moment().startOf("day"); 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; } } event.url = c; event.today = event.startDate >= today && event.startDate < (today + 24 * 60 * 60 * 1000); events.push(event); } } events.sort(function (a, b) { return a.startDate - b.startDate; }); return events.slice(0, this.config.maximumEntries); }, /* createEventList(url) * Requests node helper to add calendar url. * * argument url sting - Url to add. */ addCalendar: function (url, user, pass) { this.sendSocketNotification("ADD_CALENDAR", { url: url, maximumEntries: this.config.maximumEntries, maximumNumberOfDays: this.config.maximumNumberOfDays, fetchInterval: this.config.fetchInterval, user: user, pass: pass }); }, /* symbolForUrl(url) * Retrieves the symbol for a specific url. * * argument url sting - Url to look for. * * return string - The Symbol */ symbolForUrl: function (url) { for (var c in this.config.calendars) { var calendar = this.config.calendars[c]; if (calendar.url === url && typeof calendar.symbol === "string") { return calendar.symbol; } } return this.config.defaultSymbol; }, /* countTitleForUrl(url) * Retrieves the name for a specific url. * * argument url sting - Url to look for. * * return string - The Symbol */ countTitleForUrl: function (url) { for (var c in this.config.calendars) { var calendar = this.config.calendars[c]; if (calendar.url === url && typeof calendar.repeatingCountTitle === "string") { return calendar.repeatingCountTitle; } } return this.config.defaultRepeatingCountTitle; }, /* shorten(string, maxLength) * Shortens a sting if it's longer than maxLenthg. * Adds an ellipsis to the end. * * argument string string - The string to shorten. * argument maxLength number - The max lenth of the string. * * return string - The shortened string. */ shorten: function (string, maxLength) { if (string.length > maxLength) { return string.slice(0, maxLength) + "…"; } return string; }, /* capFirst(string) * Capitalize the first letter of a string * Return capitalized string */ capFirst: function (string) { return string.charAt(0).toUpperCase() + string.slice(1); }, /* 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 * * argument title string - The title to transform. * * return string - The transformed title. */ titleTransform: function (title) { 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]); } title = title.replace(needle, replacement); } title = this.shorten(title, this.config.maxTitleLength); 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 (url in this.calendarData) { var calendar = this.calendarData[url]; for (e in calendar) { var event = cloneObject(calendar[e]); delete event.url; eventList.push(event); } } eventList.sort(function(a,b) { return a.startDate - b.startDate; }); this.sendNotification("CALENDAR_EVENTS", eventList); } });