Software zum Installieren eines Smart-Mirror Frameworks , zum Nutzen von hochschulrelevanten Informationen, auf einem Raspberry-Pi.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

calendar.js 27KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846
  1. /* global cloneObject */
  2. /* Magic Mirror
  3. * Module: Calendar
  4. *
  5. * By Michael Teeuw https://michaelteeuw.nl
  6. * MIT Licensed.
  7. */
  8. Module.register("calendar", {
  9. // Define module defaults
  10. defaults: {
  11. maximumEntries: 10, // Total Maximum Entries
  12. maximumNumberOfDays: 365,
  13. limitDays: 0, // Limit the number of days shown, 0 = no limit
  14. displaySymbol: true,
  15. defaultSymbol: "calendar", // Fontawesome Symbol see https://fontawesome.com/cheatsheet?from=io
  16. showLocation: false,
  17. displayRepeatingCountTitle: false,
  18. defaultRepeatingCountTitle: "",
  19. maxTitleLength: 25,
  20. maxLocationTitleLength: 25,
  21. wrapEvents: false, // wrap events to multiple lines breaking at maxTitleLength
  22. wrapLocationEvents: false,
  23. maxTitleLines: 3,
  24. maxEventTitleLines: 3,
  25. fetchInterval: 5 * 60 * 1000, // Update every 5 minutes.
  26. animationSpeed: 2000,
  27. fade: true,
  28. urgency: 7,
  29. timeFormat: "relative",
  30. dateFormat: "MMM Do",
  31. dateEndFormat: "LT",
  32. fullDayEventDateFormat: "MMM Do",
  33. showEnd: false,
  34. getRelative: 6,
  35. fadePoint: 0.25, // Start on 1/4th of the list.
  36. hidePrivate: false,
  37. hideOngoing: false,
  38. hideTime: false,
  39. colored: false,
  40. coloredSymbolOnly: false,
  41. customEvents: [], // Array of {keyword: "", symbol: "", color: ""} where Keyword is a regexp and symbol/color are to be applied for matched
  42. tableClass: "small",
  43. calendars: [
  44. {
  45. symbol: "calendar",
  46. url: "https://www.calendarlabs.com/templates/ical/US-Holidays.ics"
  47. }
  48. ],
  49. titleReplace: {
  50. "De verjaardag van ": "",
  51. "'s birthday": ""
  52. },
  53. locationTitleReplace: {
  54. "street ": ""
  55. },
  56. broadcastEvents: true,
  57. excludedEvents: [],
  58. sliceMultiDayEvents: false,
  59. broadcastPastEvents: false,
  60. nextDaysRelative: false,
  61. selfSignedCert: false
  62. },
  63. requiresVersion: "2.1.0",
  64. // Define required scripts.
  65. getStyles: function () {
  66. return ["calendar.css", "font-awesome.css"];
  67. },
  68. // Define required scripts.
  69. getScripts: function () {
  70. return ["moment.js"];
  71. },
  72. // Define required translations.
  73. getTranslations: function () {
  74. // The translations for the default modules are defined in the core translation files.
  75. // Therefore we can just return false. Otherwise we should have returned a dictionary.
  76. // If you're trying to build your own module including translations, check out the documentation.
  77. return false;
  78. },
  79. // Override start method.
  80. start: function () {
  81. Log.info("Starting module: " + this.name);
  82. // Set locale.
  83. moment.updateLocale(config.language, this.getLocaleSpecification(config.timeFormat));
  84. // clear data holder before start
  85. this.calendarData = {};
  86. // indicate no data available yet
  87. this.loaded = false;
  88. this.config.calendars.forEach((calendar) => {
  89. calendar.url = calendar.url.replace("webcal://", "http://");
  90. const calendarConfig = {
  91. maximumEntries: calendar.maximumEntries,
  92. maximumNumberOfDays: calendar.maximumNumberOfDays,
  93. broadcastPastEvents: calendar.broadcastPastEvents,
  94. selfSignedCert: calendar.selfSignedCert
  95. };
  96. if (calendar.symbolClass === "undefined" || calendar.symbolClass === null) {
  97. calendarConfig.symbolClass = "";
  98. }
  99. if (calendar.titleClass === "undefined" || calendar.titleClass === null) {
  100. calendarConfig.titleClass = "";
  101. }
  102. if (calendar.timeClass === "undefined" || calendar.timeClass === null) {
  103. calendarConfig.timeClass = "";
  104. }
  105. // we check user and password here for backwards compatibility with old configs
  106. if (calendar.user && calendar.pass) {
  107. Log.warn("Deprecation warning: Please update your calendar authentication configuration.");
  108. Log.warn("https://github.com/MichMich/MagicMirror/tree/v2.1.2/modules/default/calendar#calendar-authentication-options");
  109. calendar.auth = {
  110. user: calendar.user,
  111. pass: calendar.pass
  112. };
  113. }
  114. // tell helper to start a fetcher for this calendar
  115. // fetcher till cycle
  116. this.addCalendar(calendar.url, calendar.auth, calendarConfig);
  117. });
  118. },
  119. // Override socket notification handler.
  120. socketNotificationReceived: function (notification, payload) {
  121. if (this.identifier !== payload.id) {
  122. return;
  123. }
  124. if (notification === "CALENDAR_EVENTS") {
  125. if (this.hasCalendarURL(payload.url)) {
  126. this.calendarData[payload.url] = payload.events;
  127. this.error = null;
  128. this.loaded = true;
  129. if (this.config.broadcastEvents) {
  130. this.broadcastEvents();
  131. }
  132. }
  133. } else if (notification === "CALENDAR_ERROR") {
  134. let error_message = this.translate(payload.error_type);
  135. this.error = this.translate("MODULE_CONFIG_ERROR", { MODULE_NAME: this.name, ERROR: error_message });
  136. this.loaded = true;
  137. }
  138. this.updateDom(this.config.animationSpeed);
  139. },
  140. // Override dom generator.
  141. getDom: function () {
  142. // Define second, minute, hour, and day constants
  143. const oneSecond = 1000; // 1,000 milliseconds
  144. const oneMinute = oneSecond * 60;
  145. const oneHour = oneMinute * 60;
  146. const oneDay = oneHour * 24;
  147. const events = this.createEventList();
  148. const wrapper = document.createElement("table");
  149. wrapper.className = this.config.tableClass;
  150. if (this.error) {
  151. wrapper.innerHTML = this.error;
  152. wrapper.className = this.config.tableClass + " dimmed";
  153. return wrapper;
  154. }
  155. if (events.length === 0) {
  156. wrapper.innerHTML = this.loaded ? this.translate("EMPTY") : this.translate("LOADING");
  157. wrapper.className = this.config.tableClass + " dimmed";
  158. return wrapper;
  159. }
  160. let currentFadeStep = 0;
  161. let startFade;
  162. let fadeSteps;
  163. if (this.config.fade && this.config.fadePoint < 1) {
  164. if (this.config.fadePoint < 0) {
  165. this.config.fadePoint = 0;
  166. }
  167. startFade = events.length * this.config.fadePoint;
  168. fadeSteps = events.length - startFade;
  169. }
  170. let lastSeenDate = "";
  171. events.forEach((event, index) => {
  172. const dateAsString = moment(event.startDate, "x").format(this.config.dateFormat);
  173. if (this.config.timeFormat === "dateheaders") {
  174. if (lastSeenDate !== dateAsString) {
  175. const dateRow = document.createElement("tr");
  176. dateRow.className = "normal";
  177. const dateCell = document.createElement("td");
  178. dateCell.colSpan = "3";
  179. dateCell.innerHTML = dateAsString;
  180. dateCell.style.paddingTop = "10px";
  181. dateRow.appendChild(dateCell);
  182. wrapper.appendChild(dateRow);
  183. if (this.config.fade && index >= startFade) {
  184. //fading
  185. currentFadeStep = index - startFade;
  186. dateRow.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
  187. }
  188. lastSeenDate = dateAsString;
  189. }
  190. }
  191. const eventWrapper = document.createElement("tr");
  192. if (this.config.colored && !this.config.coloredSymbolOnly) {
  193. eventWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
  194. }
  195. eventWrapper.className = "normal event";
  196. const symbolWrapper = document.createElement("td");
  197. if (this.config.displaySymbol) {
  198. if (this.config.colored && this.config.coloredSymbolOnly) {
  199. symbolWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
  200. }
  201. const symbolClass = this.symbolClassForUrl(event.url);
  202. symbolWrapper.className = "symbol align-right " + symbolClass;
  203. const symbols = this.symbolsForEvent(event);
  204. // If symbols are displayed and custom symbol is set, replace event symbol
  205. if (this.config.displaySymbol && this.config.customEvents.length > 0) {
  206. for (let ev in this.config.customEvents) {
  207. if (typeof this.config.customEvents[ev].symbol !== "undefined" && this.config.customEvents[ev].symbol !== "") {
  208. let needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
  209. if (needle.test(event.title)) {
  210. symbols[0] = this.config.customEvents[ev].symbol;
  211. break;
  212. }
  213. }
  214. }
  215. }
  216. symbols.forEach((s, index) => {
  217. const symbol = document.createElement("span");
  218. symbol.className = "fa fa-fw fa-" + s;
  219. if (index > 0) {
  220. symbol.style.paddingLeft = "5px";
  221. }
  222. symbolWrapper.appendChild(symbol);
  223. });
  224. eventWrapper.appendChild(symbolWrapper);
  225. } else if (this.config.timeFormat === "dateheaders") {
  226. const blankCell = document.createElement("td");
  227. blankCell.innerHTML = "&nbsp;&nbsp;&nbsp;";
  228. eventWrapper.appendChild(blankCell);
  229. }
  230. const titleWrapper = document.createElement("td");
  231. let repeatingCountTitle = "";
  232. if (this.config.displayRepeatingCountTitle && event.firstYear !== undefined) {
  233. repeatingCountTitle = this.countTitleForUrl(event.url);
  234. if (repeatingCountTitle !== "") {
  235. const thisYear = new Date(parseInt(event.startDate)).getFullYear(),
  236. yearDiff = thisYear - event.firstYear;
  237. repeatingCountTitle = ", " + yearDiff + ". " + repeatingCountTitle;
  238. }
  239. }
  240. // Color events if custom color is specified
  241. if (this.config.customEvents.length > 0) {
  242. for (let ev in this.config.customEvents) {
  243. if (typeof this.config.customEvents[ev].color !== "undefined" && this.config.customEvents[ev].color !== "") {
  244. let needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
  245. if (needle.test(event.title)) {
  246. // Respect parameter ColoredSymbolOnly also for custom events
  247. if (!this.config.coloredSymbolOnly) {
  248. eventWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
  249. titleWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
  250. }
  251. if (this.config.displaySymbol) {
  252. symbolWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
  253. }
  254. break;
  255. }
  256. }
  257. }
  258. }
  259. titleWrapper.innerHTML = this.titleTransform(event.title, this.config.titleReplace, this.config.wrapEvents, this.config.maxTitleLength, this.config.maxTitleLines) + repeatingCountTitle;
  260. const titleClass = this.titleClassForUrl(event.url);
  261. if (!this.config.colored) {
  262. titleWrapper.className = "title bright " + titleClass;
  263. } else {
  264. titleWrapper.className = "title " + titleClass;
  265. }
  266. if (this.config.timeFormat === "dateheaders") {
  267. if (event.fullDayEvent) {
  268. titleWrapper.colSpan = "2";
  269. titleWrapper.classList.add("align-left");
  270. } else {
  271. const timeWrapper = document.createElement("td");
  272. timeWrapper.className = "time light align-left " + this.timeClassForUrl(event.url);
  273. timeWrapper.style.paddingLeft = "2px";
  274. timeWrapper.innerHTML = moment(event.startDate, "x").format("LT");
  275. eventWrapper.appendChild(timeWrapper);
  276. titleWrapper.classList.add("align-right");
  277. }
  278. eventWrapper.appendChild(titleWrapper);
  279. } else {
  280. const timeWrapper = document.createElement("td");
  281. eventWrapper.appendChild(titleWrapper);
  282. const now = new Date();
  283. if (this.config.timeFormat === "absolute") {
  284. // Use dateFormat
  285. timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
  286. // Add end time if showEnd
  287. if (this.config.showEnd) {
  288. timeWrapper.innerHTML += "-";
  289. timeWrapper.innerHTML += this.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
  290. }
  291. // For full day events we use the fullDayEventDateFormat
  292. if (event.fullDayEvent) {
  293. //subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day
  294. event.endDate -= oneSecond;
  295. timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
  296. }
  297. if (this.config.getRelative > 0 && event.startDate < now) {
  298. // Ongoing and getRelative is set
  299. timeWrapper.innerHTML = this.capFirst(
  300. this.translate("RUNNING", {
  301. fallback: this.translate("RUNNING") + " {timeUntilEnd}",
  302. timeUntilEnd: moment(event.endDate, "x").fromNow(true)
  303. })
  304. );
  305. } else if (this.config.urgency > 0 && event.startDate - now < this.config.urgency * oneDay) {
  306. // Within urgency days
  307. timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
  308. }
  309. if (event.fullDayEvent && this.config.nextDaysRelative) {
  310. // Full days events within the next two days
  311. if (event.today) {
  312. timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
  313. } else if (event.startDate - now < oneDay && event.startDate - now > 0) {
  314. timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
  315. } else if (event.startDate - now < 2 * oneDay && event.startDate - now > 0) {
  316. if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
  317. timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
  318. }
  319. }
  320. }
  321. } else {
  322. // Show relative times
  323. if (event.startDate >= now) {
  324. // Use relative time
  325. if (!this.config.hideTime) {
  326. timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar(null, { sameElse: this.config.dateFormat }));
  327. } else {
  328. timeWrapper.innerHTML = this.capFirst(
  329. moment(event.startDate, "x").calendar(null, {
  330. sameDay: "[" + this.translate("TODAY") + "]",
  331. nextDay: "[" + this.translate("TOMORROW") + "]",
  332. nextWeek: "dddd",
  333. sameElse: this.config.dateFormat
  334. })
  335. );
  336. }
  337. if (event.startDate - now < this.config.getRelative * oneHour) {
  338. // If event is within getRelative hours, display 'in xxx' time format or moment.fromNow()
  339. timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
  340. }
  341. } else {
  342. // Ongoing event
  343. timeWrapper.innerHTML = this.capFirst(
  344. this.translate("RUNNING", {
  345. fallback: this.translate("RUNNING") + " {timeUntilEnd}",
  346. timeUntilEnd: moment(event.endDate, "x").fromNow(true)
  347. })
  348. );
  349. }
  350. }
  351. timeWrapper.className = "time light " + this.timeClassForUrl(event.url);
  352. eventWrapper.appendChild(timeWrapper);
  353. }
  354. wrapper.appendChild(eventWrapper);
  355. // Create fade effect.
  356. if (index >= startFade) {
  357. currentFadeStep = index - startFade;
  358. eventWrapper.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
  359. }
  360. if (this.config.showLocation) {
  361. if (event.location !== false) {
  362. const locationRow = document.createElement("tr");
  363. locationRow.className = "normal xsmall light";
  364. if (this.config.displaySymbol) {
  365. const symbolCell = document.createElement("td");
  366. locationRow.appendChild(symbolCell);
  367. }
  368. const descCell = document.createElement("td");
  369. descCell.className = "location";
  370. descCell.colSpan = "2";
  371. descCell.innerHTML = this.titleTransform(event.location, this.config.locationTitleReplace, this.config.wrapLocationEvents, this.config.maxLocationTitleLength, this.config.maxEventTitleLines);
  372. locationRow.appendChild(descCell);
  373. wrapper.appendChild(locationRow);
  374. if (index >= startFade) {
  375. currentFadeStep = index - startFade;
  376. locationRow.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
  377. }
  378. }
  379. }
  380. });
  381. return wrapper;
  382. },
  383. /**
  384. * This function accepts a number (either 12 or 24) and returns a moment.js LocaleSpecification with the
  385. * corresponding timeformat to be used in the calendar display. If no number is given (or otherwise invalid input)
  386. * it will a localeSpecification object with the system locale time format.
  387. *
  388. * @param {number} timeFormat Specifies either 12 or 24 hour time format
  389. * @returns {moment.LocaleSpecification} formatted time
  390. */
  391. getLocaleSpecification: function (timeFormat) {
  392. switch (timeFormat) {
  393. case 12: {
  394. return { longDateFormat: { LT: "h:mm A" } };
  395. }
  396. case 24: {
  397. return { longDateFormat: { LT: "HH:mm" } };
  398. }
  399. default: {
  400. return { longDateFormat: { LT: moment.localeData().longDateFormat("LT") } };
  401. }
  402. }
  403. },
  404. /**
  405. * Checks if this config contains the calendar url.
  406. *
  407. * @param {string} url The calendar url
  408. * @returns {boolean} True if the calendar config contains the url, False otherwise
  409. */
  410. hasCalendarURL: function (url) {
  411. for (const calendar of this.config.calendars) {
  412. if (calendar.url === url) {
  413. return true;
  414. }
  415. }
  416. return false;
  417. },
  418. /**
  419. * Creates the sorted list of all events.
  420. *
  421. * @returns {object[]} Array with events.
  422. */
  423. createEventList: function () {
  424. const now = new Date();
  425. const today = moment().startOf("day");
  426. const future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate();
  427. let events = [];
  428. for (const calendarUrl in this.calendarData) {
  429. const calendar = this.calendarData[calendarUrl];
  430. for (const e in calendar) {
  431. const event = JSON.parse(JSON.stringify(calendar[e])); // clone object
  432. if (event.endDate < now) {
  433. continue;
  434. }
  435. if (this.config.hidePrivate) {
  436. if (event.class === "PRIVATE") {
  437. // do not add the current event, skip it
  438. continue;
  439. }
  440. }
  441. if (this.config.hideOngoing) {
  442. if (event.startDate < now) {
  443. continue;
  444. }
  445. }
  446. if (this.listContainsEvent(events, event)) {
  447. continue;
  448. }
  449. event.url = calendarUrl;
  450. event.today = event.startDate >= today && event.startDate < today + 24 * 60 * 60 * 1000;
  451. /* if sliceMultiDayEvents is set to true, multiday events (events exceeding at least one midnight) are sliced into days,
  452. * otherwise, esp. in dateheaders mode it is not clear how long these events are.
  453. */
  454. const maxCount = Math.ceil((event.endDate - 1 - moment(event.startDate, "x").endOf("day").format("x")) / (1000 * 60 * 60 * 24)) + 1;
  455. if (this.config.sliceMultiDayEvents && maxCount > 1) {
  456. const splitEvents = [];
  457. let midnight = moment(event.startDate, "x").clone().startOf("day").add(1, "day").format("x");
  458. let count = 1;
  459. while (event.endDate > midnight) {
  460. const thisEvent = JSON.parse(JSON.stringify(event)); // clone object
  461. thisEvent.today = thisEvent.startDate >= today && thisEvent.startDate < today + 24 * 60 * 60 * 1000;
  462. thisEvent.endDate = midnight;
  463. thisEvent.title += " (" + count + "/" + maxCount + ")";
  464. splitEvents.push(thisEvent);
  465. event.startDate = midnight;
  466. count += 1;
  467. midnight = moment(midnight, "x").add(1, "day").format("x"); // next day
  468. }
  469. // Last day
  470. event.title += " (" + count + "/" + maxCount + ")";
  471. splitEvents.push(event);
  472. for (let splitEvent of splitEvents) {
  473. if (splitEvent.endDate > now && splitEvent.endDate <= future) {
  474. events.push(splitEvent);
  475. }
  476. }
  477. } else {
  478. events.push(event);
  479. }
  480. }
  481. }
  482. events.sort(function (a, b) {
  483. return a.startDate - b.startDate;
  484. });
  485. // Limit the number of days displayed
  486. // If limitDays is set > 0, limit display to that number of days
  487. if (this.config.limitDays > 0) {
  488. let newEvents = [];
  489. let lastDate = today.clone().subtract(1, "days").format("YYYYMMDD");
  490. let days = 0;
  491. for (const ev of events) {
  492. let eventDate = moment(ev.startDate, "x").format("YYYYMMDD");
  493. // if date of event is later than lastdate
  494. // check if we already are showing max unique days
  495. if (eventDate > lastDate) {
  496. // if the only entry in the first day is a full day event that day is not counted as unique
  497. if (newEvents.length === 1 && days === 1 && newEvents[0].fullDayEvent) {
  498. days--;
  499. }
  500. days++;
  501. if (days > this.config.limitDays) {
  502. continue;
  503. } else {
  504. lastDate = eventDate;
  505. }
  506. }
  507. newEvents.push(ev);
  508. }
  509. events = newEvents;
  510. }
  511. return events.slice(0, this.config.maximumEntries);
  512. },
  513. listContainsEvent: function (eventList, event) {
  514. for (const evt of eventList) {
  515. if (evt.title === event.title && parseInt(evt.startDate) === parseInt(event.startDate)) {
  516. return true;
  517. }
  518. }
  519. return false;
  520. },
  521. /**
  522. * Requests node helper to add calendar url.
  523. *
  524. * @param {string} url The calendar url to add
  525. * @param {object} auth The authentication method and credentials
  526. * @param {object} calendarConfig The config of the specific calendar
  527. */
  528. addCalendar: function (url, auth, calendarConfig) {
  529. this.sendSocketNotification("ADD_CALENDAR", {
  530. id: this.identifier,
  531. url: url,
  532. excludedEvents: calendarConfig.excludedEvents || this.config.excludedEvents,
  533. maximumEntries: calendarConfig.maximumEntries || this.config.maximumEntries,
  534. maximumNumberOfDays: calendarConfig.maximumNumberOfDays || this.config.maximumNumberOfDays,
  535. fetchInterval: this.config.fetchInterval,
  536. symbolClass: calendarConfig.symbolClass,
  537. titleClass: calendarConfig.titleClass,
  538. timeClass: calendarConfig.timeClass,
  539. auth: auth,
  540. broadcastPastEvents: calendarConfig.broadcastPastEvents || this.config.broadcastPastEvents,
  541. selfSignedCert: calendarConfig.selfSignedCert || this.config.selfSignedCert
  542. });
  543. },
  544. /**
  545. * Retrieves the symbols for a specific event.
  546. *
  547. * @param {object} event Event to look for.
  548. * @returns {string[]} The symbols
  549. */
  550. symbolsForEvent: function (event) {
  551. let symbols = this.getCalendarPropertyAsArray(event.url, "symbol", this.config.defaultSymbol);
  552. if (event.recurringEvent === true && this.hasCalendarProperty(event.url, "recurringSymbol")) {
  553. symbols = this.mergeUnique(this.getCalendarPropertyAsArray(event.url, "recurringSymbol", this.config.defaultSymbol), symbols);
  554. }
  555. if (event.fullDayEvent === true && this.hasCalendarProperty(event.url, "fullDaySymbol")) {
  556. symbols = this.mergeUnique(this.getCalendarPropertyAsArray(event.url, "fullDaySymbol", this.config.defaultSymbol), symbols);
  557. }
  558. return symbols;
  559. },
  560. mergeUnique: function (arr1, arr2) {
  561. return arr1.concat(
  562. arr2.filter(function (item) {
  563. return arr1.indexOf(item) === -1;
  564. })
  565. );
  566. },
  567. /**
  568. * Retrieves the symbolClass for a specific calendar url.
  569. *
  570. * @param {string} url The calendar url
  571. * @returns {string} The class to be used for the symbols of the calendar
  572. */
  573. symbolClassForUrl: function (url) {
  574. return this.getCalendarProperty(url, "symbolClass", "");
  575. },
  576. /**
  577. * Retrieves the titleClass for a specific calendar url.
  578. *
  579. * @param {string} url The calendar url
  580. * @returns {string} The class to be used for the title of the calendar
  581. */
  582. titleClassForUrl: function (url) {
  583. return this.getCalendarProperty(url, "titleClass", "");
  584. },
  585. /**
  586. * Retrieves the timeClass for a specific calendar url.
  587. *
  588. * @param {string} url The calendar url
  589. * @returns {string} The class to be used for the time of the calendar
  590. */
  591. timeClassForUrl: function (url) {
  592. return this.getCalendarProperty(url, "timeClass", "");
  593. },
  594. /**
  595. * Retrieves the calendar name for a specific calendar url.
  596. *
  597. * @param {string} url The calendar url
  598. * @returns {string} The name of the calendar
  599. */
  600. calendarNameForUrl: function (url) {
  601. return this.getCalendarProperty(url, "name", "");
  602. },
  603. /**
  604. * Retrieves the color for a specific calendar url.
  605. *
  606. * @param {string} url The calendar url
  607. * @returns {string} The color
  608. */
  609. colorForUrl: function (url) {
  610. return this.getCalendarProperty(url, "color", "#fff");
  611. },
  612. /**
  613. * Retrieves the count title for a specific calendar url.
  614. *
  615. * @param {string} url The calendar url
  616. * @returns {string} The title
  617. */
  618. countTitleForUrl: function (url) {
  619. return this.getCalendarProperty(url, "repeatingCountTitle", this.config.defaultRepeatingCountTitle);
  620. },
  621. /**
  622. * Helper method to retrieve the property for a specific calendar url.
  623. *
  624. * @param {string} url The calendar url
  625. * @param {string} property The property to look for
  626. * @param {string} defaultValue The value if the property is not found
  627. * @returns {*} The property
  628. */
  629. getCalendarProperty: function (url, property, defaultValue) {
  630. for (const calendar of this.config.calendars) {
  631. if (calendar.url === url && calendar.hasOwnProperty(property)) {
  632. return calendar[property];
  633. }
  634. }
  635. return defaultValue;
  636. },
  637. getCalendarPropertyAsArray: function (url, property, defaultValue) {
  638. let p = this.getCalendarProperty(url, property, defaultValue);
  639. if (!(p instanceof Array)) p = [p];
  640. return p;
  641. },
  642. hasCalendarProperty: function (url, property) {
  643. return !!this.getCalendarProperty(url, property, undefined);
  644. },
  645. /**
  646. * Shortens a string if it's longer than maxLength and add a ellipsis to the end
  647. *
  648. * @param {string} string Text string to shorten
  649. * @param {number} maxLength The max length of the string
  650. * @param {boolean} wrapEvents Wrap the text after the line has reached maxLength
  651. * @param {number} maxTitleLines The max number of vertical lines before cutting event title
  652. * @returns {string} The shortened string
  653. */
  654. shorten: function (string, maxLength, wrapEvents, maxTitleLines) {
  655. if (typeof string !== "string") {
  656. return "";
  657. }
  658. if (wrapEvents === true) {
  659. const words = string.split(" ");
  660. let temp = "";
  661. let currentLine = "";
  662. let line = 0;
  663. for (let i = 0; i < words.length; i++) {
  664. const word = words[i];
  665. if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) {
  666. // max - 1 to account for a space
  667. currentLine += word + " ";
  668. } else {
  669. line++;
  670. if (line > maxTitleLines - 1) {
  671. if (i < words.length) {
  672. currentLine += "&hellip;";
  673. }
  674. break;
  675. }
  676. if (currentLine.length > 0) {
  677. temp += currentLine + "<br>" + word + " ";
  678. } else {
  679. temp += word + "<br>";
  680. }
  681. currentLine = "";
  682. }
  683. }
  684. return (temp + currentLine).trim();
  685. } else {
  686. if (maxLength && typeof maxLength === "number" && string.length > maxLength) {
  687. return string.trim().slice(0, maxLength) + "&hellip;";
  688. } else {
  689. return string.trim();
  690. }
  691. }
  692. },
  693. /**
  694. * Capitalize the first letter of a string
  695. *
  696. * @param {string} string The string to capitalize
  697. * @returns {string} The capitalized string
  698. */
  699. capFirst: function (string) {
  700. return string.charAt(0).toUpperCase() + string.slice(1);
  701. },
  702. /**
  703. * Transforms the title of an event for usage.
  704. * Replaces parts of the text as defined in config.titleReplace.
  705. * Shortens title based on config.maxTitleLength and config.wrapEvents
  706. *
  707. * @param {string} title The title to transform.
  708. * @param {object} titleReplace Pairs of strings to be replaced in the title
  709. * @param {boolean} wrapEvents Wrap the text after the line has reached maxLength
  710. * @param {number} maxTitleLength The max length of the string
  711. * @param {number} maxTitleLines The max number of vertical lines before cutting event title
  712. * @returns {string} The transformed title.
  713. */
  714. titleTransform: function (title, titleReplace, wrapEvents, maxTitleLength, maxTitleLines) {
  715. for (let needle in titleReplace) {
  716. const replacement = titleReplace[needle];
  717. const regParts = needle.match(/^\/(.+)\/([gim]*)$/);
  718. if (regParts) {
  719. // the parsed pattern is a regexp.
  720. needle = new RegExp(regParts[1], regParts[2]);
  721. }
  722. title = title.replace(needle, replacement);
  723. }
  724. title = this.shorten(title, maxTitleLength, wrapEvents, maxTitleLines);
  725. return title;
  726. },
  727. /**
  728. * Broadcasts the events to all other modules for reuse.
  729. * The all events available in one array, sorted on startdate.
  730. */
  731. broadcastEvents: function () {
  732. const eventList = [];
  733. for (const url in this.calendarData) {
  734. for (const ev of this.calendarData[url]) {
  735. const event = cloneObject(ev);
  736. event.symbol = this.symbolsForEvent(event);
  737. event.calendarName = this.calendarNameForUrl(url);
  738. event.color = this.colorForUrl(url);
  739. delete event.url;
  740. eventList.push(event);
  741. }
  742. }
  743. eventList.sort(function (a, b) {
  744. return a.startDate - b.startDate;
  745. });
  746. this.sendNotification("CALENDAR_EVENTS", eventList);
  747. }
  748. });