mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 17:56:46 +00:00
Calendar Card: New Card (#5813)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
parent
06a25284e8
commit
dae42b1bd9
@ -23,8 +23,11 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@formatjs/intl-pluralrules": "^1.5.8",
|
||||
"@fullcalendar/core": "^5.0.0-beta.2",
|
||||
"@fullcalendar/daygrid": "^5.0.0-beta.2",
|
||||
"@fullcalendar/common": "5.1.0",
|
||||
"@fullcalendar/core": "5.1.0",
|
||||
"@fullcalendar/daygrid": "5.1.0",
|
||||
"@fullcalendar/interaction": "5.1.0",
|
||||
"@fullcalendar/list": "5.1.0",
|
||||
"@material/chips": "=8.0.0-canary.096a7a066.0",
|
||||
"@material/circular-progress": "=8.0.0-canary.a78ceb112.0",
|
||||
"@material/mwc-button": "^0.18.0",
|
||||
|
@ -1,3 +1,4 @@
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
@ -8,14 +9,14 @@ import {
|
||||
css,
|
||||
} from "lit-element";
|
||||
|
||||
import "./ha-icon-button";
|
||||
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { ToggleButton } from "../types";
|
||||
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-button-toggle-group")
|
||||
export class HaButtonToggleGroup extends LitElement {
|
||||
@property() public buttons!: ToggleButton[];
|
||||
@property({ attribute: false }) public buttons!: ToggleButton[];
|
||||
|
||||
@property() public active?: string;
|
||||
|
||||
@ -23,14 +24,16 @@ export class HaButtonToggleGroup extends LitElement {
|
||||
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>`
|
||||
(button) => html`
|
||||
<mwc-icon-button
|
||||
.label=${button.label}
|
||||
.value=${button.value}
|
||||
?active=${this.active === button.value}
|
||||
@click=${this._handleClick}
|
||||
>
|
||||
<ha-svg-icon .path=${button.iconPath}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
@ -48,12 +51,13 @@ export class HaButtonToggleGroup extends LitElement {
|
||||
--mdc-icon-button-size: var(--button-toggle-size, 36px);
|
||||
--mdc-icon-size: var(--button-toggle-icon-size, 20px);
|
||||
}
|
||||
ha-icon-button {
|
||||
mwc-icon-button {
|
||||
border: 1px solid var(--primary-color);
|
||||
border-right-width: 0px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
ha-icon-button::before {
|
||||
mwc-icon-button::before {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
@ -65,22 +69,26 @@ export class HaButtonToggleGroup extends LitElement {
|
||||
content: "";
|
||||
transition: opacity 15ms linear, background-color 15ms linear;
|
||||
}
|
||||
ha-icon-button[active]::before {
|
||||
mwc-icon-button[active]::before {
|
||||
opacity: var(--mdc-icon-button-ripple-opacity, 0.12);
|
||||
}
|
||||
ha-icon-button:first-child {
|
||||
mwc-icon-button:first-child {
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
ha-icon-button:last-child {
|
||||
mwc-icon-button:last-child {
|
||||
border-radius: 0 4px 4px 0;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
mwc-icon-button:only-child {
|
||||
border-radius: 4px;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-button-toggle-button": HaButtonToggleGroup;
|
||||
"ha-button-toggle-group": HaButtonToggleGroup;
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import "@material/mwc-button";
|
||||
import {
|
||||
property,
|
||||
internalProperty,
|
||||
@ -9,13 +10,19 @@ import {
|
||||
unsafeCSS,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { mdiViewModule, mdiViewWeek, mdiViewDay, mdiViewAgenda } from "@mdi/js";
|
||||
import { Calendar } from "@fullcalendar/core";
|
||||
import type { CalendarOptions } from "@fullcalendar/core";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import listPlugin from "@fullcalendar/list";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
// @ts-ignore
|
||||
import fullcalendarStyle from "@fullcalendar/core/main.css";
|
||||
import fullcalendarStyle from "@fullcalendar/common/main.css";
|
||||
// @ts-ignore
|
||||
import daygridStyle from "@fullcalendar/daygrid/main.css";
|
||||
import "@material/mwc-button";
|
||||
// @ts-ignore
|
||||
import listStyle from "@fullcalendar/list/main.css";
|
||||
import memoize from "memoize-one";
|
||||
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-button-toggle-group";
|
||||
@ -25,43 +32,56 @@ import type {
|
||||
CalendarEvent,
|
||||
ToggleButton,
|
||||
HomeAssistant,
|
||||
FullCalendarView,
|
||||
} from "../../types";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-full-calendar": HAFullCalendar;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"view-changed": CalendarViewChanged;
|
||||
}
|
||||
}
|
||||
|
||||
const fullCalendarConfig = {
|
||||
const defaultFullCalendarConfig: CalendarOptions = {
|
||||
headerToolbar: false,
|
||||
plugins: [dayGridPlugin],
|
||||
plugins: [dayGridPlugin, listPlugin, interactionPlugin],
|
||||
initialView: "dayGridMonth",
|
||||
dayMaxEventRows: true,
|
||||
height: "parent",
|
||||
eventDisplay: "list-item",
|
||||
};
|
||||
|
||||
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" },
|
||||
{ label: "Month View", value: "dayGridMonth", iconPath: mdiViewModule },
|
||||
{ label: "Week View", value: "dayGridWeek", iconPath: mdiViewWeek },
|
||||
{ label: "Day View", value: "dayGridDay", iconPath: mdiViewDay },
|
||||
{ label: "List View", value: "listWeek", iconPath: mdiViewAgenda },
|
||||
];
|
||||
|
||||
class HAFullCalendar extends LitElement {
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
@property() public events: CalendarEvent[] = [];
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public narrow!: boolean;
|
||||
@property({ attribute: false }) public events: CalendarEvent[] = [];
|
||||
|
||||
@property({ attribute: false }) public views: FullCalendarView[] = [
|
||||
"dayGridMonth",
|
||||
"dayGridWeek",
|
||||
"dayGridDay",
|
||||
];
|
||||
|
||||
@internalProperty() private calendar?: Calendar;
|
||||
|
||||
@internalProperty() private _activeView = "dayGridMonth";
|
||||
@internalProperty() private _activeView: FullCalendarView = "dayGridMonth";
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const viewToggleButtons = this._viewToggleButtons(this.views);
|
||||
|
||||
return html`
|
||||
${this.calendar
|
||||
? html`
|
||||
@ -96,27 +116,12 @@ class HAFullCalendar extends LitElement {
|
||||
${this.calendar.view.title}
|
||||
</h1>
|
||||
<ha-button-toggle-group
|
||||
.buttons=${viewButtons}
|
||||
.buttons=${viewToggleButtons}
|
||||
.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}
|
||||
@ -138,6 +143,21 @@ class HAFullCalendar extends LitElement {
|
||||
</ha-icon-button>
|
||||
</div>
|
||||
</div>
|
||||
<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=${viewToggleButtons}
|
||||
.active=${this._activeView}
|
||||
@value-changed=${this._handleView}
|
||||
></ha-button-toggle-group>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`
|
||||
@ -157,14 +177,25 @@ class HAFullCalendar extends LitElement {
|
||||
this.calendar.removeAllEventSources();
|
||||
this.calendar.addEventSource(this.events);
|
||||
}
|
||||
|
||||
if (changedProps.has("views") && !this.views.includes(this._activeView)) {
|
||||
this._activeView = this.views[0];
|
||||
this.calendar!.changeView(this._activeView);
|
||||
this._fireViewChanged();
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
const config = { ...fullCalendarConfig, locale: this.hass.language };
|
||||
const config: CalendarOptions = {
|
||||
...defaultFullCalendarConfig,
|
||||
locale: this.hass.language,
|
||||
};
|
||||
|
||||
config.dateClick = this._handleDateClick;
|
||||
config.eventClick = this._handleEventClick;
|
||||
|
||||
this.calendar = new Calendar(
|
||||
this.shadowRoot!.getElementById("calendar")!,
|
||||
// @ts-ignore
|
||||
config
|
||||
);
|
||||
|
||||
@ -172,6 +203,25 @@ class HAFullCalendar extends LitElement {
|
||||
this._fireViewChanged();
|
||||
}
|
||||
|
||||
private _handleEventClick(info): void {
|
||||
if (info.view.type !== "dayGridMonth") {
|
||||
return;
|
||||
}
|
||||
|
||||
this._activeView = "dayGridDay";
|
||||
this.calendar!.changeView("dayGridDay");
|
||||
this.calendar!.gotoDate(info.event.startStr);
|
||||
}
|
||||
|
||||
private _handleDateClick(info): void {
|
||||
if (info.view.type !== "dayGridMonth") {
|
||||
return;
|
||||
}
|
||||
this._activeView = "dayGridDay";
|
||||
this.calendar!.changeView("dayGridDay");
|
||||
this.calendar!.gotoDate(info.dateStr);
|
||||
}
|
||||
|
||||
private _handleNext(): void {
|
||||
this.calendar!.next();
|
||||
this._fireViewChanged();
|
||||
@ -201,14 +251,21 @@ class HAFullCalendar extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _viewToggleButtons = memoize((views) =>
|
||||
viewButtons.filter((button) =>
|
||||
views.includes(button.value as FullCalendarView)
|
||||
)
|
||||
);
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
${unsafeCSS(fullcalendarStyle)}
|
||||
${unsafeCSS(daygridStyle)}
|
||||
|
||||
:host {
|
||||
${unsafeCSS(listStyle)}
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
--fc-theme-standard-border-color: var(--divider-color);
|
||||
@ -262,6 +319,15 @@ class HAFullCalendar extends LitElement {
|
||||
#calendar {
|
||||
flex-grow: 1;
|
||||
background-color: var(--card-background-color);
|
||||
min-height: 400px;
|
||||
--fc-neutral-bg-color: var(--card-background-color);
|
||||
--fc-list-event-hover-bg-color: var(--card-background-color);
|
||||
--fc-theme-standard-border-color: var(--divider-color);
|
||||
--fc-border-color: var(--divider-color);
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.fc-theme-standard .fc-scrollgrid {
|
||||
@ -273,15 +339,20 @@ class HAFullCalendar extends LitElement {
|
||||
}
|
||||
|
||||
th.fc-col-header-cell.fc-day {
|
||||
color: #70757a;
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.fc-daygrid-dot-event:hover {
|
||||
background-color: inherit
|
||||
}
|
||||
|
||||
.fc-daygrid-day-top {
|
||||
text-align: center;
|
||||
padding-top: 8px;
|
||||
padding-top: 5px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
table.fc-scrollgrid-sync-table
|
||||
@ -296,13 +367,21 @@ class HAFullCalendar extends LitElement {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
td.fc-day-today {
|
||||
.fc .fc-daygrid-day-number {
|
||||
padding: 3px !important;
|
||||
}
|
||||
|
||||
.fc .fc-daygrid-day.fc-day-today {
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
td.fc-day-today .fc-daygrid-day-top {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
td.fc-day-today .fc-daygrid-day-number {
|
||||
height: 24px;
|
||||
color: var(--text-primary-color);
|
||||
color: var(--text-primary-color) !important;
|
||||
background-color: var(--primary-color);
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
@ -342,6 +421,66 @@ class HAFullCalendar extends LitElement {
|
||||
.fc-popover-header {
|
||||
background-color: var(--secondary-background-color) !important;
|
||||
}
|
||||
|
||||
.fc-theme-standard .fc-list-day-frame {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.fc-list.fc-view,
|
||||
.fc-list-event.fc-event td {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.fc-list-day.fc-day th {
|
||||
border-bottom: none;
|
||||
border-top: 1px solid var(--fc-theme-standard-border-color, #ddd) !important;
|
||||
}
|
||||
|
||||
.fc-list-day-text {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.fc-list-day-side-text {
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.fc-list-table td,
|
||||
.fc-list-day-frame {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
:host([narrow]) .fc-dayGridMonth-view
|
||||
.fc-daygrid-dot-event
|
||||
.fc-event-time,
|
||||
:host([narrow]) .fc-dayGridMonth-view
|
||||
.fc-daygrid-dot-event
|
||||
.fc-event-title,
|
||||
:host([narrow]) .fc-dayGridMonth-view .fc-daygrid-day-bottom {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host([narrow]) .fc .fc-dayGridMonth-view .fc-daygrid-event-harness-abs {
|
||||
visibility: visible !important;
|
||||
position: static;
|
||||
}
|
||||
|
||||
:host([narrow]) .fc-dayGridMonth-view .fc-daygrid-day-events {
|
||||
display: flex;
|
||||
min-height: 2em !important;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
max-height: 2em;
|
||||
height: 2em;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:host([narrow]) .fc-dayGridMonth-view .fc-scrollgrid-sync-table {
|
||||
overflow: hidden;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -53,12 +53,6 @@ class PanelCalendar extends LitElement {
|
||||
selected: true,
|
||||
calendar,
|
||||
}));
|
||||
|
||||
if (!this._start || !this._end) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._fetchEvents(this._start, this._end, this._selectedCalendars);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@ -88,8 +82,8 @@ class PanelCalendar extends LitElement {
|
||||
<mwc-formfield .label=${selCal.calendar.name}>
|
||||
<mwc-checkbox
|
||||
style=${styleMap({
|
||||
"--mdc-theme-secondary":
|
||||
selCal.calendar.backgroundColor,
|
||||
"--mdc-theme-secondary": selCal.calendar
|
||||
.backgroundColor!,
|
||||
})}
|
||||
.value=${selCal.calendar.entity_id}
|
||||
.checked=${selCal.selected}
|
||||
|
213
src/panels/lovelace/cards/hui-calendar-card.ts
Normal file
213
src/panels/lovelace/cards/hui-calendar-card.ts
Normal file
@ -0,0 +1,213 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
internalProperty,
|
||||
} from "lit-element";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../calendar/ha-full-calendar";
|
||||
|
||||
import type {
|
||||
HomeAssistant,
|
||||
CalendarEvent,
|
||||
Calendar,
|
||||
CalendarViewChanged,
|
||||
FullCalendarView,
|
||||
} from "../../../types";
|
||||
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import type { CalendarCardConfig } from "./types";
|
||||
import { findEntities } from "../common/find-entites";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import "../components/hui-warning";
|
||||
import { fetchCalendarEvents } from "../../../data/calendar";
|
||||
import { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { HA_COLOR_PALETTE } from "../../../common/const";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import { installResizeObserver } from "../common/install-resize-observer";
|
||||
|
||||
@customElement("hui-calendar-card")
|
||||
export class HuiCalendarCard extends LitElement implements LovelaceCard {
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
await import(
|
||||
/* webpackChunkName: "hui-calendar-card-editor" */ "../editor/config-elements/hui-calendar-card-editor"
|
||||
);
|
||||
return document.createElement("hui-calendar-card-editor");
|
||||
}
|
||||
|
||||
public static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
entities: string[],
|
||||
entitiesFill: string[]
|
||||
) {
|
||||
const includeDomains = ["calendar"];
|
||||
const maxEntities = 2;
|
||||
const foundEntities = findEntities(
|
||||
hass,
|
||||
maxEntities,
|
||||
entities,
|
||||
entitiesFill,
|
||||
includeDomains
|
||||
);
|
||||
|
||||
return {
|
||||
entities: foundEntities,
|
||||
};
|
||||
}
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public _events: CalendarEvent[] = [];
|
||||
|
||||
@internalProperty() private _config?: CalendarCardConfig;
|
||||
|
||||
@internalProperty() private _calendars: Calendar[] = [];
|
||||
|
||||
@internalProperty() private _narrow = false;
|
||||
|
||||
@internalProperty() private _veryNarrow = false;
|
||||
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
|
||||
public setConfig(config: CalendarCardConfig): void {
|
||||
if (!config.entities) {
|
||||
throw new Error("Entities must be defined");
|
||||
}
|
||||
|
||||
if (config.entities && !Array.isArray(config.entities)) {
|
||||
throw new Error("Entities need to be an array");
|
||||
}
|
||||
|
||||
this._calendars = config!.entities.map((entity, idx) => {
|
||||
return {
|
||||
entity_id: entity,
|
||||
backgroundColor: `#${HA_COLOR_PALETTE[idx % HA_COLOR_PALETTE.length]}`,
|
||||
};
|
||||
});
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return 4;
|
||||
}
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.updateComplete.then(() => this._attachObserver());
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
if (this._resizeObserver) {
|
||||
this._resizeObserver.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config || !this.hass || !this._calendars.length) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const views: FullCalendarView[] = this._veryNarrow
|
||||
? ["listWeek"]
|
||||
: ["listWeek", "dayGridMonth", "dayGridDay"];
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="header">${this._config.title}</div>
|
||||
<ha-full-calendar
|
||||
.narrow=${this._narrow}
|
||||
.events=${this._events}
|
||||
.hass=${this.hass}
|
||||
.views=${views}
|
||||
@view-changed=${this._handleViewChanged}
|
||||
></ha-full-calendar>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
const oldConfig = changedProps.get("_config") as
|
||||
| CalendarCardConfig
|
||||
| undefined;
|
||||
|
||||
if (
|
||||
!oldHass ||
|
||||
!oldConfig ||
|
||||
(changedProps.has("hass") && oldHass.themes !== this.hass.themes) ||
|
||||
(changedProps.has("_config") && oldConfig.theme !== this._config.theme)
|
||||
) {
|
||||
applyThemesOnElement(this, this.hass.themes, this._config!.theme);
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleViewChanged(
|
||||
ev: HASSDomEvent<CalendarViewChanged>
|
||||
): Promise<void> {
|
||||
this._events = await fetchCalendarEvents(
|
||||
this.hass!,
|
||||
ev.detail.start,
|
||||
ev.detail.end,
|
||||
this._calendars
|
||||
);
|
||||
}
|
||||
|
||||
private _measureCard() {
|
||||
const card = this.shadowRoot!.querySelector("ha-card");
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
this._narrow = card.offsetWidth < 870;
|
||||
this._veryNarrow = card.offsetWidth < 350;
|
||||
}
|
||||
|
||||
private async _attachObserver(): Promise<void> {
|
||||
if (!this._resizeObserver) {
|
||||
await installResizeObserver();
|
||||
this._resizeObserver = new ResizeObserver(
|
||||
debounce(() => this._measureCard(), 250, false)
|
||||
);
|
||||
}
|
||||
const card = this.shadowRoot!.querySelector("ha-card");
|
||||
// If we show an error or warning there is no ha-card
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
this._resizeObserver.observe(card);
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-card {
|
||||
position: relative;
|
||||
padding: 0 8px 8px;
|
||||
}
|
||||
|
||||
.header {
|
||||
color: var(--ha-card-header-color, --primary-text-color);
|
||||
font-size: var(--ha-card-header-font-size, 24px);
|
||||
line-height: 1.2;
|
||||
padding-top: 16px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-calendar-card": HuiCalendarCard;
|
||||
}
|
||||
}
|
@ -12,6 +12,12 @@ export interface AlarmPanelCardConfig extends LovelaceCardConfig {
|
||||
theme?: string;
|
||||
}
|
||||
|
||||
export interface CalendarCardConfig extends LovelaceCardConfig {
|
||||
entities: string[];
|
||||
title?: string;
|
||||
theme?: string;
|
||||
}
|
||||
|
||||
export interface ConditionalCardConfig extends LovelaceCardConfig {
|
||||
card: LovelaceCardConfig;
|
||||
conditions: Condition[];
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace";
|
||||
import "../cards/hui-button-card";
|
||||
import "../cards/hui-calendar-card";
|
||||
import "../cards/hui-entities-card";
|
||||
import "../cards/hui-entity-button-card";
|
||||
import "../cards/hui-entity-card";
|
||||
@ -52,6 +53,7 @@ const LAZY_LOAD_TYPES = {
|
||||
map: () => import("../cards/hui-map-card"),
|
||||
markdown: () => import("../cards/hui-markdown-card"),
|
||||
picture: () => import("../cards/hui-picture-card"),
|
||||
calendar: () => import("../cards/hui-calendar-card"),
|
||||
};
|
||||
|
||||
// This will not return an error card but will throw the error
|
||||
|
@ -0,0 +1,133 @@
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
internalProperty,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { CalendarCardConfig } from "../../cards/types";
|
||||
import "../../components/hui-entity-editor";
|
||||
import "../../../../components/entity/ha-entities-picker";
|
||||
import "../../components/hui-theme-select-editor";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import type { EditorTarget, EntitiesEditorEvent } from "../types";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
import {
|
||||
string,
|
||||
optional,
|
||||
object,
|
||||
boolean,
|
||||
array,
|
||||
union,
|
||||
assert,
|
||||
} from "superstruct";
|
||||
|
||||
const cardConfigStruct = object({
|
||||
type: string(),
|
||||
title: optional(union([string(), boolean()])),
|
||||
theme: optional(string()),
|
||||
entities: array(string()),
|
||||
});
|
||||
|
||||
@customElement("hui-calendar-card-editor")
|
||||
export class HuiCalendarCardEditor extends LitElement
|
||||
implements LovelaceCardEditor {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) private _config?: CalendarCardConfig;
|
||||
|
||||
@internalProperty() private _configEntities?: string[];
|
||||
|
||||
public setConfig(config: CalendarCardConfig): void {
|
||||
assert(config, cardConfigStruct);
|
||||
this._config = config;
|
||||
this._configEntities = config.entities;
|
||||
}
|
||||
|
||||
get _title(): string {
|
||||
return this._config!.title || "";
|
||||
}
|
||||
|
||||
get _theme(): string {
|
||||
return this._config!.theme || "";
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${configElementStyle}
|
||||
<div class="card-config">
|
||||
<div class="side-by-side">
|
||||
<paper-input
|
||||
.label="${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.title"
|
||||
)} (${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})"
|
||||
.value=${this._title}
|
||||
.configValue=${"title"}
|
||||
@value-changed=${this._valueChanged}
|
||||
></paper-input>
|
||||
<hui-theme-select-editor
|
||||
.hass=${this.hass}
|
||||
.value=${this._theme}
|
||||
.configValue=${"theme"}
|
||||
@value-changed=${this._valueChanged}
|
||||
></hui-theme-select-editor>
|
||||
</div>
|
||||
</div>
|
||||
<h3>
|
||||
${"Calendar Entities" +
|
||||
" (" +
|
||||
this.hass!.localize("ui.panel.lovelace.editor.card.config.required") +
|
||||
")"}
|
||||
</h3>
|
||||
<ha-entities-picker
|
||||
.hass=${this.hass!}
|
||||
.value=${this._configEntities}
|
||||
.includeDomains=${["calendar"]}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-entities-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: EntitiesEditorEvent | CustomEvent): void {
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = ev.target! as EditorTarget;
|
||||
|
||||
if (this[`_${target.configValue}`] === target.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.detail && ev.detail.value && Array.isArray(ev.detail.value)) {
|
||||
this._config = { ...this._config, entities: ev.detail.value };
|
||||
} else if (target.configValue) {
|
||||
if (target.value === "") {
|
||||
delete this._config[target.configValue!];
|
||||
} else {
|
||||
this._config = {
|
||||
...this._config,
|
||||
[target.configValue]: target.value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-calendar-card-editor": HuiCalendarCardEditor;
|
||||
}
|
||||
}
|
@ -9,6 +9,10 @@ export const coreCards: Card[] = [
|
||||
type: "button",
|
||||
showElement: true,
|
||||
},
|
||||
{
|
||||
type: "calendar",
|
||||
showElement: true,
|
||||
},
|
||||
{
|
||||
type: "entities",
|
||||
showElement: true,
|
||||
|
@ -2134,6 +2134,10 @@
|
||||
"available_states": "Available States",
|
||||
"description": "The Alarm Panel card allows you to Arm and Disarm your alarm control panel integrations."
|
||||
},
|
||||
"calendar": {
|
||||
"name": "Calendar",
|
||||
"description": "The Calendar card displays a calendar including day, week and list views"
|
||||
},
|
||||
"conditional": {
|
||||
"name": "Conditional",
|
||||
"description": "The Conditional card displays another card based on entity states.",
|
||||
|
14
src/types.ts
14
src/types.ts
@ -118,8 +118,8 @@ export interface Panels {
|
||||
|
||||
export interface Calendar {
|
||||
entity_id: string;
|
||||
name: string;
|
||||
backgroundColor: string;
|
||||
name?: string;
|
||||
backgroundColor?: string;
|
||||
}
|
||||
|
||||
export interface SelectedCalendar {
|
||||
@ -144,9 +144,15 @@ export interface CalendarViewChanged {
|
||||
view: string;
|
||||
}
|
||||
|
||||
export type FullCalendarView =
|
||||
| "dayGridMonth"
|
||||
| "dayGridWeek"
|
||||
| "dayGridDay"
|
||||
| "listWeek";
|
||||
|
||||
export interface ToggleButton {
|
||||
label?: string;
|
||||
icon: string;
|
||||
label: string;
|
||||
iconPath: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
|
52
yarn.lock
52
yarn.lock
@ -1184,20 +1184,45 @@
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-utils/-/intl-utils-2.2.5.tgz#eaafd94df3d102ee13e54e80f992a33868a6b1e8"
|
||||
integrity sha512-p7gcmazKROteL4IECCp03Qrs790fZ8tbemUAjQu0+K0AaAlK49rI1SIFFq3LzDUAqXIshV95JJhRe/yXxkal5g==
|
||||
|
||||
"@fullcalendar/core@^5.0.0-beta.2":
|
||||
version "5.0.0-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/@fullcalendar/core/-/core-5.0.0-beta.2.tgz#30a9cbbbf2d6476568f53cfa1c0746d06daa9660"
|
||||
integrity sha512-9U/kk8Y4ackY1XZ1PHvX8rG1olixoKveStXdDsX3FCtKATR8fA/O+4Pd5qyH7nGcih8TrgreUjZ+dB+DEaomqQ==
|
||||
"@fullcalendar/common@5.1.0", "@fullcalendar/common@~5.1.0":
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@fullcalendar/common/-/common-5.1.0.tgz#a45e01ebdcf00654f4d45f0457926cf2f5909820"
|
||||
integrity sha512-ubwf9T0BDocGLh0AK8achrmc0siQdrp3Wn6Rmg/Ht4/WnwBtjc5gumzJ0ezG/xHE0GWG+Pz6Tm0IJF/jE7NJHw==
|
||||
dependencies:
|
||||
preact "^10.0.5"
|
||||
tslib "^1.9.3"
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@fullcalendar/daygrid@^5.0.0-beta.2":
|
||||
version "5.0.0-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/@fullcalendar/daygrid/-/daygrid-5.0.0-beta.2.tgz#fae2a7cba8a6afc23c9d6faaa88150d820e59a73"
|
||||
integrity sha512-Dd2VLsMPWPH1W1HQ+K1iftHVXF1MYOM/lo33FUdFKM8jc0MHnF620TVyOgPojAC26u2hApbURKIY2eY87vFulg==
|
||||
"@fullcalendar/core@5.1.0":
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@fullcalendar/core/-/core-5.1.0.tgz#0d9813b0564f24fe6285e31f8313d9366c6db411"
|
||||
integrity sha512-C9OA9LHD1zgfcMABQ17TaxvAO/iubYFLDrgTIRCe77LatC0G36UBafthevUlMRqCR6rt5SBcw0oBt23HqSJO3A==
|
||||
dependencies:
|
||||
tslib "^1.9.3"
|
||||
"@fullcalendar/common" "~5.1.0"
|
||||
preact "^10.0.5"
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@fullcalendar/daygrid@5.1.0":
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@fullcalendar/daygrid/-/daygrid-5.1.0.tgz#4f8cb30e99ff00600f064645942d21e8f668c946"
|
||||
integrity sha512-zdO/EFer8wWrr+kvkMfyizPi/F7yvOCpGOb6Arz6QqyOgFl1ffNUQoRf1iQf7h2PnIEvZWZdZs+3wuMwwmhb3g==
|
||||
dependencies:
|
||||
"@fullcalendar/common" "~5.1.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@fullcalendar/interaction@5.1.0":
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@fullcalendar/interaction/-/interaction-5.1.0.tgz#0e7c6fe17bf8532de2c994bde5fc6e9220c71a89"
|
||||
integrity sha512-RuCtsVFXvGYk4vYz3Aq9+G8SMOG6iX+xNJmHELdmPfAatHTVyUSCD4GznxR1/fzUu74mb98X01DVkLwSB+78oA==
|
||||
dependencies:
|
||||
"@fullcalendar/common" "~5.1.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@fullcalendar/list@5.1.0":
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@fullcalendar/list/-/list-5.1.0.tgz#e08b011cce8259fdee46ef8737bf98b31e6eac1a"
|
||||
integrity sha512-FDLsInaLv0H/BwpoJ4RbC69M8yoJ3xoRnkulChAzhIBrwXAKOHJth5P+iNFJVVYI7DaPgSi0BzEwiu0dJdStHQ==
|
||||
dependencies:
|
||||
"@fullcalendar/common" "~5.1.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@gfx/zopfli@^1.0.9":
|
||||
version "1.0.11"
|
||||
@ -11747,6 +11772,11 @@ tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
|
||||
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
|
||||
|
||||
tslib@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.0.tgz#18d13fc2dce04051e20f074cc8387fd8089ce4f3"
|
||||
integrity sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g==
|
||||
|
||||
tsutils@^3.17.1:
|
||||
version "3.17.1"
|
||||
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"
|
||||
|
Loading…
x
Reference in New Issue
Block a user