Calendar Panel: FullCalendar (#5742)

* WIP

* remove big calendar

* remove file

* Convert to lit

* More

* Ready for the public to see? prob not

* Fix types and imports

* Remove dependencies

* ignore the typing that hasnt been finished in Beta

* Convert paper to MWC

* Styling

* View list as name of view | MWC components version

* Updates action directive for ripple. MWC 14.1.0

* Update

* Updates

* Update height styling

* Toggle Button Group

* Adds Toggle group transition

* style updates

* Few fixes

* Fix Yarn lock from merge | height of celndar as parent

* Update package Json and yarn | remove unneeded pkg

* Remove mwc-list

* Search hass.states for calendars instead of api

* Move function to file in data | event fetch logic

* compute state name

* add ha button menu | refresh

* Remove Event ffetch logic

* copy pasta

* Types

* Fix for toggling

* Translations

* Update ha-button-toggle-group

* Update ha-button-toggle-group.ts

* Update ha-button-toggle-group.ts

* Change mobile view

* Locale in fullcalendar

* Comments

* ha-button-menu trigger slot

* Comments

* icon-x

* Update src/panels/calendar/ha-panel-calendar.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Update src/panels/calendar/ha-panel-calendar.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
Zack Arnett 2020-05-06 17:22:12 -04:00 committed by GitHub
parent 9630a58ea7
commit 71492d0467
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1330 additions and 924 deletions

View File

@ -18,10 +18,6 @@
"project": "./tsconfig.json"
},
"settings": {
"react": {
"pragma": "h",
"version": "15.0"
},
"import/resolver": {
"webpack": {
"config": "./webpack.config.js"
@ -88,13 +84,6 @@
"@typescript-eslint/no-unused-vars": 0,
"@typescript-eslint/explicit-function-return-type": 0
},
"plugins": [
"disable",
"import",
"react",
"lit",
"prettier",
"@typescript-eslint"
],
"plugins": ["disable", "import", "lit", "prettier", "@typescript-eslint"],
"processor": "disable/disable"
}

View File

@ -27,12 +27,6 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
],
// Only support the syntax, Webpack will handle it.
"@babel/syntax-dynamic-import",
[
"@babel/transform-react-jsx",
{
pragma: "h",
},
],
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-nullish-coalescing-operator",
[

View File

@ -80,7 +80,6 @@ gulp.task("copy-translations", (done) => {
gulp.task("copy-static", (done) => {
const staticDir = paths.static;
const staticPath = genStaticPath(paths.static);
// Basic static files
fs.copySync(polyPath("public"), paths.root);
@ -90,10 +89,6 @@ gulp.task("copy-static", (done) => {
copyMdiIcons(staticDir);
// Panel assets
copyFileDir(
npmPath("react-big-calendar/lib/css/react-big-calendar.css"),
staticPath("panels/calendar/")
);
copyMapPanel(staticDir);
done();
});

View File

@ -100,14 +100,6 @@ const createWebpackConfig = ({
].filter(Boolean),
resolve: {
extensions: [".ts", ".js", ".json"],
alias: {
react: "preact-compat",
"react-dom": "preact-compat",
// Not necessary unless you consume a module using `createClass`
"create-react-class": "preact-compat/lib/create-react-class",
// Not necessary unless you consume a module requiring `react-dom-factories`
"react-dom-factories": "preact-compat/lib/react-dom-factories",
},
},
output: {
filename: ({ chunk }) => {

View File

@ -24,14 +24,19 @@
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
"license": "Apache-2.0",
"dependencies": {
"@material/chips": "^5.0.0",
"@material/mwc-button": "^0.13.0",
"@material/mwc-checkbox": "^0.13.0",
"@material/mwc-dialog": "^0.13.0",
"@material/mwc-fab": "^0.13.0",
"@material/mwc-icon-button": "^0.13.0",
"@material/mwc-ripple": "^0.13.0",
"@material/mwc-switch": "^0.13.0",
"@fullcalendar/core": "5.0.0-beta.2",
"@fullcalendar/daygrid": "5.0.0-beta.2",
"@material/chips": "^6.0.0-canary.35a32aaea.0",
"@material/mwc-button": "0.14.1",
"@material/mwc-checkbox": "0.14.1",
"@material/mwc-dialog": "0.14.1",
"@material/mwc-fab": "0.14.1",
"@material/mwc-formfield": "0.14.1",
"@material/mwc-icon-button": "0.14.1",
"@material/mwc-list": "0.14.1",
"@material/mwc-menu": "0.14.1",
"@material/mwc-ripple": "0.14.1",
"@material/mwc-switch": "0.14.1",
"@mdi/js": "4.9.95",
"@mdi/svg": "4.9.95",
"@polymer/app-layout": "^3.0.2",
@ -102,9 +107,6 @@
"memoize-one": "^5.0.2",
"moment": "^2.24.0",
"node-vibrant": "^3.1.5",
"preact": "^8.4.2",
"preact-compat": "^3.18.4",
"react-big-calendar": "^0.20.4",
"regenerator-runtime": "^0.13.2",
"resize-observer": "^1.0.0",
"roboto-fontface": "^0.10.0",
@ -123,7 +125,6 @@
"@babel/plugin-proposal-object-rest-spread": "^7.9.5",
"@babel/plugin-proposal-optional-chaining": "^7.9.0",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-react-jsx": "^7.9.4",
"@babel/preset-env": "^7.9.5",
"@babel/preset-typescript": "^7.9.0",
"@types/chai": "^4.1.7",
@ -151,7 +152,6 @@
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-lit": "^1.2.0",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.19.0",
"eslint-plugin-wc": "^1.2.0",
"fs-extra": "^7.0.1",
"gulp": "^4.0.0",
@ -198,17 +198,19 @@
"@webcomponents/webcomponentsjs": "^2.2.10",
"@polymer/polymer": "3.1.0",
"lit-html": "^1.1.2",
"@material/button": "^5.0.0",
"@material/checkbox": "^5.0.0",
"@material/density": "^5.0.0",
"@material/dialog": "^5.0.0",
"@material/fab": "^5.0.0",
"@material/feature-targeting": "^5.0.0",
"@material/switch": "^5.0.0",
"@material/ripple": "^5.0.0",
"@material/dom": "^5.0.0",
"@material/touch-target": "^5.0.0",
"@material/theme": "^5.0.0"
"@material/animation": "6.0.0",
"@material/base": "6.0.0",
"@material/checkbox": "6.0.0",
"@material/density": "6.0.0",
"@material/dom": "6.0.0",
"@material/elevation": "6.0.0",
"@material/feature-targeting": "6.0.0",
"@material/ripple": "6.0.0",
"@material/rtl": "6.0.0",
"@material/shape": "6.0.0",
"@material/theme": "6.0.0",
"@material/touch-target": "6.0.0",
"@material/typography": "6.0.0"
},
"main": "src/home-assistant.js",
"husky": {

View File

@ -87,3 +87,66 @@ export const UNIT_F = "°F";
/** Entity ID of the default view. */
export const DEFAULT_VIEW_ENTITY_ID = "group.default_view";
/** HA Color Pallete. */
export const HA_COLOR_PALETTE = [
"ff0029",
"66a61e",
"377eb8",
"984ea3",
"00d2d5",
"ff7f00",
"af8d00",
"7f80cd",
"b3e900",
"c42e60",
"a65628",
"f781bf",
"8dd3c7",
"bebada",
"fb8072",
"80b1d3",
"fdb462",
"fccde5",
"bc80bd",
"ffed6f",
"c4eaff",
"cf8c00",
"1b9e77",
"d95f02",
"e7298a",
"e6ab02",
"a6761d",
"0097ff",
"00d067",
"f43600",
"4ba93b",
"5779bb",
"927acc",
"97ee3f",
"bf3947",
"9f5b00",
"f48758",
"8caed6",
"f2b94f",
"eff26e",
"e43872",
"d9b100",
"9d7a00",
"698cff",
"d9d9d9",
"00d27e",
"d06800",
"009f82",
"c49200",
"cbe8ff",
"fecddf",
"c27eb6",
"8cd2ce",
"c4b8d9",
"f883b0",
"a49100",
"f48800",
"27d0df",
"a04a9b",
];

View File

@ -0,0 +1,55 @@
import {
customElement,
html,
TemplateResult,
LitElement,
CSSResult,
css,
query,
} from "lit-element";
import "@material/mwc-button";
import "@material/mwc-menu";
import "@material/mwc-list/mwc-list-item";
import type { Menu } from "@material/mwc-menu";
import { haStyle } from "../resources/styles";
import "./ha-icon-button";
@customElement("ha-button-menu")
export class HaButtonMenu extends LitElement {
@query("mwc-menu") private _menu?: Menu;
protected render(): TemplateResult {
return html`
<div @click=${this._handleClick}>
<slot name="trigger"></slot>
</div>
<mwc-menu>
<slot></slot>
</mwc-menu>
`;
}
private _handleClick(): void {
this._menu!.anchor = this;
this._menu!.show();
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
:host {
position: relative;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-button-menu": HaButtonMenu;
}
}

View File

@ -0,0 +1,86 @@
import {
customElement,
html,
TemplateResult,
property,
LitElement,
CSSResult,
css,
} from "lit-element";
import "./ha-icon-button";
import { fireEvent } from "../common/dom/fire_event";
import type { ToggleButton } from "../types";
@customElement("ha-button-toggle-group")
export class HaButtonToggleGroup extends LitElement {
@property() public buttons!: ToggleButton[];
@property() public active?: string;
protected render(): TemplateResult {
return html`
<div>
${this.buttons.map(
(button) => html` <ha-icon-button
.label=${button.label}
.icon=${button.icon}
.value=${button.value}
?active=${this.active === button.value}
@click=${this._handleClick}
>
</ha-icon-button>`
)}
</div>
`;
}
private _handleClick(ev): void {
this.active = ev.target.value;
fireEvent(this, "value-changed", { value: this.active });
}
static get styles(): CSSResult {
return css`
div {
display: flex;
--mdc-icon-button-size: var(--button-toggle-size, 36px);
--mdc-icon-size: var(--button-toggle-icon-size, 20px);
}
ha-icon-button {
border: 1px solid var(--primary-color);
border-right-width: 0px;
position: relative;
}
ha-icon-button::before {
top: 0;
left: 0;
width: 100%;
height: 100%;
position: absolute;
background-color: currentColor;
opacity: 0;
pointer-events: none;
content: "";
transition: opacity 15ms linear, background-color 15ms linear;
}
ha-icon-button[active]::before {
opacity: var(--mdc-icon-button-ripple-opacity, 0.12);
}
ha-icon-button:first-child {
border-radius: 4px 0 0 4px;
}
ha-icon-button:last-child {
border-radius: 0 4px 4px 0;
border-right-width: 1px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-button-toggle-button": HaButtonToggleGroup;
}
}

View File

@ -50,8 +50,7 @@ export class HaIconButton extends LitElement {
--mdc-theme-on-primary: currentColor;
}
ha-icon {
display: inline-flex;
vertical-align: initial;
--ha-icon-display: inline;
}
`;
}

View File

@ -127,11 +127,6 @@ export class HaIcon extends LitElement {
static get styles(): CSSResult {
return css`
:host {
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
vertical-align: middle;
fill: currentcolor;
}
`;

View File

@ -27,7 +27,7 @@ export class HaSvgIcon extends LitElement {
static get styles(): CSSResult {
return css`
:host {
display: inline-flex;
display: var(--ha-icon-display, inline-flex);
align-items: center;
justify-content: center;
position: relative;

80
src/data/calendar.ts Normal file
View File

@ -0,0 +1,80 @@
import type { HomeAssistant, Calendar, CalendarEvent } from "../types";
import { computeDomain } from "../common/entity/compute_domain";
import { HA_COLOR_PALETTE } from "../common/const";
import { computeStateName } from "../common/entity/compute_state_name";
export const fetchCalendarEvents = async (
hass: HomeAssistant,
start: Date,
end: Date,
calendars: Calendar[]
): Promise<CalendarEvent[]> => {
const params = encodeURI(
`?start=${start.toISOString()}&end=${end.toISOString()}`
);
const calEvents: CalendarEvent[] = [];
const promises: Promise<any>[] = [];
calendars.forEach((cal) => {
promises.push(
hass.callApi<CalendarEvent[]>(
"GET",
`calendars/${cal.entity_id}${params}`
)
);
});
const results = await Promise.all(promises);
results.forEach((result, idx) => {
const cal = calendars[idx];
result.forEach((ev) => {
const eventStart = getCalendarDate(ev.start);
if (!eventStart) {
return;
}
const eventEnd = getCalendarDate(ev.end);
const event: CalendarEvent = {
start: eventStart,
end: eventEnd,
title: ev.summary,
summary: ev.summary,
backgroundColor: cal.backgroundColor,
borderColor: cal.backgroundColor,
calendar: cal.entity_id,
};
calEvents.push(event);
});
});
return calEvents;
};
const getCalendarDate = (dateObj: any): string | undefined => {
if (typeof dateObj === "string") {
return dateObj;
}
if (dateObj.dateTime) {
return dateObj.dateTime;
}
if (dateObj.date) {
return dateObj.date;
}
return undefined;
};
export const getCalendars = (hass: HomeAssistant): Calendar[] => {
return Object.keys(hass.states)
.filter((eid) => computeDomain(eid) === "calendar")
.sort()
.map((eid, idx) => ({
entity_id: eid,
name: computeStateName(hass.states[eid]),
backgroundColor: `#${HA_COLOR_PALETTE[idx % HA_COLOR_PALETTE.length]}`,
}));
};

View File

@ -1,69 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import moment from "moment";
// eslint-disable-next-line import/no-duplicates,import/no-extraneous-dependencies
import React from "react";
import BigCalendar from "react-big-calendar";
// eslint-disable-next-line import/no-duplicates,import/no-extraneous-dependencies
import { render } from "react-dom";
import { EventsMixin } from "../../mixins/events-mixin";
import "../../resources/ha-style";
BigCalendar.setLocalizer(BigCalendar.momentLocalizer(moment));
const DEFAULT_VIEW = "month";
class HaBigCalendar extends EventsMixin(PolymerElement) {
static get template() {
return html`
<link
rel="stylesheet"
href="/static/panels/calendar/react-big-calendar.css"
/>
<style>
div#root {
height: 100%;
width: 100%;
}
</style>
<div id="root"></div>
`;
}
static get properties() {
return {
events: {
type: Array,
observer: "_update",
},
};
}
_update(events) {
const allViews = BigCalendar.Views.values;
const BCElement = React.createElement(BigCalendar, {
events: events,
views: allViews,
popup: true,
onNavigate: (date, viewName) => this.fire("navigate", { date, viewName }),
onView: (viewName) => this.fire("view-changed", { viewName }),
eventPropGetter: this._setEventStyle,
defaultView: DEFAULT_VIEW,
defaultDate: new Date(),
});
render(BCElement, this.$.root);
}
_setEventStyle(event) {
// https://stackoverflow.com/questions/34587067/change-color-of-react-big-calendar-events
const newStyle = {};
if (event.color) {
newStyle.backgroundColor = event.color;
}
return { style: newStyle };
}
}
customElements.define("ha-big-calendar", HaBigCalendar);

View File

@ -0,0 +1,331 @@
import {
property,
PropertyValues,
LitElement,
CSSResult,
html,
css,
unsafeCSS,
TemplateResult,
} from "lit-element";
import { Calendar } from "@fullcalendar/core";
import dayGridPlugin from "@fullcalendar/daygrid";
// @ts-ignore
import fullcalendarStyle from "@fullcalendar/core/main.css";
// @ts-ignore
import daygridStyle from "@fullcalendar/daygrid/main.css";
import "@material/mwc-button";
import "../../components/ha-icon-button";
import "../../components/ha-button-toggle-group";
import type {
CalendarViewChanged,
CalendarEvent,
ToggleButton,
HomeAssistant,
} from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
import { haStyle } from "../../resources/styles";
declare global {
interface HASSDomEvents {
"view-changed": CalendarViewChanged;
}
}
const fullCalendarConfig = {
headerToolbar: false,
plugins: [dayGridPlugin],
initialView: "dayGridMonth",
dayMaxEventRows: true,
height: "parent",
};
const viewButtons: ToggleButton[] = [
{ label: "Month View", value: "dayGridMonth", icon: "hass:view-module" },
{ label: "Week View", value: "dayGridWeek", icon: "hass:view-week" },
{ label: "Day View", value: "dayGridDay", icon: "hass:view-day" },
];
class HAFullCalendar extends LitElement {
public hass!: HomeAssistant;
@property() public events: CalendarEvent[] = [];
@property({ type: Boolean, reflect: true })
public narrow!: boolean;
@property() private calendar?: Calendar;
@property() private _activeView = "dayGridMonth";
protected render(): TemplateResult {
return html`
${this.calendar
? html`
<div class="header">
${!this.narrow
? html`
<div class="navigation">
<mwc-button
outlined
class="today"
@click=${this._handleToday}
>${this.hass.localize(
"ui.panel.calendar.today"
)}</mwc-button
>
<ha-icon-button
label=${this.hass.localize("ui.common.previous")}
icon="hass:chevron-left"
class="prev"
@click=${this._handlePrev}
>
</ha-icon-button>
<ha-icon-button
label=${this.hass.localize("ui.common.next")}
icon="hass:chevron-right"
class="next"
@click=${this._handleNext}
>
</ha-icon-button>
</div>
<h1>
${this.calendar.view.title}
</h1>
<ha-button-toggle-group
.buttons=${viewButtons}
.active=${this._activeView}
@value-changed=${this._handleView}
></ha-button-toggle-group>
`
: html`
<div class="controls">
<mwc-button
outlined
class="today"
@click=${this._handleToday}
>${this.hass.localize(
"ui.panel.calendar.today"
)}</mwc-button
>
<ha-button-toggle-group
.buttons=${viewButtons}
.active=${this._activeView}
@value-changed=${this._handleView}
></ha-button-toggle-group>
</div>
<div class="controls">
<h1>
${this.calendar.view.title}
</h1>
<div>
<ha-icon-button
label=${this.hass.localize("ui.common.previous")}
icon="hass:chevron-left"
class="prev"
@click=${this._handlePrev}
>
</ha-icon-button>
<ha-icon-button
label=${this.hass.localize("ui.common.next")}
icon="hass:chevron-right"
class="next"
@click=${this._handleNext}
>
</ha-icon-button>
</div>
</div>
`}
</div>
`
: ""}
<div id="calendar"></div>
`;
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this.calendar) {
return;
}
if (changedProps.has("events")) {
this.calendar.removeAllEventSources();
this.calendar.addEventSource(this.events);
}
}
protected firstUpdated(): void {
const config = { ...fullCalendarConfig, locale: this.hass.language };
this.calendar = new Calendar(
this.shadowRoot!.getElementById("calendar")!,
// @ts-ignore
config
);
this.calendar!.render();
this._fireViewChanged();
}
private _handleNext(): void {
this.calendar!.next();
this._fireViewChanged();
}
private _handlePrev(): void {
this.calendar!.prev();
this._fireViewChanged();
}
private _handleToday(): void {
this.calendar!.today();
this._fireViewChanged();
}
private _handleView(ev): void {
this._activeView = ev.detail.value;
this.calendar!.changeView(this._activeView);
this._fireViewChanged();
}
private _fireViewChanged(): void {
fireEvent(this, "view-changed", {
start: this.calendar!.view.activeStart,
end: this.calendar!.view.activeEnd,
view: this.calendar!.view.type,
});
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
${unsafeCSS(fullcalendarStyle)}
${unsafeCSS(daygridStyle)}
:host {
display: flex;
flex-direction: column;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 8px;
}
:host([narrow]) .header {
padding-right: 8px;
padding-left: 8px;
flex-direction: column;
align-items: flex-start;
justify-content: initial;
}
.navigation {
display: flex;
align-items: center;
flex-grow: 0;
}
.controls {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.today {
margin-right: 20px;
}
.prev,
.next {
--mdc-icon-button-size: 32px;
}
ha-button-toggle-group {
color: var(--primary-color);
}
#calendar {
flex-grow: 1;
background-color: var(--card-background-color);
}
.fc-scrollgrid-section-header td {
border: none;
}
th.fc-col-header-cell.fc-day {
color: #70757a;
font-size: 11px;
font-weight: 400;
text-transform: uppercase;
}
.fc-daygrid-day-top {
text-align: center;
padding-top: 8px;
}
table.fc-scrollgrid-sync-table
tbody
tr:first-child
.fc-daygrid-day-top {
padding-top: 0;
}
a.fc-daygrid-day-number {
float: none !important;
font-size: 12px;
}
td.fc-day-today {
background: inherit;
}
td.fc-day-today .fc-daygrid-day-number {
height: 24px;
color: #fff;
background-color: #1a73e8;
border-radius: 50%;
display: inline-block;
text-align: center;
white-space: nowrap;
width: max-content;
min-width: 24px;
}
.fc-daygrid-day-events {
margin-top: 4px;
}
.fc-event {
border-radius: 4px;
line-height: 1.7;
}
.fc-daygrid-block-event .fc-event-main {
padding: 0 1px;
}
.fc-day-past .fc-daygrid-day-events {
opacity: 0.5;
}
.fc-icon-x:before {
font-family: var(--material-font-family);
content: "X";
}
`,
];
}
}
window.customElements.define("ha-full-calendar", HAFullCalendar);

View File

@ -1,220 +0,0 @@
import "@polymer/app-layout/app-header-layout/app-header-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import moment from "moment";
import dates from "react-big-calendar/lib/utils/dates";
import "../../components/ha-card";
import "../../components/ha-menu-button";
import LocalizeMixin from "../../mixins/localize-mixin";
import "../../resources/ha-style";
import "./ha-big-calendar";
const DEFAULT_VIEW = "month";
/*
* @appliesMixin LocalizeMixin
*/
class HaPanelCalendar extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include="iron-flex ha-style">
.content {
padding: 16px;
@apply --layout-horizontal;
}
ha-big-calendar {
min-height: 500px;
min-width: 100%;
}
#calendars {
padding-right: 16px;
width: 15%;
min-width: 170px;
}
paper-item {
cursor: pointer;
}
div.all_calendars {
height: 20px;
text-align: center;
}
.iron-selected {
background-color: #e5e5e5;
font-weight: normal;
}
:host([narrow]) .content {
flex-direction: column;
}
:host([narrow]) #calendars {
margin-bottom: 24px;
width: 100%;
}
</style>
<app-header-layout has-scrolling-region>
<app-header slot="header" fixed>
<app-toolbar>
<ha-menu-button
hass="[[hass]]"
narrow="[[narrow]]"
></ha-menu-button>
<div main-title>[[localize('panel.calendar')]]</div>
</app-toolbar>
</app-header>
<div class="flex content">
<div id="calendars" class="layout vertical wrap">
<ha-card header="Calendars">
<paper-listbox
id="calendar_list"
multi
on-selected-items-changed="_fetchData"
selected-values="{{selectedCalendars}}"
attr-for-selected="item-name"
>
<template is="dom-repeat" items="[[calendars]]">
<paper-item item-name="[[item.entity_id]]">
<span
class="calendar_color"
style$="background-color: [[item.color]]"
></span>
<span class="calendar_color_spacer"></span> [[item.name]]
</paper-item>
</template>
</paper-listbox>
</ha-card>
</div>
<div class="flex layout horizontal wrap">
<ha-big-calendar
default-date="[[currentDate]]"
default-view="[[currentView]]"
on-navigate="_handleNavigate"
on-view="_handleViewChanged"
events="[[events]]"
>
</ha-big-calendar>
</div>
</div>
</app-header-layout>
`;
}
static get properties() {
return {
hass: Object,
currentView: {
type: String,
value: DEFAULT_VIEW,
},
currentDate: {
type: Object,
value: new Date(),
},
events: {
type: Array,
value: [],
},
calendars: {
type: Array,
value: [],
},
selectedCalendars: {
type: Array,
value: [],
},
narrow: {
type: Boolean,
reflectToAttribute: true,
},
};
}
connectedCallback() {
super.connectedCallback();
this._fetchCalendars();
}
_fetchCalendars() {
this.hass.callApi("get", "calendars").then((result) => {
this.calendars = result;
this.selectedCalendars = result.map((cal) => cal.entity_id);
});
}
_fetchData() {
const start = dates.firstVisibleDay(this.currentDate).toISOString();
const end = dates.lastVisibleDay(this.currentDate).toISOString();
const params = encodeURI(`?start=${start}&end=${end}`);
const calls = this.selectedCalendars.map((cal) =>
this.hass.callApi("get", `calendars/${cal}${params}`)
);
Promise.all(calls).then((results) => {
const tmpEvents = [];
results.forEach((res) => {
res.forEach((ev) => {
ev.start = new Date(ev.start);
if (ev.end) {
ev.end = new Date(ev.end);
} else {
ev.end = null;
}
tmpEvents.push(ev);
});
});
this.events = tmpEvents;
});
}
_getDateRange() {
let startDate;
let endDate;
if (this.currentView === "day") {
startDate = moment(this.currentDate).startOf("day");
endDate = moment(this.currentDate).startOf("day");
} else if (this.currentView === "week") {
startDate = moment(this.currentDate).startOf("isoWeek");
endDate = moment(this.currentDate).endOf("isoWeek");
} else if (this.currentView === "month") {
startDate = moment(this.currentDate).startOf("month").subtract(7, "days");
endDate = moment(this.currentDate).endOf("month").add(7, "days");
} else if (this.currentView === "agenda") {
startDate = moment(this.currentDate).startOf("day");
endDate = moment(this.currentDate).endOf("day").add(1, "month");
}
return [startDate.toISOString(), endDate.toISOString()];
}
_handleViewChanged(ev) {
// Calendar view changed
this.currentView = ev.detail.viewName;
this._fetchData();
}
_handleNavigate(ev) {
// Calendar date range changed
this.currentDate = ev.detail.date;
this.currentView = ev.detail.viewName;
this._fetchData();
}
}
customElements.define("ha-panel-calendar", HaPanelCalendar);

View File

@ -0,0 +1,236 @@
import {
customElement,
LitElement,
property,
CSSResultArray,
css,
TemplateResult,
html,
PropertyValues,
} from "lit-element";
import { styleMap } from "lit-html/directives/style-map";
import "@polymer/app-layout/app-header-layout/app-header-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@material/mwc-checkbox";
import "@material/mwc-formfield";
import "../../components/ha-menu-button";
import "../../components/ha-card";
import "./ha-full-calendar";
import type {
HomeAssistant,
SelectedCalendar,
CalendarEvent,
CalendarViewChanged,
Calendar,
} from "../../types";
import { haStyle } from "../../resources/styles";
import { HASSDomEvent } from "../../common/dom/fire_event";
import { getCalendars, fetchCalendarEvents } from "../../data/calendar";
@customElement("ha-panel-calendar")
class PanelCalendar extends LitElement {
@property() public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true })
public narrow!: boolean;
@property() private _calendars: SelectedCalendar[] = [];
@property() private _events: CalendarEvent[] = [];
private _start?: Date;
private _end?: Date;
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this._calendars = getCalendars(this.hass).map((calendar) => ({
selected: true,
calendar,
}));
if (!this._start || !this._end) {
return;
}
this._fetchEvents(this._start, this._end, this._selectedCalendars);
}
protected render(): TemplateResult {
return html`
<app-header-layout has-scrolling-region>
<app-header fixed slot="header">
<app-toolbar>
<ha-menu-button
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
<div main-title>${this.hass.localize("panel.calendar")}</div>
<ha-icon-button
icon="hass:refresh"
@click=${this._handleRefresh}
></ha-icon-button>
</app-toolbar>
</app-header>
<div class="content">
<div class="calendar-list">
<div class="calendar-list-header">
${this.hass.localize("ui.panel.calendar.my_calendars")}
</div>
${this._calendars.map(
(selCal) =>
html`<div>
<mwc-formfield .label=${selCal.calendar.name}>
<mwc-checkbox
style=${styleMap({
"--mdc-theme-secondary":
selCal.calendar.backgroundColor,
})}
.value=${selCal.calendar.entity_id}
.checked=${selCal.selected}
@change=${this._handleToggle}
></mwc-checkbox>
</mwc-formfield>
</div>`
)}
</div>
<ha-full-calendar
.events=${this._events}
.narrow=${this.narrow}
.hass=${this.hass}
@view-changed=${this._handleViewChanged}
></ha-full-calendar>
</div>
</app-header-layout>
`;
}
private get _selectedCalendars(): Calendar[] {
return this._calendars
.filter((selCal) => selCal.selected)
.map((cal) => cal.calendar);
}
private async _fetchEvents(
start: Date,
end: Date,
calendars: Calendar[]
): Promise<CalendarEvent[]> {
if (!calendars.length) {
return [];
}
return fetchCalendarEvents(this.hass, start, end, calendars);
}
private async _handleToggle(ev): Promise<void> {
const results = this._calendars.map(async (cal) => {
if (ev.target.value !== cal.calendar.entity_id) {
return cal;
}
const checked = ev.target.checked;
if (checked) {
const events = await this._fetchEvents(this._start!, this._end!, [
cal.calendar,
]);
this._events = [...this._events, ...events];
} else {
this._events = this._events.filter(
(event) => event.calendar !== cal.calendar.entity_id
);
}
cal.selected = checked;
return cal;
});
this._calendars = await Promise.all(results);
}
private async _handleViewChanged(
ev: HASSDomEvent<CalendarViewChanged>
): Promise<void> {
this._start = ev.detail.start;
this._end = ev.detail.end;
this._events = await this._fetchEvents(
this._start,
this._end,
this._selectedCalendars
);
}
private async _handleRefresh(): Promise<void> {
this._events = await this._fetchEvents(
this._start!,
this._end!,
this._selectedCalendars
);
}
static get styles(): CSSResultArray {
return [
haStyle,
css`
.content {
padding: 16px;
display: flex;
box-sizing: border-box;
}
:host(:not([narrow])) .content {
height: calc(100vh - 64px);
}
.calendar-list {
padding-right: 16px;
min-width: 170px;
flex: 0 0 15%;
overflow: hidden;
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
.calendar-list > div {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.calendar-list-header {
font-size: 16px;
padding: 16px 16px 8px 8px;
}
ha-full-calendar {
flex-grow: 1;
}
:host([narrow]) ha-full-calendar {
height: calc(100vh - 72px);
}
:host([narrow]) .content {
flex-direction: column-reverse;
padding: 8px 0 0 0;
}
:host([narrow]) .calendar-list {
margin-bottom: 24px;
width: 100%;
padding-right: 0;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-panel-calendar": PanelCalendar;
}
}

View File

@ -170,12 +170,12 @@ class ActionHandler extends HTMLElement implements ActionHandler {
display: null,
});
this.ripple.disabled = false;
this.ripple.active = true;
this.ripple.activate();
this.ripple.unbounded = true;
}
private stopAnimation() {
this.ripple.active = false;
this.ripple.deactivate();
this.ripple.disabled = true;
this.style.display = "none";
}

View File

@ -42,6 +42,7 @@ export const derivedStyles = {
"mdc-theme-on-primary": "var(--text-primary-color)",
"mdc-theme-on-secondary": "var(--text-primary-color)",
"mdc-theme-on-surface": "var(--primary-text-color)",
"mdc-theme-text-primary-on-background": "var(--primary-text-color)",
"app-header-text-color": "var(--text-primary-color)",
"app-header-background-color": "var(--primary-color)",
"material-body-text-color": "var(--primary-text-color)",

View File

@ -231,10 +231,13 @@
}
},
"common": {
"previous": "Previous",
"loading": "Loading",
"refresh": "Refresh",
"cancel": "Cancel",
"delete": "Delete",
"close": "Close",
"next": "Next",
"undo": "Undo",
"save": "Save",
"yes": "Yes",
@ -499,6 +502,7 @@
"sidebar_toggle": "Sidebar Toggle"
},
"panel": {
"calendar": { "my_calendars": "My Calendars", "today": "Today" },
"config": {
"header": "Configure Home Assistant",
"introduction": "Here it is possible to configure your components and Home Assistant. Not everything is possible to configure from the UI yet, but we're working on it.",

View File

@ -96,6 +96,40 @@ export interface Panels {
[name: string]: PanelInfo;
}
export interface Calendar {
entity_id: string;
name: string;
backgroundColor: string;
}
export interface SelectedCalendar {
selected: boolean;
calendar: Calendar;
}
export interface CalendarEvent {
summary: string;
title: string;
start: string;
end?: string;
backgroundColor?: string;
borderColor?: string;
calendar: string;
[key: string]: any;
}
export interface CalendarViewChanged {
end: Date;
start: Date;
view: string;
}
export interface ToggleButton {
label?: string;
icon: string;
value: string;
}
export interface Translation {
nativeName: string;
isRTL: boolean;

979
yarn.lock

File diff suppressed because it is too large Load Diff