mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-27 03:39:55 +00:00
## [2.25.0] - 2023-10-01 Thanks to: @bugsounet, @dgoth, @dependabot, @kenzal, @Knapoc, @KristjanESPERANTO, @martingron, @NolanKingdon, @Paranoid93, @TeddyStarinvest and @Ybbet. Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not all) of the work on this release as project collaborators. This version would not be there without their effort. Thank you guys! You are awesome! > ⚠️ This release needs nodejs version >= `v18`, older releases have reached end of life and will not work! ### Added - Added UV Index support to OpenWeatherMap - Added 'hideDuplicates' flag to the calendar module - Added `allowOverrideNotification` to weather module to enable sending current weather objects with the `CURRENT_WEATHER_OVERRIDE` notification to supplement/replace the current weather displayed - Added optional AnimateCSS animate for `hide()`, `show()`, `updateDom()` - Added AnimateIn and animateOut in module config definition - Apply AnimateIn rules on the first start - Added automatic client page reload when server was restarted by setting `reloadAfterServerRestart: true` in `config.js`, per default `false` (#3105) - Added eventClass option for customEvents on the default calendar - Added AnimateCSS integration in tests suite (#3206) - Added npm dependabot [Reserved to developer] (#3210) - Added improved logging for calendar (#3110) ### Removed - **Breaking Change**: Removed `digest` authentication method from calendar module (which was already broken since release `2.15.0`) ### Updated - Update roboto fonts to version v5 - Update issue template - Update dev/dependencies incl. electron to v26 - Replace pretty-quick by lint-staged (<https://github.com/azz/pretty-quick/issues/164>) - Update engine node >=18. v16 reached it's end of life. (#3170) - Update typescript definition for modules - Cleaned up nunjuck templates - Replace `node-fetch` with internal fetch (#2649) and remove `digest-fetch` - Update the French translation according to the English file. - Update dependabot incl. vendor/fonts (monthly check) - Renew `package-lock.json` for release ### Fixed - Fix engine check on npm install (#3135) - Fix undefined formatTime method in clock module (#3143) - Fix clientonly startup fails after async added (#3151) - Fix electron width/heigth when using xrandr under bullseye - Fix time issue with certain recurring events in calendar module - Fix ipWhiteList test (#3179) - Fix newsfeed: Convert HTML entities, codes and tag in description (#3191) - Respect width/height (no fullscreen) if set in electronOptions (together with `fullscreen: false`) in `config.js` (#3174) - Fix: AnimateCSS merge hide() and show() animated css class when we do multiple call - Fix `Uncaught SyntaxError: Identifier 'getCorsUrl' has already been declared (at utils.js:1:1)` when using `clock` and `weather` module (#3204) - Fix overriding `config.js` when running tests (#3201) - Fix issue in weathergov provider with probability of precipitation not showing up on hourly or daily forecast --------- Signed-off-by: naveen <172697+naveensrinivasan@users.noreply.github.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Karsten Hassel <hassel@gmx.de> Co-authored-by: Malte Hallström <46646495+SkySails@users.noreply.github.com> Co-authored-by: Veeck <github@veeck.de> Co-authored-by: veeck <michael@veeck.de> Co-authored-by: dWoolridge <dwoolridge@charter.net> Co-authored-by: Johan <jojjepersson@yahoo.se> Co-authored-by: Dario Mratovich <dario_mratovich@hotmail.com> Co-authored-by: Dario Mratovich <dario.mratovich@outlook.com> Co-authored-by: Magnus <34011212+MagMar94@users.noreply.github.com> Co-authored-by: Naveen <172697+naveensrinivasan@users.noreply.github.com> Co-authored-by: buxxi <buxxi@omfilm.net> Co-authored-by: Thomas Hirschberger <47733292+Tom-Hirschberger@users.noreply.github.com> Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Co-authored-by: Andrés Vanegas Jiménez <142350+angeldeejay@users.noreply.github.com> Co-authored-by: Dave Child <dave@addedbytes.com> Co-authored-by: grenagit <46225780+grenagit@users.noreply.github.com> Co-authored-by: Grena <grena@grenabox.fr> Co-authored-by: Magnus Marthinsen <magmar@online.no> Co-authored-by: Patrick <psieg@users.noreply.github.com> Co-authored-by: Piotr Rajnisz <56397164+rajniszp@users.noreply.github.com> Co-authored-by: Suthep Yonphimai <tomzt@users.noreply.github.com> Co-authored-by: CarJem Generations (Carter Wallace) <cwallacecs@gmail.com> Co-authored-by: Nicholas Fogal <nfogal.misc@gmail.com> Co-authored-by: JakeBinney <126349119+JakeBinney@users.noreply.github.com> Co-authored-by: OWL4C <124401812+OWL4C@users.noreply.github.com> Co-authored-by: Oscar Björkman <17575446+oscarb@users.noreply.github.com> Co-authored-by: Ismar Slomic <ismar@slomic.no> Co-authored-by: Jørgen Veum-Wahlberg <jorgen.wahlberg@amedia.no> Co-authored-by: Eddie Hung <6740044+eddiehung@users.noreply.github.com> Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr> Co-authored-by: bugsounet <bugsounet@bugsounet.fr> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Knapoc <Knapoc@users.noreply.github.com> Co-authored-by: sam detweiler <sdetweil@gmail.com> Co-authored-by: veeck <michael.veeck@nebenan.de> Co-authored-by: Paranoid93 <6515818+Paranoid93@users.noreply.github.com> Co-authored-by: NolanKingdon <27908974+NolanKingdon@users.noreply.github.com> Co-authored-by: J. Kenzal Hunter <kenzal.hunter@gmail.com> Co-authored-by: Teddy <teddy.payet@gmail.com> Co-authored-by: TeddyStarinvest <teddy.payet@starinvest.com> Co-authored-by: martingron <61826403+martingron@users.noreply.github.com> Co-authored-by: dgoth <132394363+dgoth@users.noreply.github.com>
865 lines
31 KiB
JavaScript
865 lines
31 KiB
JavaScript
/* global CalendarUtils, cloneObject */
|
|
|
|
/* MagicMirror²
|
|
* Module: Calendar
|
|
*
|
|
* By Michael Teeuw https://michaelteeuw.nl
|
|
* MIT Licensed.
|
|
*/
|
|
Module.register("calendar", {
|
|
// Define module defaults
|
|
defaults: {
|
|
maximumEntries: 10, // Total Maximum Entries
|
|
maximumNumberOfDays: 365,
|
|
limitDays: 0, // Limit the number of days shown, 0 = no limit
|
|
pastDaysCount: 0,
|
|
displaySymbol: true,
|
|
defaultSymbol: "calendar-alt", // Fontawesome Symbol see https://fontawesome.com/cheatsheet?from=io
|
|
defaultSymbolClassName: "fas fa-fw fa-",
|
|
showLocation: false,
|
|
displayRepeatingCountTitle: false,
|
|
defaultRepeatingCountTitle: "",
|
|
maxTitleLength: 25,
|
|
maxLocationTitleLength: 25,
|
|
wrapEvents: false, // Wrap events to multiple lines breaking at maxTitleLength
|
|
wrapLocationEvents: false,
|
|
maxTitleLines: 3,
|
|
maxEventTitleLines: 3,
|
|
fetchInterval: 60 * 60 * 1000, // Update every hour
|
|
animationSpeed: 2000,
|
|
fade: true,
|
|
urgency: 7,
|
|
timeFormat: "relative",
|
|
dateFormat: "MMM Do",
|
|
dateEndFormat: "LT",
|
|
fullDayEventDateFormat: "MMM Do",
|
|
showEnd: false,
|
|
getRelative: 6,
|
|
fadePoint: 0.25, // Start on 1/4th of the list.
|
|
hidePrivate: false,
|
|
hideOngoing: false,
|
|
hideTime: false,
|
|
hideDuplicates: true,
|
|
showTimeToday: false,
|
|
colored: false,
|
|
customEvents: [], // Array of {keyword: "", symbol: "", color: "", eventClass: ""} where Keyword is a regexp and symbol/color/eventClass are to be applied for matched
|
|
tableClass: "small",
|
|
calendars: [
|
|
{
|
|
symbol: "calendar-alt",
|
|
url: "https://www.calendarlabs.com/templates/ical/US-Holidays.ics"
|
|
}
|
|
],
|
|
titleReplace: {
|
|
"De verjaardag van ": "",
|
|
"'s birthday": ""
|
|
},
|
|
locationTitleReplace: {
|
|
"street ": ""
|
|
},
|
|
broadcastEvents: true,
|
|
excludedEvents: [],
|
|
sliceMultiDayEvents: false,
|
|
broadcastPastEvents: false,
|
|
nextDaysRelative: false,
|
|
selfSignedCert: false,
|
|
coloredText: false,
|
|
coloredBorder: false,
|
|
coloredSymbol: false,
|
|
coloredBackground: false,
|
|
limitDaysNeverSkip: false,
|
|
flipDateHeaderTitle: false
|
|
},
|
|
|
|
requiresVersion: "2.1.0",
|
|
|
|
// Define required scripts.
|
|
getStyles: function () {
|
|
return ["calendar.css", "font-awesome.css"];
|
|
},
|
|
|
|
// Define required scripts.
|
|
getScripts: function () {
|
|
return ["calendarutils.js", "moment.js"];
|
|
},
|
|
|
|
// Define required translations.
|
|
getTranslations: function () {
|
|
// The translations for the default modules are defined in the core translation files.
|
|
// Therefore 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.
|
|
return false;
|
|
},
|
|
|
|
// Override start method.
|
|
start: function () {
|
|
const ONE_MINUTE = 60 * 1000;
|
|
|
|
Log.info(`Starting module: ${this.name}`);
|
|
|
|
if (this.config.colored) {
|
|
Log.warn("Your are using the deprecated config values 'colored'. Please switch to 'coloredSymbol' & 'coloredText'!");
|
|
this.config.coloredText = true;
|
|
this.config.coloredSymbol = true;
|
|
}
|
|
if (this.config.coloredSymbolOnly) {
|
|
Log.warn("Your are using the deprecated config values 'coloredSymbolOnly'. Please switch to 'coloredSymbol' & 'coloredText'!");
|
|
this.config.coloredText = false;
|
|
this.config.coloredSymbol = true;
|
|
}
|
|
|
|
// Set locale.
|
|
moment.updateLocale(config.language, CalendarUtils.getLocaleSpecification(config.timeFormat));
|
|
|
|
// clear data holder before start
|
|
this.calendarData = {};
|
|
|
|
// indicate no data available yet
|
|
this.loaded = false;
|
|
|
|
this.config.calendars.forEach((calendar) => {
|
|
calendar.url = calendar.url.replace("webcal://", "http://");
|
|
|
|
const calendarConfig = {
|
|
maximumEntries: calendar.maximumEntries,
|
|
maximumNumberOfDays: calendar.maximumNumberOfDays,
|
|
pastDaysCount: calendar.pastDaysCount,
|
|
broadcastPastEvents: calendar.broadcastPastEvents,
|
|
selfSignedCert: calendar.selfSignedCert
|
|
};
|
|
|
|
if (calendar.symbolClass === "undefined" || calendar.symbolClass === null) {
|
|
calendarConfig.symbolClass = "";
|
|
}
|
|
if (calendar.titleClass === "undefined" || calendar.titleClass === null) {
|
|
calendarConfig.titleClass = "";
|
|
}
|
|
if (calendar.timeClass === "undefined" || calendar.timeClass === null) {
|
|
calendarConfig.timeClass = "";
|
|
}
|
|
|
|
// 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
|
|
};
|
|
}
|
|
|
|
// tell helper to start a fetcher for this calendar
|
|
// fetcher till cycle
|
|
this.addCalendar(calendar.url, calendar.auth, calendarConfig);
|
|
});
|
|
|
|
// Refresh the DOM every minute if needed: When using relative date format for events that start
|
|
// or end in less than an hour, the date shows minute granularity and we want to keep that accurate.
|
|
setTimeout(
|
|
() => {
|
|
setInterval(() => {
|
|
this.updateDom(1);
|
|
}, ONE_MINUTE);
|
|
},
|
|
ONE_MINUTE - (new Date() % ONE_MINUTE)
|
|
);
|
|
},
|
|
|
|
// Override socket notification handler.
|
|
socketNotificationReceived: function (notification, payload) {
|
|
if (notification === "FETCH_CALENDAR") {
|
|
this.sendSocketNotification(notification, { url: payload.url, id: this.identifier });
|
|
}
|
|
|
|
if (this.identifier !== payload.id) {
|
|
return;
|
|
}
|
|
|
|
if (notification === "CALENDAR_EVENTS") {
|
|
if (this.hasCalendarURL(payload.url)) {
|
|
this.calendarData[payload.url] = payload.events;
|
|
this.error = null;
|
|
this.loaded = true;
|
|
|
|
if (this.config.broadcastEvents) {
|
|
this.broadcastEvents();
|
|
}
|
|
}
|
|
} else if (notification === "CALENDAR_ERROR") {
|
|
let error_message = this.translate(payload.error_type);
|
|
this.error = this.translate("MODULE_CONFIG_ERROR", { MODULE_NAME: this.name, ERROR: error_message });
|
|
this.loaded = true;
|
|
}
|
|
|
|
this.updateDom(this.config.animationSpeed);
|
|
},
|
|
|
|
// Override dom generator.
|
|
getDom: function () {
|
|
const ONE_SECOND = 1000; // 1,000 milliseconds
|
|
const ONE_MINUTE = ONE_SECOND * 60;
|
|
const ONE_HOUR = ONE_MINUTE * 60;
|
|
const ONE_DAY = ONE_HOUR * 24;
|
|
|
|
const events = this.createEventList(true);
|
|
const wrapper = document.createElement("table");
|
|
wrapper.className = this.config.tableClass;
|
|
|
|
if (this.error) {
|
|
wrapper.innerHTML = this.error;
|
|
wrapper.className = `${this.config.tableClass} dimmed`;
|
|
return wrapper;
|
|
}
|
|
|
|
if (events.length === 0) {
|
|
wrapper.innerHTML = this.loaded ? this.translate("EMPTY") : this.translate("LOADING");
|
|
wrapper.className = `${this.config.tableClass} dimmed`;
|
|
return wrapper;
|
|
}
|
|
|
|
let currentFadeStep = 0;
|
|
let startFade;
|
|
let fadeSteps;
|
|
|
|
if (this.config.fade && this.config.fadePoint < 1) {
|
|
if (this.config.fadePoint < 0) {
|
|
this.config.fadePoint = 0;
|
|
}
|
|
startFade = events.length * this.config.fadePoint;
|
|
fadeSteps = events.length - startFade;
|
|
}
|
|
|
|
let lastSeenDate = "";
|
|
|
|
events.forEach((event, index) => {
|
|
const dateAsString = moment(event.startDate, "x").format(this.config.dateFormat);
|
|
if (this.config.timeFormat === "dateheaders") {
|
|
if (lastSeenDate !== dateAsString) {
|
|
const dateRow = document.createElement("tr");
|
|
dateRow.className = "dateheader normal";
|
|
if (event.today) dateRow.className += " today";
|
|
else if (event.dayBeforeYesterday) dateRow.className += " dayBeforeYesterday";
|
|
else if (event.yesterday) dateRow.className += " yesterday";
|
|
else if (event.tomorrow) dateRow.className += " tomorrow";
|
|
else if (event.dayAfterTomorrow) dateRow.className += " dayAfterTomorrow";
|
|
|
|
const dateCell = document.createElement("td");
|
|
dateCell.colSpan = "3";
|
|
dateCell.innerHTML = dateAsString;
|
|
dateCell.style.paddingTop = "10px";
|
|
dateRow.appendChild(dateCell);
|
|
wrapper.appendChild(dateRow);
|
|
|
|
if (this.config.fade && index >= startFade) {
|
|
//fading
|
|
currentFadeStep = index - startFade;
|
|
dateRow.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
|
|
}
|
|
|
|
lastSeenDate = dateAsString;
|
|
}
|
|
}
|
|
|
|
const eventWrapper = document.createElement("tr");
|
|
|
|
if (this.config.coloredText) {
|
|
eventWrapper.style.cssText = `color:${this.colorForUrl(event.url, false)}`;
|
|
}
|
|
|
|
if (this.config.coloredBackground) {
|
|
eventWrapper.style.backgroundColor = this.colorForUrl(event.url, true);
|
|
}
|
|
|
|
if (this.config.coloredBorder) {
|
|
eventWrapper.style.borderColor = this.colorForUrl(event.url, false);
|
|
}
|
|
|
|
eventWrapper.className = "event-wrapper normal event";
|
|
if (event.today) eventWrapper.className += " today";
|
|
else if (event.dayBeforeYesterday) eventWrapper.className += " dayBeforeYesterday";
|
|
else if (event.yesterday) eventWrapper.className += " yesterday";
|
|
else if (event.tomorrow) eventWrapper.className += " tomorrow";
|
|
else if (event.dayAfterTomorrow) eventWrapper.className += " dayAfterTomorrow";
|
|
|
|
const symbolWrapper = document.createElement("td");
|
|
|
|
if (this.config.displaySymbol) {
|
|
if (this.config.coloredSymbol) {
|
|
symbolWrapper.style.cssText = `color:${this.colorForUrl(event.url, false)}`;
|
|
}
|
|
|
|
const symbolClass = this.symbolClassForUrl(event.url);
|
|
symbolWrapper.className = `symbol align-right ${symbolClass}`;
|
|
|
|
const symbols = this.symbolsForEvent(event);
|
|
symbols.forEach((s, index) => {
|
|
const symbol = document.createElement("span");
|
|
symbol.className = s;
|
|
if (index > 0) {
|
|
symbol.style.paddingLeft = "5px";
|
|
}
|
|
symbolWrapper.appendChild(symbol);
|
|
});
|
|
eventWrapper.appendChild(symbolWrapper);
|
|
} else if (this.config.timeFormat === "dateheaders") {
|
|
const blankCell = document.createElement("td");
|
|
blankCell.innerHTML = " ";
|
|
eventWrapper.appendChild(blankCell);
|
|
}
|
|
|
|
const titleWrapper = document.createElement("td");
|
|
let repeatingCountTitle = "";
|
|
|
|
if (this.config.displayRepeatingCountTitle && event.firstYear !== undefined) {
|
|
repeatingCountTitle = this.countTitleForUrl(event.url);
|
|
|
|
if (repeatingCountTitle !== "") {
|
|
const thisYear = new Date(parseInt(event.startDate)).getFullYear(),
|
|
yearDiff = thisYear - event.firstYear;
|
|
|
|
repeatingCountTitle = `, ${yearDiff} ${repeatingCountTitle}`;
|
|
}
|
|
}
|
|
|
|
// Color events if custom color or eventClass are specified
|
|
if (this.config.customEvents.length > 0) {
|
|
for (let ev in this.config.customEvents) {
|
|
let needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
|
|
if (needle.test(event.title)) {
|
|
if (typeof this.config.customEvents[ev].color !== "undefined" && this.config.customEvents[ev].color !== "") {
|
|
// Respect parameter ColoredSymbolOnly also for custom events
|
|
if (this.config.coloredText) {
|
|
eventWrapper.style.cssText = `color:${this.config.customEvents[ev].color}`;
|
|
titleWrapper.style.cssText = `color:${this.config.customEvents[ev].color}`;
|
|
}
|
|
if (this.config.displaySymbol && this.config.coloredSymbol) {
|
|
symbolWrapper.style.cssText = `color:${this.config.customEvents[ev].color}`;
|
|
}
|
|
break;
|
|
}
|
|
if (typeof this.config.customEvents[ev].eventClass !== "undefined" && this.config.customEvents[ev].eventClass !== "") {
|
|
eventWrapper.className += ` ${this.config.customEvents[ev].eventClass}`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const transformedTitle = CalendarUtils.titleTransform(event.title, this.config.titleReplace);
|
|
titleWrapper.innerHTML = CalendarUtils.shorten(transformedTitle, this.config.maxTitleLength, this.config.wrapEvents, this.config.maxTitleLines) + repeatingCountTitle;
|
|
|
|
const titleClass = this.titleClassForUrl(event.url);
|
|
|
|
if (!this.config.coloredText) {
|
|
titleWrapper.className = `title bright ${titleClass}`;
|
|
} else {
|
|
titleWrapper.className = `title ${titleClass}`;
|
|
}
|
|
|
|
if (this.config.timeFormat === "dateheaders") {
|
|
if (this.config.flipDateHeaderTitle) eventWrapper.appendChild(titleWrapper);
|
|
|
|
if (event.fullDayEvent) {
|
|
titleWrapper.colSpan = "2";
|
|
titleWrapper.classList.add("align-left");
|
|
} else {
|
|
const timeWrapper = document.createElement("td");
|
|
timeWrapper.className = `time light ${this.config.flipDateHeaderTitle ? "align-right " : "align-left "}${this.timeClassForUrl(event.url)}`;
|
|
timeWrapper.style.paddingLeft = "2px";
|
|
timeWrapper.style.textAlign = this.config.flipDateHeaderTitle ? "right" : "left";
|
|
timeWrapper.innerHTML = moment(event.startDate, "x").format("LT");
|
|
|
|
// Add endDate to dataheaders if showEnd is enabled
|
|
if (this.config.showEnd) {
|
|
timeWrapper.innerHTML += ` - ${CalendarUtils.capFirst(moment(event.endDate, "x").format("LT"))}`;
|
|
}
|
|
|
|
eventWrapper.appendChild(timeWrapper);
|
|
|
|
if (!this.config.flipDateHeaderTitle) titleWrapper.classList.add("align-right");
|
|
}
|
|
if (!this.config.flipDateHeaderTitle) eventWrapper.appendChild(titleWrapper);
|
|
} else {
|
|
const timeWrapper = document.createElement("td");
|
|
|
|
eventWrapper.appendChild(titleWrapper);
|
|
const now = new Date();
|
|
|
|
if (this.config.timeFormat === "absolute") {
|
|
// Use dateFormat
|
|
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
|
|
// Add end time if showEnd
|
|
if (this.config.showEnd) {
|
|
timeWrapper.innerHTML += "-";
|
|
timeWrapper.innerHTML += CalendarUtils.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
|
|
}
|
|
// For full day events we use the fullDayEventDateFormat
|
|
if (event.fullDayEvent) {
|
|
//subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day
|
|
event.endDate -= ONE_SECOND;
|
|
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
|
|
} else if (this.config.getRelative > 0 && event.startDate < now) {
|
|
// Ongoing and getRelative is set
|
|
timeWrapper.innerHTML = CalendarUtils.capFirst(
|
|
this.translate("RUNNING", {
|
|
fallback: `${this.translate("RUNNING")} {timeUntilEnd}`,
|
|
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
|
|
})
|
|
);
|
|
} else if (this.config.urgency > 0 && event.startDate - now < this.config.urgency * ONE_DAY) {
|
|
// Within urgency days
|
|
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").fromNow());
|
|
}
|
|
if (event.fullDayEvent && this.config.nextDaysRelative) {
|
|
// Full days events within the next two days
|
|
if (event.today) {
|
|
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY"));
|
|
} else if (event.yesterday) {
|
|
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("YESTERDAY"));
|
|
} else if (event.startDate - now < ONE_DAY && event.startDate - now > 0) {
|
|
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TOMORROW"));
|
|
} else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) {
|
|
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
|
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW"));
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Show relative times
|
|
if (event.startDate >= now || (event.fullDayEvent && event.today)) {
|
|
// Use relative time
|
|
if (!this.config.hideTime && !event.fullDayEvent) {
|
|
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").calendar(null, { sameElse: this.config.dateFormat }));
|
|
} else {
|
|
timeWrapper.innerHTML = CalendarUtils.capFirst(
|
|
moment(event.startDate, "x").calendar(null, {
|
|
sameDay: this.config.showTimeToday ? "LT" : `[${this.translate("TODAY")}]`,
|
|
nextDay: `[${this.translate("TOMORROW")}]`,
|
|
nextWeek: "dddd",
|
|
sameElse: event.fullDayEvent ? this.config.fullDayEventDateFormat : this.config.dateFormat
|
|
})
|
|
);
|
|
}
|
|
if (event.fullDayEvent) {
|
|
// Full days events within the next two days
|
|
if (event.today) {
|
|
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY"));
|
|
} else if (event.dayBeforeYesterday) {
|
|
if (this.translate("DAYBEFOREYESTERDAY") !== "DAYBEFOREYESTERDAY") {
|
|
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYBEFOREYESTERDAY"));
|
|
}
|
|
} else if (event.yesterday) {
|
|
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("YESTERDAY"));
|
|
} else if (event.startDate - now < ONE_DAY && event.startDate - now > 0) {
|
|
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TOMORROW"));
|
|
} else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) {
|
|
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
|
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW"));
|
|
}
|
|
}
|
|
} else if (event.startDate - now < this.config.getRelative * ONE_HOUR) {
|
|
// If event is within getRelative hours, display 'in xxx' time format or moment.fromNow()
|
|
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").fromNow());
|
|
}
|
|
} else {
|
|
// Ongoing event
|
|
timeWrapper.innerHTML = CalendarUtils.capFirst(
|
|
this.translate("RUNNING", {
|
|
fallback: `${this.translate("RUNNING")} {timeUntilEnd}`,
|
|
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
|
|
})
|
|
);
|
|
}
|
|
}
|
|
timeWrapper.className = `time light ${this.timeClassForUrl(event.url)}`;
|
|
eventWrapper.appendChild(timeWrapper);
|
|
}
|
|
|
|
// Create fade effect.
|
|
if (index >= startFade) {
|
|
currentFadeStep = index - startFade;
|
|
eventWrapper.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
|
|
}
|
|
wrapper.appendChild(eventWrapper);
|
|
|
|
if (this.config.showLocation) {
|
|
if (event.location !== false) {
|
|
const locationRow = document.createElement("tr");
|
|
locationRow.className = "event-wrapper-location normal xsmall light";
|
|
if (event.today) locationRow.className += " today";
|
|
else if (event.dayBeforeYesterday) locationRow.className += " dayBeforeYesterday";
|
|
else if (event.yesterday) locationRow.className += " yesterday";
|
|
else if (event.tomorrow) locationRow.className += " tomorrow";
|
|
else if (event.dayAfterTomorrow) locationRow.className += " dayAfterTomorrow";
|
|
|
|
if (this.config.displaySymbol) {
|
|
const symbolCell = document.createElement("td");
|
|
locationRow.appendChild(symbolCell);
|
|
}
|
|
|
|
if (this.config.coloredText) {
|
|
locationRow.style.cssText = `color:${this.colorForUrl(event.url, false)}`;
|
|
}
|
|
|
|
if (this.config.coloredBackground) {
|
|
locationRow.style.backgroundColor = this.colorForUrl(event.url, true);
|
|
}
|
|
|
|
if (this.config.coloredBorder) {
|
|
locationRow.style.borderColor = this.colorForUrl(event.url, false);
|
|
}
|
|
|
|
const descCell = document.createElement("td");
|
|
descCell.className = "location";
|
|
descCell.colSpan = "2";
|
|
|
|
const transformedTitle = CalendarUtils.titleTransform(event.location, this.config.locationTitleReplace);
|
|
descCell.innerHTML = CalendarUtils.shorten(transformedTitle, this.config.maxLocationTitleLength, this.config.wrapLocationEvents, this.config.maxEventTitleLines);
|
|
locationRow.appendChild(descCell);
|
|
|
|
wrapper.appendChild(locationRow);
|
|
|
|
if (index >= startFade) {
|
|
currentFadeStep = index - startFade;
|
|
locationRow.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
return wrapper;
|
|
},
|
|
|
|
/**
|
|
* Checks if this config contains the calendar url.
|
|
* @param {string} url The calendar url
|
|
* @returns {boolean} True if the calendar config contains the url, False otherwise
|
|
*/
|
|
hasCalendarURL: function (url) {
|
|
for (const calendar of this.config.calendars) {
|
|
if (calendar.url === url) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Creates the sorted list of all events.
|
|
* @param {boolean} limitNumberOfEntries Whether to filter returned events for display.
|
|
* @returns {object[]} Array with events.
|
|
*/
|
|
createEventList: function (limitNumberOfEntries) {
|
|
const ONE_SECOND = 1000; // 1,000 milliseconds
|
|
const ONE_MINUTE = ONE_SECOND * 60;
|
|
const ONE_HOUR = ONE_MINUTE * 60;
|
|
const ONE_DAY = ONE_HOUR * 24;
|
|
|
|
const now = new Date();
|
|
const today = moment().startOf("day");
|
|
const future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate();
|
|
let events = [];
|
|
|
|
for (const calendarUrl in this.calendarData) {
|
|
const calendar = this.calendarData[calendarUrl];
|
|
let remainingEntries = this.maximumEntriesForUrl(calendarUrl);
|
|
let maxPastDaysCompare = now - this.maximumPastDaysForUrl(calendarUrl) * ONE_DAY;
|
|
for (const e in calendar) {
|
|
const event = JSON.parse(JSON.stringify(calendar[e])); // clone object
|
|
|
|
if (this.config.hidePrivate && event.class === "PRIVATE") {
|
|
// do not add the current event, skip it
|
|
continue;
|
|
}
|
|
if (limitNumberOfEntries) {
|
|
if (event.endDate < maxPastDaysCompare) {
|
|
continue;
|
|
}
|
|
if (this.config.hideOngoing && event.startDate < now) {
|
|
continue;
|
|
}
|
|
if (this.config.hideDuplicates && this.listContainsEvent(events, event)) {
|
|
continue;
|
|
}
|
|
if (--remainingEntries < 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
event.url = calendarUrl;
|
|
event.today = event.startDate >= today && event.startDate < today + ONE_DAY;
|
|
event.dayBeforeYesterday = event.startDate >= today - ONE_DAY * 2 && event.startDate < today - ONE_DAY;
|
|
event.yesterday = event.startDate >= today - ONE_DAY && event.startDate < today;
|
|
event.tomorrow = !event.today && event.startDate >= today + ONE_DAY && event.startDate < today + 2 * ONE_DAY;
|
|
event.dayAfterTomorrow = !event.tomorrow && event.startDate >= today + ONE_DAY * 2 && event.startDate < today + 3 * ONE_DAY;
|
|
|
|
/* if sliceMultiDayEvents is set to true, multiday events (events exceeding at least one midnight) are sliced into days,
|
|
* otherwise, esp. in dateheaders mode it is not clear how long these events are.
|
|
*/
|
|
const maxCount = Math.ceil((event.endDate - 1 - moment(event.startDate, "x").endOf("day").format("x")) / ONE_DAY) + 1;
|
|
if (this.config.sliceMultiDayEvents && maxCount > 1) {
|
|
const splitEvents = [];
|
|
let midnight = moment(event.startDate, "x").clone().startOf("day").add(1, "day").format("x");
|
|
let count = 1;
|
|
while (event.endDate > midnight) {
|
|
const thisEvent = JSON.parse(JSON.stringify(event)); // clone object
|
|
thisEvent.today = thisEvent.startDate >= today && thisEvent.startDate < today + ONE_DAY;
|
|
thisEvent.tomorrow = !thisEvent.today && thisEvent.startDate >= today + ONE_DAY && thisEvent.startDate < today + 2 * ONE_DAY;
|
|
thisEvent.endDate = midnight;
|
|
thisEvent.title += ` (${count}/${maxCount})`;
|
|
splitEvents.push(thisEvent);
|
|
|
|
event.startDate = midnight;
|
|
count += 1;
|
|
midnight = moment(midnight, "x").add(1, "day").format("x"); // next day
|
|
}
|
|
// Last day
|
|
event.title += ` (${count}/${maxCount})`;
|
|
event.today += event.startDate >= today && event.startDate < today + ONE_DAY;
|
|
event.tomorrow = !event.today && event.startDate >= today + ONE_DAY && event.startDate < today + 2 * ONE_DAY;
|
|
splitEvents.push(event);
|
|
|
|
for (let splitEvent of splitEvents) {
|
|
if (splitEvent.endDate > now && splitEvent.endDate <= future) {
|
|
events.push(splitEvent);
|
|
}
|
|
}
|
|
} else {
|
|
events.push(event);
|
|
}
|
|
}
|
|
}
|
|
|
|
events.sort(function (a, b) {
|
|
return a.startDate - b.startDate;
|
|
});
|
|
|
|
if (!limitNumberOfEntries) {
|
|
return events;
|
|
}
|
|
|
|
// Limit the number of days displayed
|
|
// If limitDays is set > 0, limit display to that number of days
|
|
if (this.config.limitDays > 0) {
|
|
let newEvents = [];
|
|
let lastDate = today.clone().subtract(1, "days").format("YYYYMMDD");
|
|
let days = 0;
|
|
for (const ev of events) {
|
|
let eventDate = moment(ev.startDate, "x").format("YYYYMMDD");
|
|
// if date of event is later than lastdate
|
|
// check if we already are showing max unique days
|
|
if (eventDate > lastDate) {
|
|
// if the only entry in the first day is a full day event that day is not counted as unique
|
|
if (!this.config.limitDaysNeverSkip && newEvents.length === 1 && days === 1 && newEvents[0].fullDayEvent) {
|
|
days--;
|
|
}
|
|
days++;
|
|
if (days > this.config.limitDays) {
|
|
continue;
|
|
} else {
|
|
lastDate = eventDate;
|
|
}
|
|
}
|
|
newEvents.push(ev);
|
|
}
|
|
events = newEvents;
|
|
}
|
|
|
|
return events.slice(0, this.config.maximumEntries);
|
|
},
|
|
|
|
listContainsEvent: function (eventList, event) {
|
|
for (const evt of eventList) {
|
|
if (evt.title === event.title && parseInt(evt.startDate) === parseInt(event.startDate) && parseInt(evt.endDate) === parseInt(event.endDate)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Requests node helper to add calendar url.
|
|
* @param {string} url The calendar url to add
|
|
* @param {object} auth The authentication method and credentials
|
|
* @param {object} calendarConfig The config of the specific calendar
|
|
*/
|
|
addCalendar: function (url, auth, calendarConfig) {
|
|
this.sendSocketNotification("ADD_CALENDAR", {
|
|
id: this.identifier,
|
|
url: url,
|
|
excludedEvents: calendarConfig.excludedEvents || this.config.excludedEvents,
|
|
maximumEntries: calendarConfig.maximumEntries || this.config.maximumEntries,
|
|
maximumNumberOfDays: calendarConfig.maximumNumberOfDays || this.config.maximumNumberOfDays,
|
|
pastDaysCount: calendarConfig.pastDaysCount || this.config.pastDaysCount,
|
|
fetchInterval: calendarConfig.fetchInterval || this.config.fetchInterval,
|
|
symbolClass: calendarConfig.symbolClass,
|
|
titleClass: calendarConfig.titleClass,
|
|
timeClass: calendarConfig.timeClass,
|
|
auth: auth,
|
|
broadcastPastEvents: calendarConfig.broadcastPastEvents || this.config.broadcastPastEvents,
|
|
selfSignedCert: calendarConfig.selfSignedCert || this.config.selfSignedCert
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Retrieves the symbols for a specific event.
|
|
* @param {object} event Event to look for.
|
|
* @returns {string[]} The symbols
|
|
*/
|
|
symbolsForEvent: function (event) {
|
|
let symbols = this.getCalendarPropertyAsArray(event.url, "symbol", this.config.defaultSymbol);
|
|
|
|
if (event.recurringEvent === true && this.hasCalendarProperty(event.url, "recurringSymbol")) {
|
|
symbols = this.mergeUnique(this.getCalendarPropertyAsArray(event.url, "recurringSymbol", this.config.defaultSymbol), symbols);
|
|
}
|
|
|
|
if (event.fullDayEvent === true && this.hasCalendarProperty(event.url, "fullDaySymbol")) {
|
|
symbols = this.mergeUnique(this.getCalendarPropertyAsArray(event.url, "fullDaySymbol", this.config.defaultSymbol), symbols);
|
|
}
|
|
|
|
// If custom symbol is set, replace event symbol
|
|
for (let ev of this.config.customEvents) {
|
|
if (typeof ev.symbol !== "undefined" && ev.symbol !== "") {
|
|
let needle = new RegExp(ev.keyword, "gi");
|
|
if (needle.test(event.title)) {
|
|
// Get the default prefix for this class name and add to the custom symbol provided
|
|
const className = this.getCalendarProperty(event.url, "symbolClassName", this.config.defaultSymbolClassName);
|
|
symbols[0] = className + ev.symbol;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return symbols;
|
|
},
|
|
|
|
mergeUnique: function (arr1, arr2) {
|
|
return arr1.concat(
|
|
arr2.filter(function (item) {
|
|
return arr1.indexOf(item) === -1;
|
|
})
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Retrieves the symbolClass for a specific calendar url.
|
|
* @param {string} url The calendar url
|
|
* @returns {string} The class to be used for the symbols of the calendar
|
|
*/
|
|
symbolClassForUrl: function (url) {
|
|
return this.getCalendarProperty(url, "symbolClass", "");
|
|
},
|
|
|
|
/**
|
|
* Retrieves the titleClass for a specific calendar url.
|
|
* @param {string} url The calendar url
|
|
* @returns {string} The class to be used for the title of the calendar
|
|
*/
|
|
titleClassForUrl: function (url) {
|
|
return this.getCalendarProperty(url, "titleClass", "");
|
|
},
|
|
|
|
/**
|
|
* Retrieves the timeClass for a specific calendar url.
|
|
* @param {string} url The calendar url
|
|
* @returns {string} The class to be used for the time of the calendar
|
|
*/
|
|
timeClassForUrl: function (url) {
|
|
return this.getCalendarProperty(url, "timeClass", "");
|
|
},
|
|
|
|
/**
|
|
* Retrieves the calendar name for a specific calendar url.
|
|
* @param {string} url The calendar url
|
|
* @returns {string} The name of the calendar
|
|
*/
|
|
calendarNameForUrl: function (url) {
|
|
return this.getCalendarProperty(url, "name", "");
|
|
},
|
|
|
|
/**
|
|
* Retrieves the color for a specific calendar url.
|
|
* @param {string} url The calendar url
|
|
* @param {boolean} isBg Determines if we fetch the bgColor or not
|
|
* @returns {string} The color
|
|
*/
|
|
colorForUrl: function (url, isBg) {
|
|
return this.getCalendarProperty(url, isBg ? "bgColor" : "color", "#fff");
|
|
},
|
|
|
|
/**
|
|
* Retrieves the count title for a specific calendar url.
|
|
* @param {string} url The calendar url
|
|
* @returns {string} The title
|
|
*/
|
|
countTitleForUrl: function (url) {
|
|
return this.getCalendarProperty(url, "repeatingCountTitle", this.config.defaultRepeatingCountTitle);
|
|
},
|
|
|
|
/**
|
|
* Retrieves the maximum entry count for a specific calendar url.
|
|
* @param {string} url The calendar url
|
|
* @returns {number} The maximum entry count
|
|
*/
|
|
maximumEntriesForUrl: function (url) {
|
|
return this.getCalendarProperty(url, "maximumEntries", this.config.maximumEntries);
|
|
},
|
|
|
|
/**
|
|
* Retrieves the maximum count of past days which events of should be displayed for a specific calendar url.
|
|
* @param {string} url The calendar url
|
|
* @returns {number} The maximum past days count
|
|
*/
|
|
maximumPastDaysForUrl: function (url) {
|
|
return this.getCalendarProperty(url, "pastDaysCount", this.config.pastDaysCount);
|
|
},
|
|
|
|
/**
|
|
* Helper method to retrieve the property for a specific calendar url.
|
|
* @param {string} url The calendar url
|
|
* @param {string} property The property to look for
|
|
* @param {string} defaultValue The value if the property is not found
|
|
* @returns {*} The property
|
|
*/
|
|
getCalendarProperty: function (url, property, defaultValue) {
|
|
for (const calendar of this.config.calendars) {
|
|
if (calendar.url === url && calendar.hasOwnProperty(property)) {
|
|
return calendar[property];
|
|
}
|
|
}
|
|
|
|
return defaultValue;
|
|
},
|
|
|
|
getCalendarPropertyAsArray: function (url, property, defaultValue) {
|
|
let p = this.getCalendarProperty(url, property, defaultValue);
|
|
if (property === "symbol" || property === "recurringSymbol" || property === "fullDaySymbol") {
|
|
const className = this.getCalendarProperty(url, "symbolClassName", this.config.defaultSymbolClassName);
|
|
p = className + p;
|
|
}
|
|
|
|
if (!(p instanceof Array)) p = [p];
|
|
return p;
|
|
},
|
|
|
|
hasCalendarProperty: function (url, property) {
|
|
return !!this.getCalendarProperty(url, property, undefined);
|
|
},
|
|
|
|
/**
|
|
* Broadcasts the events to all other modules for reuse.
|
|
* The all events available in one array, sorted on startdate.
|
|
*/
|
|
broadcastEvents: function () {
|
|
const eventList = this.createEventList(false);
|
|
for (const event of eventList) {
|
|
event.symbol = this.symbolsForEvent(event);
|
|
event.calendarName = this.calendarNameForUrl(event.url);
|
|
event.color = this.colorForUrl(event.url, false);
|
|
delete event.url;
|
|
}
|
|
|
|
this.sendNotification("CALENDAR_EVENTS", eventList);
|
|
}
|
|
});
|