From 6578a8c44eb12703ba063aa5341b7f6adc106ddb Mon Sep 17 00:00:00 2001 From: Leonardo Merza Date: Wed, 6 Feb 2019 09:44:37 -0500 Subject: [PATCH] rewrote card for version 2.0.0 --- README.md | 21 +- calendar-card.js | 565 ++++++++++++++++++++------------------------ custom_updater.json | 4 +- 3 files changed, 265 insertions(+), 325 deletions(-) diff --git a/README.md b/README.md index 7ffcac8..f9eabce 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,23 @@

+ +

Upgrading from version 1.x.x to 2.x.x

+ +Version 2.x.x introduced the following breaking changes: +* eventColor is no longer supported +* progressBar is no longer supported +* explicit adding moment.js to YAML config is no longer needed +* Card was convertered from a module to js (see new YAML example below) +

Features

-* show the next 5 events on your Google Calendar (default set by home assistant) +* Show the next 5 events on your Google Calendar (default set by home assistant) * Set custom time format for each event -* click on event to open in your Google calendar app +* Click on event to open in your Google calendar app * Integrate multiple calendars * Update notifications via custom_updater -* Show event color +* Click on event location to open maps app

Track Updates

@@ -35,11 +44,9 @@ You should have setup Google calendar integration or Caldav integration in HomeA | ---- | ---- | ------- | ----------- | type | string | **Required** | `custom:calendar-card` | title | string | **Optional** | `Calendar` Header shown at top of card -| showProgressBar | boolean | **Optional** | `true` Option to show the progress bar | numberOfDays | number | **Optional** | `7` Number of days to display from calendars | entities | object | **Required** | List of calendars to display | timeFormat | string | **Optional** | `HH:mm` Format to show event time (see [here](https://momentjs.com/docs/#/displaying/format/) for options) -| showColors | boolean | **Optional** | `false` Add event color marker to event summary

Configuration

Go to your config directory and create a www folder. Inside the www run @@ -52,10 +59,8 @@ In your ui-lovelace.yaml ``` resources: - - url: https://unpkg.com/moment@2.23.0/moment.js + - url: /local/calendar-card/calendar-card.js?v=2.0.0 type: js - - url: /local/calendar-card/calendar-card.js?v=1.2.2 - type: module ``` Add the custom card to views: diff --git a/calendar-card.js b/calendar-card.js index 45ceb2e..77acdba 100644 --- a/calendar-card.js +++ b/calendar-card.js @@ -1,113 +1,157 @@ -/** - * - */ -class CalendarCard extends HTMLElement { - - +var LitElement = LitElement || Object.getPrototypeOf(customElements.get("hui-error-entity-row")); +var html = LitElement.prototype.html; + +class CalendarCard extends LitElement { + + static get properties() { + return { + hass: Object, + config: Object, + content: Object + }; + } + + constructor() { + super(); + + this.content = html``; + this.isSomethingChanged = true; + this.events; + this.lastUpdate; + this.isUpdating = false; + + this.momentSrc = 'https://unpkg.com/moment@2.23.0/moment.js'; + } + /** - * called by hass - creates card, sets up any conmfig settings, and generates card - * @param {[type]} hass [description] - * @return {[type]} [description] + * merge the user configuration with default configuration + * @param {[type]} config */ - set hass(hass) { - - // if we don't have the card yet then create it - if (!this.content) { - const card = document.createElement('ha-card'); - card.header = this.config.title; - this.content = document.createElement('div'); - this.content.style.padding = '0 16px 10px'; - card.appendChild(this.content); - this.appendChild(card); - moment.locale(hass.language); + setConfig(config) { + if (!config.entities) { + throw new Error('You need to define at least one calendar entity via entities'); } - // save an instance of hass for later - this._hass = hass; - - // save css rules - this.cssRules = ` - `; + } - // update card with calendars - this - .getAllEvents(this.config.entities) - .then(events => this.updateHtmlIfNecessary(events)) - .catch(error => console.log('error', error)); + updated(){ + this.isSomethingChanged = false; } + render(){ + (async () => { + try { + + // since this is async then we need to know + // when we are updating outisde the LitElement hooks + if (this.isUpdating) return; + this.isUpdating = true; + + await this.addScript(); + + const events = await this.getAllEvents(this.config.entities); + + if (!this.isSomethingChanged) { + this.isUpdating = false; + return; + } + + await this.updateCard(events); + this.isUpdating = false; + + } catch(e){ + console.log(e); + } + })(); + + return this.content; + } + /** - * [getAllEvents description] - * @param {[type]} entities [description] - * @return {[type]} [description] + * gets all events for all calendars added to this card's config + * @param {CalendarEntity[]} entities + * @return {Promise>} */ async getAllEvents(entities) { // don't update if it's only been 15 min if(this.lastUpdate && moment().diff(this.lastUpdate, 'minutes') <= 15) { + console.log('testtt'); return this.events; } @@ -118,78 +162,32 @@ class CalendarCard extends HTMLElement { const end = today.add(this.config.numberOfDays, 'days').format(dateFormat); // generate urls for calendars and get each calendar data - const urls = this.createCalendarUrls(entities); - const events = await this.getAllUrls(urls); - - // show progress bar if turned on - if (this.config.showProgressBar && events.length > 0 && moment().format('DD') === moment(events[0].startDateTime).format('DD')) { - let now = {startDateTime: moment().format(), type: 'now'} - events.push(now); - } + const urls = entities.map(entity => `calendars/${entity}?start=${start}Z&end=${end}Z`); + let allResults = await this.getAllUrls(urls); + // convert each calendar object to a UI event + let events = [].concat.apply([], allResults).map(event => new CalendarEvent(event)); + // sort events by date starting with soonest events.sort((a, b) => new Date(a.startDateTime) - new Date(b.startDateTime)); // see if anything changed since last time, save events, and update last time we updated - const isSomethingChanged = this.isSomethingChanged(events); + console.log(events, this.events); + this.isSomethingChanged = JSON.stringify(events) !== JSON.stringify(this.events); this.events = events; this.lastUpdate = moment(); - return { events, isSomethingChanged }; - } - - /** - * generate calendar urls to get calendars - * @param {Array} entities - * @return {Array} - */ - createCalendarUrls(entities){ - - // create url params - let start = new Date(); - start = this.getFormattedDate(start); - - let end = new Date(); - end = this.addDays(end, this.config.numberOfDays); - end = this.getFormattedDate(end); - // generate urls for calendars and get each calendar data - return entities.map(entity => `calendars/${entity}?start=${start}&end=${end}`); - } - - /** - * get date in YYYY-MM-DDTHH:MM:SST format - * @param {Date} date the date object to format - * @return {string} - */ - getFormattedDate(date){ - const month = ( '0' + (date.getMonth()+1) ).slice(-2); - const day = ( '0' + date.getDate() ).slice(-2); - const year = date.getFullYear(); - return `${year}-${month}-${day}T00:00:00Z`; - } - - /** - * [addDays description] - * @param {Date} date the date object - * @param {number} days number of days to add - * @return {Date} new date object with days added - */ - addDays(date, days) { - let newDate = new Date(date.valueOf()); - newDate.setDate(newDate.getDate() + days); - return newDate; + return events; } /** * given a list of urls get the data from them * @param {Array} urls - * @return {Array} + * @return {Promise>} */ async getAllUrls(urls) { try { - const allResults = await Promise.all(urls.map(url => this._hass.callApi('get', url))); - return [].concat.apply([], allResults).map(event => new CalendarEvent(event)); - + return await Promise.all(urls.map(url => this.__hass.callApi('get', url))); } catch (error) { throw error; } @@ -197,205 +195,151 @@ class CalendarCard extends HTMLElement { /** * updates the entire card if we need to - * @param {[type]} eventList [description] - * @return {[type]} [description] - */ - updateHtmlIfNecessary(eventList) { - if(!eventList.isSomethingChanged) return; - - // save CSS rules then group events by day - this.content.innerHTML = this.cssRules; - const events = eventList.events; - const groupedEventsPerDay = this.groupBy(events, event => moment(event.startDateTime).format('YYYY-MM-DD')); - - // for each group event create a UI 'day' - groupedEventsPerDay.forEach((events, day) => { - const eventStateCardContentElement = document.createElement('div'); - eventStateCardContentElement.classList.add('day-wrapper'); - eventStateCardContentElement.innerHTML = this.getDayHtml(day, events); - this.content.append(eventStateCardContentElement); - }); - } - - /** - * generates the HTML for a single day - * @param {[type]} day [description] - * @param {[type]} events [description] - * @return {[type]} [description] + * @param {Array} eventList + * @return {TemplateResult} */ - getDayHtml(day, events) { - const className = moment().format('DD') === moment(day).format('DD') ? 'date now' : 'date'; - let momentDay = moment(day); - - return ` -
-
-
${momentDay.format('DD')}
-
${momentDay.format('ddd')}
-
-
- ${events.map(event => this.getEventHtml(event)).join('')} -
-
`; - } - - /** - * generate HTML for a single event - * @param {[type]} event [description] - * @return {[type]} [description] - */ - getEventHtml(event) { - if(event.type) return ''; - - return ` -
-
-
-
- ${this.getTitleHtml(event)} -
- ${this.getLocationHtml(event)} -
+ updateCard(eventList) { + const groupedEventsByDay = this.groupEventsByDay(eventList); + + const calendar = groupedEventsByDay.reduce((htmlTemplate, eventDay) => { + const momentDay = moment(eventDay.day); + + // for each event in a day create template for that event + const eventsTemplate = eventDay.events.map((event, index) => { + return html` + + +
${index === 0 ? momentDay.format('DD') : ''}
+
${index === 0 ? momentDay.format('ddd') : ''}
+ + +
this.getLinkHtml(event)}>${event.title}
${this.getTimeHtml(event)}
-
-
`; + + + ${this.getLocationHtml(event)} + + + `; + }); + + // add day template + htmlTemplate = html` + ${htmlTemplate} + ${eventsTemplate} + `; + + return htmlTemplate; + }, html``); + + + // create overall card + this.content = html` + +
+ ${this.config.title} +
+ + + ${calendar} + +
+
+ `; } /** - * gets the ebent title with a colored marker if user wants - * @return {[type]} [description] + * group events by the day it's on + * @param {Array} events + * @return {Array} */ - getTitleHtml(event){ - return this.config.showColors ? `● ${event.title}` : `${event.title}`; + groupEventsByDay(events) { + const groupedEvents = []; + + events.forEach(event => { + const day = moment(event.startDateTime).format('YYYY-MM-DD'); + const matchingDateIndex = groupedEvents.findIndex(events => events.day === day) + + if (matchingDateIndex > -1) { + groupedEvents[matchingDateIndex].events.push(event); + } else { + groupedEvents.push({ day, events: [event] }); + } + }); + + return groupedEvents; } /** - * generates HTML for opening an event - * @param {*} event + * opens a calendar event in a new tab if has link + * @param {CalendarEvent} event */ getLinkHtml(event){ - return event.htmlLink ? `onClick="(function(){window.open('${event.htmlLink}');return false;})();return false;"` : ''; + event.htmlLink && window.open(event.htmlLink); } /** * generates HTML for showing an event times - * @param {*} event + * @param {CalendarEvent} event */ getTimeHtml(event){ - if (event.isFullDayEvent) return 'All day' + if (event.isFullDayEvent) return html`All day`; const start = moment(event.startDateTime).format(this.config.timeFormat); const end = moment(event.endDateTime).format(this.config.timeFormat); - return `${start} - ${end}`; + return html`${start} - ${end}`; } /** * generate the html for showing an event location - * @param {*} event + * @param {CalendarEvent} event */ getLocationHtml(event){ - let locationHtml = ``; - - if (event.location) { - locationHtml += ` -
-  ` - } - - if (event.location && event.locationAddress) { - locationHtml += ` - - ${event.location} - -
`; - - } else if (event.location) { - locationHtml += `` - } - - return locationHtml; - } - - /** - * merge the user configuration with default configuration - * @param {[type]} config [description] - */ - setConfig(config) { - if (!config.entities) { - throw new Error('You need to define at least one calendar entity via entities'); - } - - this.config = { - title: 'Calendar', - showProgressBar: true, - numberOfDays: 7, - showColors: false, - timeFormat: 'HH:mm', - ...config - }; - } - - /** - * get the size of the card - * @return {[type]} [description] - */ - getCardSize() { - return 3; - } - - /** - * did any event change since the last time we checked? - * @param {[type]} events [description] - * @return {Boolean} [description] - */ - isSomethingChanged(events) { - let isSomethingChanged = JSON.stringify(events) !== JSON.stringify(this.events); - return isSomethingChanged; - } - - /** - * ddep clone a js object - * @param {[type]} obj [description] - * @return {[type]} [description] - */ - deepClone(obj) { - return JSON.parse(JSON.stringify(obj)); + if (!event.location || !event.locationAddress) + return html``; + + return html` + +   + + `; } /** - * group evbents by a givenkey - * @param {[type]} list [description] - * @param {[type]} keyGetter [description] - * @return {[type]} [description] + * adds the moment.js script + * @return {Promise} */ - groupBy(list, keyGetter) { - const map = new Map(); - - list.forEach(item => { - const key = keyGetter(item); - const collection = map.get(key); - - if (!collection) { - map.set(key, [item]); - } else { - collection.push(item); - } + async addScript() { + return new Promise(resolve => { + if(window.moment) return resolve(); + + const s = document.createElement('script'); + s.setAttribute('src', this.momentSrc); + s.onload = () => { + moment.locale(this.hass.language); + return resolve(); + }; + + document.body.appendChild(s); }); - - return map; } } +customElements.define('calendar-card', CalendarCard); + + /** - * Creaates an generalized Calendar Event to use when creating the calendar card + * Creates an generalized Calendar Event to use when creating the calendar card * There can be Google Events and CalDav Events. This class normalizes those */ class CalendarEvent { - + /** - * [constructor description] - * @param {[type]} calendarEvent [description] - * @return {[type]} [description] + * @param {Object} calendarEvent */ constructor(calendarEvent) { this.calendarEvent = calendarEvent; @@ -403,11 +347,11 @@ class CalendarEvent { /** * get the start time for an event - * @return {[type]} [description] + * @return {String} */ get startDateTime() { if (this.calendarEvent.start.date) { - let dateTime = moment(this.calendarEvent.start.date); + const dateTime = moment(this.calendarEvent.start.date); return dateTime.toISOString(); } @@ -416,7 +360,7 @@ class CalendarEvent { /** * get the end time for an event - * @return {[type]} [description] + * @return {String} */ get endDateTime() { return this.calendarEvent.end && this.calendarEvent.end.dateTime || this.calendarEvent.end || ''; @@ -424,23 +368,23 @@ class CalendarEvent { /** * get the URL for an event - * @return {[type]} [description] + * @return {String} */ - get htmlLink(){ - return this.calendarEvent.htmlLink; + get htmlLink() { + return this.calendarEvent.htmlLink || ''; } /** * get the title for an event - * @return {[type]} [description] + * @return {String} */ get title() { - return this.calendarEvent.summary || this.calendarEvent.title; + return this.calendarEvent.summary || this.calendarEvent.title || ''; } /** * get the description for an event - * @return {[type]} [description] + * @return {String} */ get description() { return this.calendarEvent.description; @@ -448,34 +392,30 @@ class CalendarEvent { /** * parse location for an event - * @return {[type]} [description] + * @return {String} */ get location() { - if(this.calendarEvent.location) { - return this.calendarEvent.location.split(',')[0] - } - - return undefined; + if (!this.calendarEvent.location) return ''; + return this.calendarEvent.location.split(',')[0] || ''; } /** * get location address for an event - * @return {[type]} [description] + * @return {String} */ get locationAddress() { - if(this.calendarEvent.location) { - let address = this.calendarEvent.location.substring(this.calendarEvent.location.indexOf(',') + 1); - return address.split(' ').join('+'); - } - return undefined; + if (!this.calendarEvent.location) return ''; + + let address = this.calendarEvent.location.substring(this.calendarEvent.location.indexOf(',') + 1); + return address.split(' ').join('+'); } /** * is the event a full day event? - * @return {Boolean} [description] + * @return {Boolean} */ get isFullDayEvent() { - if (this.calendarEvent.start && this.calendarEvent.start.date){ + if (this.calendarEvent.start && this.calendarEvent.start.date) { return this.calendarEvent.start.date; } @@ -484,9 +424,4 @@ class CalendarEvent { let diffInHours = end.diff(start, 'hours'); return diffInHours >= 24; } -} - -/** - * add card definition to hass - */ -customElements.define('calendar-card', CalendarCard); \ No newline at end of file +} \ No newline at end of file diff --git a/custom_updater.json b/custom_updater.json index 6f76a61..af6356b 100644 --- a/custom_updater.json +++ b/custom_updater.json @@ -1,7 +1,7 @@ { "calendar-card": { - "updated_at": "2018-12-26", - "version": "1.2.2", + "updated_at": "2019-02-06", + "version": "2.0.0", "remote_location": "https://raw.githubusercontent.com/ljmerza/calendar-card/master/calendar-card.js", "visit_repo": "https://github.com/ljmerza/calendar-card", "changelog": "https://github.com/ljmerza/calendar-card/releases/latest"