mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-18 23:06:40 +00:00
Add two pane view to calendar panel (#18286)
Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
This commit is contained in:
parent
6cae11f0a6
commit
463a3244cf
@ -182,6 +182,8 @@ const createWebpackConfig = ({
|
|||||||
"@lit-labs/virtualizer/layouts/grid.js",
|
"@lit-labs/virtualizer/layouts/grid.js",
|
||||||
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver":
|
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver":
|
||||||
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js",
|
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js",
|
||||||
|
"@lit-labs/observers/resize-controller":
|
||||||
|
"@lit-labs/observers/resize-controller.js",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
|
@ -52,10 +52,12 @@
|
|||||||
"@lezer/highlight": "1.1.6",
|
"@lezer/highlight": "1.1.6",
|
||||||
"@lit-labs/context": "0.4.1",
|
"@lit-labs/context": "0.4.1",
|
||||||
"@lit-labs/motion": "1.0.4",
|
"@lit-labs/motion": "1.0.4",
|
||||||
|
"@lit-labs/observers": "2.0.1",
|
||||||
"@lit-labs/virtualizer": "2.0.7",
|
"@lit-labs/virtualizer": "2.0.7",
|
||||||
"@lrnwebcomponents/simple-tooltip": "7.0.18",
|
"@lrnwebcomponents/simple-tooltip": "7.0.18",
|
||||||
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
||||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||||
|
"@material/mwc-base": "0.27.0",
|
||||||
"@material/mwc-button": "0.27.0",
|
"@material/mwc-button": "0.27.0",
|
||||||
"@material/mwc-checkbox": "0.27.0",
|
"@material/mwc-checkbox": "0.27.0",
|
||||||
"@material/mwc-circular-progress": "0.27.0",
|
"@material/mwc-circular-progress": "0.27.0",
|
||||||
|
@ -26,6 +26,8 @@ export class HaButtonMenu extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public fixed = false;
|
@property({ type: Boolean }) public fixed = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "no-anchor" }) public noAnchor = false;
|
||||||
|
|
||||||
@query("mwc-menu", true) private _menu?: Menu;
|
@query("mwc-menu", true) private _menu?: Menu;
|
||||||
|
|
||||||
public get items() {
|
public get items() {
|
||||||
@ -82,7 +84,7 @@ export class HaButtonMenu extends LitElement {
|
|||||||
if (this.disabled) {
|
if (this.disabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._menu!.anchor = this;
|
this._menu!.anchor = this.noAnchor ? null : this;
|
||||||
this._menu!.show();
|
this._menu!.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,9 @@ export class HaButton extends Button {
|
|||||||
.mdc-button {
|
.mdc-button {
|
||||||
height: var(--button-height, 36px);
|
height: var(--button-height, 36px);
|
||||||
}
|
}
|
||||||
|
.trailing-icon {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
320
src/components/ha-two-pane-top-app-bar-fixed.ts
Normal file
320
src/components/ha-two-pane-top-app-bar-fixed.ts
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
import {
|
||||||
|
addHasRemoveClass,
|
||||||
|
BaseElement,
|
||||||
|
} from "@material/mwc-base/base-element";
|
||||||
|
import { supportsPassiveEventListener } from "@material/mwc-base/utils";
|
||||||
|
import { MDCTopAppBarAdapter } from "@material/top-app-bar/adapter";
|
||||||
|
import { strings } from "@material/top-app-bar/constants";
|
||||||
|
import MDCFixedTopAppBarFoundation from "@material/top-app-bar/fixed/foundation";
|
||||||
|
import { html, css, nothing } from "lit";
|
||||||
|
import { property, query, customElement } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import { styles } from "@material/mwc-top-app-bar/mwc-top-app-bar.css";
|
||||||
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
|
|
||||||
|
export const passiveEventOptionsIfSupported = supportsPassiveEventListener
|
||||||
|
? { passive: true }
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
@customElement("ha-two-pane-top-app-bar-fixed")
|
||||||
|
export abstract class TopAppBarBaseBase extends BaseElement {
|
||||||
|
protected override mdcFoundation!: MDCFixedTopAppBarFoundation;
|
||||||
|
|
||||||
|
protected override mdcFoundationClass = MDCFixedTopAppBarFoundation;
|
||||||
|
|
||||||
|
@query(".mdc-top-app-bar") protected mdcRoot!: HTMLElement;
|
||||||
|
|
||||||
|
// _actionItemsSlot should have type HTMLSlotElement, but when TypeScript's
|
||||||
|
// emitDecoratorMetadata is enabled, the HTMLSlotElement constructor will
|
||||||
|
// be emitted into the runtime, which will cause an "HTMLSlotElement is
|
||||||
|
// undefined" error in browsers that don't define it (e.g. IE11).
|
||||||
|
@query('slot[name="actionItems"]') protected _actionItemsSlot!: HTMLElement;
|
||||||
|
|
||||||
|
protected _scrollTarget!: HTMLElement | Window;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) centerTitle = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) prominent = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) dense = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) pane = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) footer = false;
|
||||||
|
|
||||||
|
@query(".content") private _contentElement!: HTMLElement;
|
||||||
|
|
||||||
|
@query(".pane .ha-scrollbar") private _paneElement?: HTMLElement;
|
||||||
|
|
||||||
|
@property({ type: Object })
|
||||||
|
get scrollTarget() {
|
||||||
|
return this._scrollTarget || window;
|
||||||
|
}
|
||||||
|
|
||||||
|
set scrollTarget(value) {
|
||||||
|
this.unregisterListeners();
|
||||||
|
const old = this.scrollTarget;
|
||||||
|
this._scrollTarget = value;
|
||||||
|
this.updateRootPosition();
|
||||||
|
this.requestUpdate("scrollTarget", old);
|
||||||
|
this.registerListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateRootPosition() {
|
||||||
|
if (this.mdcRoot) {
|
||||||
|
const windowScroller = this.scrollTarget === window;
|
||||||
|
// we add support for top-app-bar's tied to an element scroller.
|
||||||
|
this.mdcRoot.style.position = windowScroller ? "" : "absolute";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected barClasses() {
|
||||||
|
return {
|
||||||
|
"mdc-top-app-bar--dense": this.dense,
|
||||||
|
"mdc-top-app-bar--prominent": this.prominent,
|
||||||
|
"center-title": this.centerTitle,
|
||||||
|
"mdc-top-app-bar--fixed": true,
|
||||||
|
"mdc-top-app-bar--pane": this.pane,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected contentClasses() {
|
||||||
|
return {
|
||||||
|
"mdc-top-app-bar--fixed-adjust": !this.dense && !this.prominent,
|
||||||
|
"mdc-top-app-bar--dense-fixed-adjust": this.dense && !this.prominent,
|
||||||
|
"mdc-top-app-bar--prominent-fixed-adjust": !this.dense && this.prominent,
|
||||||
|
"mdc-top-app-bar--dense-prominent-fixed-adjust":
|
||||||
|
this.dense && this.prominent,
|
||||||
|
"mdc-top-app-bar--pane": this.pane,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override render() {
|
||||||
|
const title = html`<span class="mdc-top-app-bar__title"
|
||||||
|
><slot name="title"></slot
|
||||||
|
></span>`;
|
||||||
|
return html`
|
||||||
|
<header class="mdc-top-app-bar ${classMap(this.barClasses())}">
|
||||||
|
<div class="mdc-top-app-bar__row">
|
||||||
|
${this.pane
|
||||||
|
? html`<section
|
||||||
|
class="mdc-top-app-bar__section mdc-top-app-bar__section--align-start"
|
||||||
|
id="title"
|
||||||
|
>
|
||||||
|
<slot
|
||||||
|
name="navigationIcon"
|
||||||
|
@click=${this.handleNavigationClick}
|
||||||
|
></slot>
|
||||||
|
${title}
|
||||||
|
</section>`
|
||||||
|
: nothing}
|
||||||
|
<section class="mdc-top-app-bar__section" id="navigation">
|
||||||
|
${this.pane
|
||||||
|
? nothing
|
||||||
|
: html`<slot
|
||||||
|
name="navigationIcon"
|
||||||
|
@click=${this.handleNavigationClick}
|
||||||
|
></slot
|
||||||
|
>${title}`}
|
||||||
|
</section>
|
||||||
|
<section
|
||||||
|
class="mdc-top-app-bar__section mdc-top-app-bar__section--align-end"
|
||||||
|
id="actions"
|
||||||
|
role="toolbar"
|
||||||
|
>
|
||||||
|
<slot name="actionItems"></slot>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class=${classMap(this.contentClasses())}>
|
||||||
|
${this.pane
|
||||||
|
? html`<div class="pane">
|
||||||
|
<div class="shadow-container"></div>
|
||||||
|
<div class="ha-scrollbar">
|
||||||
|
<slot name="pane"></slot>
|
||||||
|
</div>
|
||||||
|
${this.footer
|
||||||
|
? html`<div class="footer">
|
||||||
|
<slot name="pane-footer"></slot>
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
|
<div class="main">
|
||||||
|
${this.pane ? html`<div class="shadow-container"></div>` : nothing}
|
||||||
|
<div class="content">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProperties) {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
if (
|
||||||
|
changedProperties.has("pane") &&
|
||||||
|
changedProperties.get("pane") !== undefined
|
||||||
|
) {
|
||||||
|
this.unregisterListeners();
|
||||||
|
this.registerListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createAdapter(): MDCTopAppBarAdapter {
|
||||||
|
return {
|
||||||
|
...addHasRemoveClass(this.mdcRoot),
|
||||||
|
setStyle: (prprty: string, value: string) =>
|
||||||
|
this.mdcRoot.style.setProperty(prprty, value),
|
||||||
|
getTopAppBarHeight: () => this.mdcRoot.clientHeight,
|
||||||
|
notifyNavigationIconClicked: () => {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new Event(strings.NAVIGATION_EVENT, {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getViewportScrollY: () =>
|
||||||
|
this.scrollTarget instanceof Window
|
||||||
|
? this.scrollTarget.pageYOffset
|
||||||
|
: this.scrollTarget.scrollTop,
|
||||||
|
getTotalActionItems: () =>
|
||||||
|
(this._actionItemsSlot as HTMLSlotElement).assignedNodes({
|
||||||
|
flatten: true,
|
||||||
|
}).length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected handleTargetScroll = () => {
|
||||||
|
this.mdcFoundation.handleTargetScroll();
|
||||||
|
};
|
||||||
|
|
||||||
|
protected handlePaneScroll = (ev) => {
|
||||||
|
if (ev.target.scrollTop > 0) {
|
||||||
|
ev.target.parentElement.classList.add("scrolled");
|
||||||
|
} else {
|
||||||
|
ev.target.parentElement.classList.remove("scrolled");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected handleNavigationClick = () => {
|
||||||
|
this.mdcFoundation.handleNavigationClick();
|
||||||
|
};
|
||||||
|
|
||||||
|
protected registerListeners() {
|
||||||
|
if (this.pane) {
|
||||||
|
this._paneElement!.addEventListener(
|
||||||
|
"scroll",
|
||||||
|
this.handlePaneScroll,
|
||||||
|
passiveEventOptionsIfSupported
|
||||||
|
);
|
||||||
|
this._contentElement.addEventListener(
|
||||||
|
"scroll",
|
||||||
|
this.handlePaneScroll,
|
||||||
|
passiveEventOptionsIfSupported
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.scrollTarget.addEventListener(
|
||||||
|
"scroll",
|
||||||
|
this.handleTargetScroll,
|
||||||
|
passiveEventOptionsIfSupported
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected unregisterListeners() {
|
||||||
|
this._paneElement?.removeEventListener("scroll", this.handlePaneScroll);
|
||||||
|
this._contentElement.removeEventListener("scroll", this.handlePaneScroll);
|
||||||
|
this.scrollTarget.removeEventListener("scroll", this.handleTargetScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override firstUpdated() {
|
||||||
|
super.firstUpdated();
|
||||||
|
this.updateRootPosition();
|
||||||
|
this.registerListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
override disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this.unregisterListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
static override styles = [
|
||||||
|
styles,
|
||||||
|
haStyleScrollbar,
|
||||||
|
css`
|
||||||
|
.mdc-top-app-bar__row {
|
||||||
|
height: var(--header-height);
|
||||||
|
border-bottom: var(--app-header-border-bottom);
|
||||||
|
}
|
||||||
|
.mdc-top-app-bar--fixed-adjust {
|
||||||
|
padding-top: var(--header-height);
|
||||||
|
}
|
||||||
|
.shadow-container {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(-1 * var(--header-height));
|
||||||
|
width: 100%;
|
||||||
|
height: var(--header-height);
|
||||||
|
z-index: 1;
|
||||||
|
transition: box-shadow 200ms linear;
|
||||||
|
}
|
||||||
|
.scrolled .shadow-container {
|
||||||
|
box-shadow: var(
|
||||||
|
--mdc-top-app-bar-fixed-box-shadow,
|
||||||
|
0px 2px 4px -1px rgba(0, 0, 0, 0.2),
|
||||||
|
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
|
||||||
|
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
.mdc-top-app-bar {
|
||||||
|
--mdc-typography-headline6-font-weight: 400;
|
||||||
|
color: var(--app-header-text-color, var(--mdc-theme-on-primary, #fff));
|
||||||
|
background-color: var(
|
||||||
|
--app-header-background-color,
|
||||||
|
var(--mdc-theme-primary)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
.mdc-top-app-bar--pane.mdc-top-app-bar--fixed-scrolled {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
#title {
|
||||||
|
border-right: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex: 0 0 var(--sidepane-width, 250px);
|
||||||
|
width: var(--sidepane-width, 250px);
|
||||||
|
}
|
||||||
|
div.mdc-top-app-bar--pane {
|
||||||
|
display: flex;
|
||||||
|
height: calc(100vh - var(--header-height));
|
||||||
|
}
|
||||||
|
.pane {
|
||||||
|
border-right: 1px solid var(--divider-color);
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 var(--sidepane-width, 250px);
|
||||||
|
width: var(--sidepane-width, 250px);
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.pane .ha-scrollbar {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.pane .footer {
|
||||||
|
border-top: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
.main {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
.mdc-top-app-bar--pane .main {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.mdc-top-app-bar--pane .content {
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
@ -143,7 +143,7 @@ export const getCalendars = (hass: HomeAssistant): Calendar[] =>
|
|||||||
)
|
)
|
||||||
.sort()
|
.sort()
|
||||||
.map((eid, idx) => ({
|
.map((eid, idx) => ({
|
||||||
entity_id: eid,
|
...hass.states[eid],
|
||||||
name: computeStateName(hass.states[eid]),
|
name: computeStateName(hass.states[eid]),
|
||||||
backgroundColor: getColorByIndex(idx),
|
backgroundColor: getColorByIndex(idx),
|
||||||
}));
|
}));
|
||||||
|
@ -439,6 +439,11 @@ export class HAFullCalendar extends LitElement {
|
|||||||
justify-content: initial;
|
justify-content: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding-right: var(--calendar-header-padding);
|
||||||
|
padding-left: var(--calendar-header-padding);
|
||||||
|
}
|
||||||
|
|
||||||
.navigation {
|
.navigation {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -513,7 +518,11 @@ export class HAFullCalendar extends LitElement {
|
|||||||
|
|
||||||
.fc-theme-standard .fc-scrollgrid {
|
.fc-theme-standard .fc-scrollgrid {
|
||||||
border: 1px solid var(--divider-color);
|
border: 1px solid var(--divider-color);
|
||||||
border-radius: var(--mdc-shape-small, 4px);
|
border-width: var(--calendar-border-width, 1px);
|
||||||
|
border-radius: var(
|
||||||
|
--calendar-border-radius,
|
||||||
|
var(--mdc-shape-small, 4px)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc-theme-standard td {
|
.fc-theme-standard td {
|
||||||
|
@ -1,22 +1,31 @@
|
|||||||
import "@material/mwc-checkbox";
|
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||||
import "@material/mwc-formfield";
|
import "@material/mwc-list";
|
||||||
import { mdiRefresh } from "@mdi/js";
|
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
|
||||||
|
import { mdiChevronDown, mdiRefresh } from "@mdi/js";
|
||||||
import {
|
import {
|
||||||
css,
|
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
html,
|
|
||||||
LitElement,
|
LitElement,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
|
css,
|
||||||
|
html,
|
||||||
|
nothing,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
import { storage } from "../../common/decorators/storage";
|
import { storage } from "../../common/decorators/storage";
|
||||||
import { HASSDomEvent } from "../../common/dom/fire_event";
|
import { HASSDomEvent } from "../../common/dom/fire_event";
|
||||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||||
|
import "../../components/ha-button";
|
||||||
|
import "../../components/ha-button-menu";
|
||||||
import "../../components/ha-card";
|
import "../../components/ha-card";
|
||||||
|
import "../../components/ha-check-list-item";
|
||||||
import "../../components/ha-icon-button";
|
import "../../components/ha-icon-button";
|
||||||
|
import type { HaListItem } from "../../components/ha-list-item";
|
||||||
import "../../components/ha-menu-button";
|
import "../../components/ha-menu-button";
|
||||||
|
import "../../components/ha-state-icon";
|
||||||
|
import "../../components/ha-svg-icon";
|
||||||
|
import "../../components/ha-two-pane-top-app-bar-fixed";
|
||||||
import {
|
import {
|
||||||
Calendar,
|
Calendar,
|
||||||
CalendarEvent,
|
CalendarEvent,
|
||||||
@ -26,7 +35,6 @@ import {
|
|||||||
import { haStyle } from "../../resources/styles";
|
import { haStyle } from "../../resources/styles";
|
||||||
import type { CalendarViewChanged, HomeAssistant } from "../../types";
|
import type { CalendarViewChanged, HomeAssistant } from "../../types";
|
||||||
import "./ha-full-calendar";
|
import "./ha-full-calendar";
|
||||||
import "../../components/ha-top-app-bar-fixed";
|
|
||||||
|
|
||||||
@customElement("ha-panel-calendar")
|
@customElement("ha-panel-calendar")
|
||||||
class PanelCalendar extends LitElement {
|
class PanelCalendar extends LitElement {
|
||||||
@ -35,6 +43,8 @@ class PanelCalendar extends LitElement {
|
|||||||
@property({ type: Boolean, reflect: true })
|
@property({ type: Boolean, reflect: true })
|
||||||
public narrow!: boolean;
|
public narrow!: boolean;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public mobile = false;
|
||||||
|
|
||||||
@state() private _calendars: Calendar[] = [];
|
@state() private _calendars: Calendar[] = [];
|
||||||
|
|
||||||
@state() private _events: CalendarEvent[] = [];
|
@state() private _events: CalendarEvent[] = [];
|
||||||
@ -51,6 +61,38 @@ class PanelCalendar extends LitElement {
|
|||||||
|
|
||||||
private _end?: Date;
|
private _end?: Date;
|
||||||
|
|
||||||
|
private _showPaneController = new ResizeController(this, {
|
||||||
|
callback: (entries: ResizeObserverEntry[]) =>
|
||||||
|
entries[0]?.contentRect.width > 750,
|
||||||
|
});
|
||||||
|
|
||||||
|
private _mql?: MediaQueryList;
|
||||||
|
|
||||||
|
private _headerHeight = 56;
|
||||||
|
|
||||||
|
public connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._mql = window.matchMedia(
|
||||||
|
"(max-width: 450px), all and (max-height: 500px)"
|
||||||
|
);
|
||||||
|
this._mql.addListener(this._setIsMobile);
|
||||||
|
this.mobile = this._mql.matches;
|
||||||
|
const computedStyles = getComputedStyle(this);
|
||||||
|
this._headerHeight = Number(
|
||||||
|
computedStyles.getPropertyValue("--header-height").replace("px", "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._mql?.removeListener(this._setIsMobile!);
|
||||||
|
this._mql = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setIsMobile = (ev: MediaQueryListEvent) => {
|
||||||
|
this.mobile = ev.matches;
|
||||||
|
};
|
||||||
|
|
||||||
public willUpdate(changedProps: PropertyValues): void {
|
public willUpdate(changedProps: PropertyValues): void {
|
||||||
super.willUpdate(changedProps);
|
super.willUpdate(changedProps);
|
||||||
if (!this.hasUpdated) {
|
if (!this.hasUpdated) {
|
||||||
@ -59,54 +101,73 @@ class PanelCalendar extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
|
const calendarItems = this._calendars.map(
|
||||||
|
(selCal) => html`
|
||||||
|
<ha-check-list-item
|
||||||
|
@request-selected=${this._requestSelected}
|
||||||
|
graphic="icon"
|
||||||
|
style=${styleMap({
|
||||||
|
"--mdc-theme-secondary": selCal.backgroundColor!,
|
||||||
|
})}
|
||||||
|
.value=${selCal.entity_id}
|
||||||
|
.selected=${!this._deSelectedCalendars.includes(selCal.entity_id)}
|
||||||
|
>
|
||||||
|
<ha-state-icon slot="graphic" .state=${selCal}></ha-state-icon>
|
||||||
|
${selCal.name}
|
||||||
|
</ha-check-list-item>
|
||||||
|
`
|
||||||
|
);
|
||||||
|
const showPane = this._showPaneController.value ?? !this.narrow;
|
||||||
return html`
|
return html`
|
||||||
<ha-top-app-bar-fixed>
|
<ha-two-pane-top-app-bar-fixed .pane=${showPane}>
|
||||||
<ha-menu-button
|
<ha-menu-button
|
||||||
slot="navigationIcon"
|
slot="navigationIcon"
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
></ha-menu-button>
|
></ha-menu-button>
|
||||||
<div slot="title">${this.hass.localize("panel.calendar")}</div>
|
|
||||||
|
${!showPane
|
||||||
|
? html`<ha-button-menu
|
||||||
|
slot="title"
|
||||||
|
class="lists"
|
||||||
|
multi
|
||||||
|
fixed
|
||||||
|
.noAnchor=${this.mobile}
|
||||||
|
.y=${this.mobile
|
||||||
|
? this._headerHeight / 2
|
||||||
|
: this._headerHeight / 4}
|
||||||
|
.x=${this.mobile ? 0 : undefined}
|
||||||
|
>
|
||||||
|
<ha-button slot="trigger">
|
||||||
|
${this.hass.localize("ui.components.calendar.my_calendars")}
|
||||||
|
<ha-svg-icon
|
||||||
|
slot="trailingIcon"
|
||||||
|
.path=${mdiChevronDown}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</ha-button>
|
||||||
|
${calendarItems}
|
||||||
|
</ha-button-menu>`
|
||||||
|
: html`<div slot="title">
|
||||||
|
${this.hass.localize("ui.components.calendar.my_calendars")}
|
||||||
|
</div>`}
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
slot="actionItems"
|
slot="actionItems"
|
||||||
.path=${mdiRefresh}
|
.path=${mdiRefresh}
|
||||||
.label=${this.hass.localize("ui.common.refresh")}
|
.label=${this.hass.localize("ui.common.refresh")}
|
||||||
@click=${this._handleRefresh}
|
@click=${this._handleRefresh}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
<div class="content">
|
${showPane
|
||||||
<div class="calendar-list">
|
? html`<mwc-list slot="pane" multi}>${calendarItems}</mwc-list>`
|
||||||
<div class="calendar-list-header">
|
: nothing}
|
||||||
${this.hass.localize("ui.components.calendar.my_calendars")}
|
<ha-full-calendar
|
||||||
</div>
|
.events=${this._events}
|
||||||
${this._calendars.map(
|
.calendars=${this._calendars}
|
||||||
(selCal) => html`
|
.narrow=${this.narrow}
|
||||||
<div>
|
.hass=${this.hass}
|
||||||
<mwc-formfield .label=${selCal.name}>
|
.error=${this._error}
|
||||||
<mwc-checkbox
|
@view-changed=${this._handleViewChanged}
|
||||||
style=${styleMap({
|
></ha-full-calendar>
|
||||||
"--mdc-theme-secondary": selCal.backgroundColor!,
|
</ha-two-pane-top-app-bar-fixed>
|
||||||
})}
|
|
||||||
.value=${selCal.entity_id}
|
|
||||||
.checked=${!this._deSelectedCalendars.includes(
|
|
||||||
selCal.entity_id
|
|
||||||
)}
|
|
||||||
@change=${this._handleToggle}
|
|
||||||
></mwc-checkbox>
|
|
||||||
</mwc-formfield>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<ha-full-calendar
|
|
||||||
.events=${this._events}
|
|
||||||
.calendars=${this._calendars}
|
|
||||||
.narrow=${this.narrow}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.error=${this._error}
|
|
||||||
@view-changed=${this._handleViewChanged}
|
|
||||||
></ha-full-calendar>
|
|
||||||
</div>
|
|
||||||
</ha-top-app-bar-fixed>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,46 +178,45 @@ class PanelCalendar extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _fetchEvents(
|
private async _fetchEvents(
|
||||||
start: Date,
|
start: Date | undefined,
|
||||||
end: Date,
|
end: Date | undefined,
|
||||||
calendars: Calendar[]
|
calendars: Calendar[]
|
||||||
): Promise<{ events: CalendarEvent[]; errors: string[] }> {
|
): Promise<{ events: CalendarEvent[]; errors: string[] }> {
|
||||||
if (!calendars.length) {
|
if (!calendars.length || !start || !end) {
|
||||||
return { events: [], errors: [] };
|
return { events: [], errors: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetchCalendarEvents(this.hass, start, end, calendars);
|
return fetchCalendarEvents(this.hass, start, end, calendars);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _handleToggle(ev): Promise<void> {
|
private async _requestSelected(ev: CustomEvent<RequestSelectedDetail>) {
|
||||||
const results = this._calendars.map(async (cal) => {
|
ev.stopPropagation();
|
||||||
if (ev.target.value !== cal.entity_id) {
|
const entityId = (ev.target as HaListItem).value;
|
||||||
return cal;
|
if (ev.detail.selected) {
|
||||||
|
this._deSelectedCalendars = this._deSelectedCalendars.filter(
|
||||||
|
(cal) => cal !== entityId
|
||||||
|
);
|
||||||
|
if (ev.detail.source === "interaction") {
|
||||||
|
// prevent adding the same calendar twice, an interaction event will be followed by a property event
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
const calendar = this._calendars.find(
|
||||||
const checked = ev.target.checked;
|
(cal) => cal.entity_id === entityId
|
||||||
|
);
|
||||||
if (checked) {
|
if (!calendar) {
|
||||||
const result = await this._fetchEvents(this._start!, this._end!, [cal]);
|
return;
|
||||||
this._events = [...this._events, ...result.events];
|
|
||||||
this._handleErrors(result.errors);
|
|
||||||
this._deSelectedCalendars = this._deSelectedCalendars.filter(
|
|
||||||
(deCal) => deCal !== cal.entity_id
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this._events = this._events.filter(
|
|
||||||
(event) => event.calendar !== cal.entity_id
|
|
||||||
);
|
|
||||||
this._deSelectedCalendars = [
|
|
||||||
...this._deSelectedCalendars,
|
|
||||||
cal.entity_id,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
const result = await this._fetchEvents(this._start, this._end, [
|
||||||
return cal;
|
calendar,
|
||||||
});
|
]);
|
||||||
|
this._events = [...this._events, ...result.events];
|
||||||
this._calendars = await Promise.all(results);
|
this._handleErrors(result.errors);
|
||||||
|
} else {
|
||||||
|
this._deSelectedCalendars = [...this._deSelectedCalendars, entityId];
|
||||||
|
this._events = this._events.filter(
|
||||||
|
(event) => event.calendar !== entityId
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _handleViewChanged(
|
private async _handleViewChanged(
|
||||||
@ -175,8 +235,8 @@ class PanelCalendar extends LitElement {
|
|||||||
|
|
||||||
private async _handleRefresh(): Promise<void> {
|
private async _handleRefresh(): Promise<void> {
|
||||||
const result = await this._fetchEvents(
|
const result = await this._fetchEvents(
|
||||||
this._start!,
|
this._start,
|
||||||
this._end!,
|
this._end,
|
||||||
this._selectedCalendars
|
this._selectedCalendars
|
||||||
);
|
);
|
||||||
this._events = result.events;
|
this._events = result.events;
|
||||||
@ -204,56 +264,42 @@ class PanelCalendar extends LitElement {
|
|||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
css`
|
css`
|
||||||
.content {
|
:host {
|
||||||
padding: 16px;
|
display: block;
|
||||||
display: flex;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:host(:not([narrow])) .content {
|
|
||||||
height: calc(100vh - var(--header-height));
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-list {
|
|
||||||
padding-right: 16px;
|
|
||||||
padding-inline-end: 16px;
|
|
||||||
padding-inline-start: initial;
|
|
||||||
min-width: 170px;
|
|
||||||
flex: 0 0 15%;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
|
||||||
direction: var(--direction);
|
|
||||||
}
|
|
||||||
|
|
||||||
.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 {
|
ha-full-calendar {
|
||||||
flex-grow: 1;
|
height: calc(100vh - var(--header-height));
|
||||||
|
--calendar-header-padding: 12px;
|
||||||
|
--calendar-border-radius: 0;
|
||||||
|
--calendar-border-width: 1px 0;
|
||||||
}
|
}
|
||||||
|
ha-button-menu ha-button {
|
||||||
:host([narrow]) ha-full-calendar {
|
--mdc-theme-primary: currentColor;
|
||||||
height: calc(100vh - 72px);
|
--mdc-typography-button-text-transform: none;
|
||||||
|
--mdc-typography-button-font-size: var(
|
||||||
|
--mdc-typography-headline6-font-size,
|
||||||
|
1.25rem
|
||||||
|
);
|
||||||
|
--mdc-typography-button-font-weight: var(
|
||||||
|
--mdc-typography-headline6-font-weight,
|
||||||
|
500
|
||||||
|
);
|
||||||
|
--mdc-typography-button-letter-spacing: var(
|
||||||
|
--mdc-typography-headline6-letter-spacing,
|
||||||
|
0.0125em
|
||||||
|
);
|
||||||
|
--mdc-typography-button-line-height: var(
|
||||||
|
--mdc-typography-headline6-line-height,
|
||||||
|
2rem
|
||||||
|
);
|
||||||
|
--button-height: 40px;
|
||||||
}
|
}
|
||||||
|
:host([mobile]) .lists {
|
||||||
:host([narrow]) .content {
|
--mdc-menu-min-width: 100vw;
|
||||||
flex-direction: column-reverse;
|
|
||||||
padding: 8px 0 0 0;
|
|
||||||
}
|
}
|
||||||
|
:host([mobile]) ha-button-menu {
|
||||||
:host([narrow]) .calendar-list {
|
--mdc-shape-medium: 0 0 var(--mdc-shape-medium)
|
||||||
margin-bottom: 24px;
|
var(--mdc-shape-medium);
|
||||||
width: 100%;
|
|
||||||
padding-right: 0;
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
24
yarn.lock
24
yarn.lock
@ -2106,7 +2106,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@lit-labs/ssr-dom-shim@npm:^1.0.0, @lit-labs/ssr-dom-shim@npm:^1.1.0":
|
"@lit-labs/observers@npm:2.0.1":
|
||||||
|
version: 2.0.1
|
||||||
|
resolution: "@lit-labs/observers@npm:2.0.1"
|
||||||
|
dependencies:
|
||||||
|
"@lit/reactive-element": ^2.0.0
|
||||||
|
checksum: 6c4518ee37678d86b263799590edd5c202a9f6800b52b52d2a0fc47e572d029c587fa9722108e449ab6bc7b1b1aee9e780b3e42ca2e91d63a43fc96541a80f98
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@lit-labs/ssr-dom-shim@npm:^1.0.0, @lit-labs/ssr-dom-shim@npm:^1.1.0, @lit-labs/ssr-dom-shim@npm:^1.1.2-pre.0":
|
||||||
version: 1.1.2
|
version: 1.1.2
|
||||||
resolution: "@lit-labs/ssr-dom-shim@npm:1.1.2"
|
resolution: "@lit-labs/ssr-dom-shim@npm:1.1.2"
|
||||||
checksum: 73fd787893851d4ec4aaa5c775405ed2aae4ca0891b2dd3c973b32c2f4bf70ada5481dd0224e52b786d037aa8a00052186ad1623c44551affd66f6409cca8da6
|
checksum: 73fd787893851d4ec4aaa5c775405ed2aae4ca0891b2dd3c973b32c2f4bf70ada5481dd0224e52b786d037aa8a00052186ad1623c44551affd66f6409cca8da6
|
||||||
@ -2132,6 +2141,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@lit/reactive-element@npm:^2.0.0":
|
||||||
|
version: 2.0.0
|
||||||
|
resolution: "@lit/reactive-element@npm:2.0.0"
|
||||||
|
dependencies:
|
||||||
|
"@lit-labs/ssr-dom-shim": ^1.1.2-pre.0
|
||||||
|
checksum: afa12f1cf72e8735cb7eaa51d428610785ee796882ca52108310e75ac54bbf5690da718c8bf85d042060f98c139ff0d5efd54f677a9d3fc4d794ad2e0f7a12c5
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@lokalise/node-api@npm:12.0.0":
|
"@lokalise/node-api@npm:12.0.0":
|
||||||
version: 12.0.0
|
version: 12.0.0
|
||||||
resolution: "@lokalise/node-api@npm:12.0.0"
|
resolution: "@lokalise/node-api@npm:12.0.0"
|
||||||
@ -2497,7 +2515,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@material/mwc-base@npm:^0.27.0":
|
"@material/mwc-base@npm:0.27.0, @material/mwc-base@npm:^0.27.0":
|
||||||
version: 0.27.0
|
version: 0.27.0
|
||||||
resolution: "@material/mwc-base@npm:0.27.0"
|
resolution: "@material/mwc-base@npm:0.27.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -9636,11 +9654,13 @@ __metadata:
|
|||||||
"@lezer/highlight": 1.1.6
|
"@lezer/highlight": 1.1.6
|
||||||
"@lit-labs/context": 0.4.1
|
"@lit-labs/context": 0.4.1
|
||||||
"@lit-labs/motion": 1.0.4
|
"@lit-labs/motion": 1.0.4
|
||||||
|
"@lit-labs/observers": 2.0.1
|
||||||
"@lit-labs/virtualizer": 2.0.7
|
"@lit-labs/virtualizer": 2.0.7
|
||||||
"@lokalise/node-api": 12.0.0
|
"@lokalise/node-api": 12.0.0
|
||||||
"@lrnwebcomponents/simple-tooltip": 7.0.18
|
"@lrnwebcomponents/simple-tooltip": 7.0.18
|
||||||
"@material/chips": =14.0.0-canary.53b3cad2f.0
|
"@material/chips": =14.0.0-canary.53b3cad2f.0
|
||||||
"@material/data-table": =14.0.0-canary.53b3cad2f.0
|
"@material/data-table": =14.0.0-canary.53b3cad2f.0
|
||||||
|
"@material/mwc-base": 0.27.0
|
||||||
"@material/mwc-button": 0.27.0
|
"@material/mwc-button": 0.27.0
|
||||||
"@material/mwc-checkbox": 0.27.0
|
"@material/mwc-checkbox": 0.27.0
|
||||||
"@material/mwc-circular-progress": 0.27.0
|
"@material/mwc-circular-progress": 0.27.0
|
||||||
|
Loading…
x
Reference in New Issue
Block a user