diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 977620c9dd..746fa30697 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -16,6 +16,9 @@ "runem.lit-plugin", "ms-python.vscode-pylance" ], + "containerEnv": { + "WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}" + }, "settings": { "terminal.integrated.shell.linux": "/bin/bash", "files.eol": "\n", diff --git a/.yarn/patches/@lit-labs/virtualizer/0.7.0.patch b/.yarn/patches/@lit-labs/virtualizer/0.7.0.patch deleted file mode 100644 index d7c1a6d703..0000000000 --- a/.yarn/patches/@lit-labs/virtualizer/0.7.0.patch +++ /dev/null @@ -1,1536 +0,0 @@ -diff --git a/CHANGELOG.md b/CHANGELOG.md -index c5ea17be2de03bbfe82bcc30aa28f7035cc93da0..0a21c3a1d32f5b8a2068c89df83f1b84ce4abbda 100644 ---- a/CHANGELOG.md -+++ b/CHANGELOG.md -@@ -1,16 +1,24 @@ -+## [0.7.0] - 2021-05-26 -+### Changed -+- Made significant changes and improvements to TypeScript types (work ongoing) -+ -+### Fixed -+- Fixed scrolling issue on iOS ([#54](https://github.com/PolymerLabs/uni-virtualizer/issues/54)) -+ - ## [0.6.0] - 2021-05-01 - - This is a stopgap release to unblock migrations to Lit 2.0 --- In the near future: -- - Source will move to the Lit monorepo -- - Subsequent releases will likely be as `@lit-labs/virtualizer` -+- In the near future, source will move to the Lit monorepo - - ### Changed - - Migrated to Lit 2.x - - ## [0.5.0] - 2021-05-01 - ### Changed -+- Changed npm package to `@lit-labs/virtualizer` - - Significant refactoring - - Now emits custom events, access data from `detail` object -+- `layout` property is now required for both the `lit-virtualizer` -+ element and the `scroll()` directive - - ### Added - - Support for older browsers (IE11, legacy Edge) -diff --git a/README.md b/README.md -index 86e6610be14ed8f375074a628298e8e9c8c50823..0ddf6a9d89e1a73514e0d11e92b8d007f864f887 100644 ---- a/README.md -+++ b/README.md -@@ -182,7 +182,7 @@ const handleEvent = (e) => { - } - - const example = (contacts) => html` --
-+
- ${scroll({ - items: contacts, - renderItem: ({ mediumText }) => html`

${mediumText}

`, -diff --git a/lib/lit-virtualizer.d.ts b/lib/lit-virtualizer.d.ts -index d6a63bd658542fee62c36f8658fc3561ab61d2e8..4c93ff899f483d4824d24024b0803c8838150d0a 100644 ---- a/lib/lit-virtualizer.d.ts -+++ b/lib/lit-virtualizer.d.ts -@@ -1,5 +1,5 @@ - import { LitElement, TemplateResult } from 'lit'; --import { Type, Layout, LayoutConfig } from './uni-virtualizer/lib/layouts/Layout.js'; -+import { LayoutSpecifier, Layout, LayoutConstructor } from './uni-virtualizer/lib/layouts/Layout.js'; - /** - * A LitElement wrapper of the scroll directive. - * -@@ -7,19 +7,19 @@ import { Type, Layout, LayoutConfig } from './uni-virtualizer/lib/layouts/Layout - * Pass an items array, renderItem method, and scroll target as properties - * to the element. - */ --export declare class LitVirtualizer extends LitElement { -- renderItem: (item: Item, index?: number) => TemplateResult; -- items: Array; -+export declare class LitVirtualizer extends LitElement { -+ renderItem?: ((item: any, index?: number) => TemplateResult); -+ items: Array; - scrollTarget: Element | Window; -- keyFunction: (item: any) => any; -+ keyFunction: ((item: unknown) => unknown) | undefined; - private _layout; - private _scrollToIndex; - createRenderRoot(): this; - /** - * The method used for rendering each item. - */ -- set layout(layout: Layout | Type | LayoutConfig); -- get layout(): Layout | Type | LayoutConfig; -+ set layout(layout: Layout | LayoutConstructor | LayoutSpecifier | null); -+ get layout(): Layout | LayoutConstructor | LayoutSpecifier | null; - /** - * Scroll to the specified index, placing that item at the given position - * in the scroll view. -@@ -29,7 +29,7 @@ export declare class LitVirtualizer extends LitElement { - } - declare global { - interface HTMLElementTagNameMap { -- 'lit-virtualizer': LitVirtualizer; -+ 'lit-virtualizer': LitVirtualizer; - } - } - //# sourceMappingURL=lit-virtualizer.d.ts.map -\ No newline at end of file -diff --git a/lib/lit-virtualizer.d.ts.map b/lib/lit-virtualizer.d.ts.map -index 65ca47b388981b8454619ef71c1eade0c847b6da..763404320ecd414b07e98aee537f55aeacb223ba 100644 ---- a/lib/lit-virtualizer.d.ts.map -+++ b/lib/lit-virtualizer.d.ts.map -@@ -1 +1 @@ --{"version":3,"file":"lit-virtualizer.d.ts","sourceRoot":"","sources":["../src/lib/lit-virtualizer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,UAAU,EAAE,cAAc,EAAE,MAAM,KAAK,CAAC;AAKvD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,yCAAyC,CAAC;AAErF;;;;;;GAMG;AACH,qBACa,cAAc,CAAC,IAAI,CAAE,SAAQ,UAAU;IAEhD,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,cAAc,CAAC;IAG3D,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IAGnB,YAAY,EAAE,OAAO,GAAG,MAAM,CAAQ;IAGtC,WAAW,EAAE,CAAC,IAAI,EAAC,GAAG,KAAK,GAAG,CAAC;IAE/B,OAAO,CAAC,OAAO,CAAsC;IAErD,OAAO,CAAC,cAAc,CAAoC;IAE1D,gBAAgB;IAahB;;OAEG;IAWH,IACI,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,YAAY,EAItD;IAED,IAAI,MAAM,IANS,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,YAAY,CAQtD;IAGD;;;OAGG;IACG,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAgB;IAO7D,MAAM,IAAI,cAAc;CAO3B;AAED,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,qBAAqB;QAC3B,iBAAiB,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;KAC9C;CACJ"} -\ No newline at end of file -+{"version":3,"file":"lit-virtualizer.d.ts","sourceRoot":"","sources":["../src/lib/lit-virtualizer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,UAAU,EAAE,cAAc,EAAE,MAAM,KAAK,CAAC;AAKvD,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,yCAAyC,CAAC;AAErG;;;;;;GAMG;AACH,qBACa,cAAe,SAAQ,UAAU;IAE1C,UAAU,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,cAAc,CAAC,CAAC;IAG7D,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,CAAM;IAG3B,YAAY,EAAE,OAAO,GAAG,MAAM,CAAQ;IAGtC,WAAW,EAAE,CAAC,CAAC,IAAI,EAAC,OAAO,KAAK,OAAO,CAAC,GAAG,SAAS,CAAa;IAEjE,OAAO,CAAC,OAAO,CAA6D;IAE5E,OAAO,CAAC,cAAc,CAAkD;IAExE,gBAAgB;IAahB;;OAEG;IAWH,IACI,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB,GAAG,eAAe,GAAG,IAAI,EAIrE;IAED,IAAI,MAAM,IAAI,MAAM,GAAG,iBAAiB,GAAG,eAAe,GAAG,IAAI,CAEhE;IAGD;;;OAGG;IACG,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAgB;IAO7D,MAAM,IAAI,cAAc;CAO3B;AAED,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,qBAAqB;QAC3B,iBAAiB,EAAE,cAAc,CAAC;KACrC;CACJ"} -\ No newline at end of file -diff --git a/lib/lit-virtualizer.js b/lib/lit-virtualizer.js -index d7846882e0c98212ee4be144728f26a4c7736fe8..7fb00024c2df2290559fc78db1ad13faf86a2c62 100644 ---- a/lib/lit-virtualizer.js -+++ b/lib/lit-virtualizer.js -@@ -1,4 +1,9 @@ --import { __decorate } from "tslib"; -+var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { -+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; -+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); -+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; -+ return c > 3 && r && Object.defineProperty(target, key, r), r; -+}; - import { html, LitElement } from 'lit'; - import { customElement } from 'lit/decorators/custom-element.js'; - import { property } from 'lit/decorators/property.js'; -@@ -14,7 +19,11 @@ import { scrollerRef } from './uni-virtualizer/lib/VirtualScroller.js'; - let LitVirtualizer = class LitVirtualizer extends LitElement { - constructor() { - super(...arguments); -+ this.items = []; - this.scrollTarget = this; -+ this.keyFunction = undefined; -+ this._layout = null; -+ this._scrollToIndex = null; - } - createRenderRoot() { - return this; -@@ -83,3 +92,4 @@ LitVirtualizer = __decorate([ - customElement('lit-virtualizer') - ], LitVirtualizer); - export { LitVirtualizer }; -+//# sourceMappingURL=lit-virtualizer.js.map -\ No newline at end of file -diff --git a/lib/lit-virtualizer.js.map b/lib/lit-virtualizer.js.map -new file mode 100644 -index 0000000000000000000000000000000000000000..54c885bd148d6bae8f5de85e3417a97a99035d24 ---- /dev/null -+++ b/lib/lit-virtualizer.js.map -@@ -0,0 +1 @@ -+{"version":3,"file":"lit-virtualizer.js","sourceRoot":"","sources":["../src/lib/lit-virtualizer.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,IAAI,EAAE,UAAU,EAAkB,MAAM,KAAK,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AACjE,OAAO,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AACtD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,WAAW,EAAoB,MAAM,0CAA0C,CAAC;AAGzF;;;;;;GAMG;AAEH,IAAa,cAAc,GAA3B,MAAa,cAAe,SAAQ,UAAU;IAA9C;;QAKI,UAAK,GAAmB,EAAE,CAAC;QAG3B,iBAAY,GAAqB,IAAI,CAAC;QAGtC,gBAAW,GAA4C,SAAS,CAAC;QAEzD,YAAO,GAAwD,IAAI,CAAC;QAEpE,mBAAc,GAA6C,IAAI,CAAC;IA0D5E,CAAC;IAxDG,gBAAgB;QACZ,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,gBAAgB;IAChB,0BAA0B;IAC1B,IAAI;IAEJ,qBAAqB;IACrB,2BAA2B;IAC3B,gDAAgD;IAChD,IAAI;IAEJ;;OAEG;IACH,qBAAqB;IACrB,+BAA+B;IAC/B,IAAI;IACJ,+BAA+B;IAC/B,4CAA4C;IAC5C,yCAAyC;IACzC,gCAAgC;IAChC,QAAQ;IACR,IAAI;IAGJ,IAAI,MAAM,CAAC,MAA2D;QAClE,qDAAqD;QACrD,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACtB,IAAI,CAAC,aAAa,EAAE,CAAC;IACzB,CAAC;IAED,IAAI,MAAM;QACN,OAAQ,IAAyB,CAAC,WAAW,CAAE,CAAC,MAAM,CAAC;IAC3D,CAAC;IAGD;;;OAGG;IACH,KAAK,CAAC,aAAa,CAAC,KAAa,EAAE,WAAmB,OAAO;QACzD,IAAI,CAAC,cAAc,GAAG,EAAC,KAAK,EAAE,QAAQ,EAAC,CAAC;QACxC,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,MAAM,IAAI,CAAC,cAAc,CAAC;QAC1B,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;IAC/B,CAAC;IAED,MAAM;QACF,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;QAC9D,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC;QAC5B,OAAO,IAAI,CAAA;cACL,MAAM,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,IAAI,CAAC,cAAc,EAAE,CAAC;SACzG,CAAC;IACN,CAAC;CACJ,CAAA;AAvEG;IADC,QAAQ,EAAE;kDACkD;AAG7D;IADC,QAAQ,CAAC,EAAC,SAAS,EAAE,KAAK,EAAC,CAAC;6CACF;AAG3B;IADC,QAAQ,CAAC,EAAC,SAAS,EAAE,KAAK,EAAC,CAAC;oDACS;AAGtC;IADC,QAAQ,EAAE;mDACsD;AAiCjE;IADC,QAAQ,CAAC,EAAC,SAAS,EAAC,KAAK,EAAC,CAAC;4CAK3B;AAhDQ,cAAc;IAD1B,aAAa,CAAC,iBAAiB,CAAC;GACpB,cAAc,CAyE1B;SAzEY,cAAc","sourcesContent":["import { html, LitElement, TemplateResult } from 'lit';\nimport { customElement } from 'lit/decorators/custom-element.js';\nimport { property } from 'lit/decorators/property.js';\nimport { scroll } from './scroll.js';\nimport { scrollerRef, ContainerElement } from './uni-virtualizer/lib/VirtualScroller.js';\nimport { LayoutSpecifier, Layout, LayoutConstructor } from './uni-virtualizer/lib/layouts/Layout.js';\n\n/**\n * A LitElement wrapper of the scroll directive.\n *\n * Import this module to declare the lit-virtualizer custom element.\n * Pass an items array, renderItem method, and scroll target as properties\n * to the element.\n */\n@customElement('lit-virtualizer')\nexport class LitVirtualizer extends LitElement {\n @property()\n renderItem?: ((item: any, index?: number) => TemplateResult);\n\n @property({attribute: false})\n items: Array = [];\n\n @property({attribute: false})\n scrollTarget: Element | Window = this;\n\n @property()\n keyFunction: ((item:unknown) => unknown) | undefined = undefined;\n\n private _layout: Layout | LayoutConstructor | LayoutSpecifier | null = null;\n\n private _scrollToIndex: {index: number, position: string} | null = null;\n \n createRenderRoot() {\n return this;\n }\n\n // get items() {\n // return this._items;\n // }\n\n // set items(items) {\n // this._items = items;\n // this._scroller.totalItems = items.length;\n // }\n\n /**\n * The method used for rendering each item.\n */\n // get renderItem() {\n // return this._renderItem;\n // }\n // set renderItem(renderItem) {\n // if (renderItem !== this.renderItem) {\n // this._renderItem = renderItem;\n // this.requestUpdate();\n // }\n // }\n\n @property({attribute:false})\n set layout(layout: Layout | LayoutConstructor | LayoutSpecifier | null) {\n // TODO (graynorton): Shouldn't have to set this here\n this._layout = layout;\n this.requestUpdate();\n }\n\n get layout(): Layout | LayoutConstructor | LayoutSpecifier | null {\n return (this as ContainerElement)[scrollerRef]!.layout;\n }\n \n \n /**\n * Scroll to the specified index, placing that item at the given position\n * in the scroll view.\n */\n async scrollToIndex(index: number, position: string = 'start') {\n this._scrollToIndex = {index, position};\n this.requestUpdate();\n await this.updateComplete;\n this._scrollToIndex = null;\n }\n\n render(): TemplateResult {\n const { items, renderItem, keyFunction, scrollTarget } = this;\n const layout = this._layout;\n return html`\n ${scroll({ items, renderItem, layout, keyFunction, scrollTarget, scrollToIndex: this._scrollToIndex })}\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'lit-virtualizer': LitVirtualizer;\n }\n}"]} -\ No newline at end of file -diff --git a/lib/scroll.d.ts b/lib/scroll.d.ts -index bca0558061c88063428fcb2e8468a86b31c31157..b75789ee00a5abbdae3a15a2bd2392a5b7a213bb 100644 ---- a/lib/scroll.d.ts -+++ b/lib/scroll.d.ts -@@ -1,19 +1,19 @@ - import { TemplateResult, ChildPart } from 'lit'; - import { PartInfo } from 'lit/directive.js'; - import { AsyncDirective } from 'lit/async-directive.js'; --import { Type, Layout, LayoutConfig } from './uni-virtualizer/lib/layouts/Layout.js'; --import { VirtualScroller } from './uni-virtualizer/lib/VirtualScroller.js'; -+import { Layout, LayoutConstructor, LayoutSpecifier } from './uni-virtualizer/lib/layouts/Layout.js'; -+import { VirtualScroller, ScrollToIndexValue } from './uni-virtualizer/lib/VirtualScroller.js'; - /** - * Configuration options for the scroll directive. - */ --interface ScrollConfig { -+interface ScrollConfig { - /** - * A function that returns a lit-html TemplateResult. It will be used - * to generate the DOM for each item in the virtual list. - */ -- renderItem?: (item: Item, index?: number) => TemplateResult; -- keyFunction?: (item: any) => any; -- layout?: Layout | Type | LayoutConfig; -+ renderItem?: (item: any, index?: number) => TemplateResult; -+ keyFunction?: (item: any) => unknown; -+ layout?: Layout | LayoutConstructor | LayoutSpecifier | null; - /** - * An element that receives scroll events for the virtual scroller. - */ -@@ -21,7 +21,7 @@ interface ScrollConfig { - /** - * The list of items to display via the renderItem function. - */ -- items?: Array; -+ items?: Array; - /** - * Limit for the number of items to display. Defaults to the length of the - * items array. -@@ -30,24 +30,23 @@ interface ScrollConfig { - /** - * Index and position of the item to scroll to. - */ -- scrollToIndex?: { -- index: number; -- position?: string; -- }; -+ scrollToIndex?: ScrollToIndexValue; - } -+export declare const defaultKeyFunction: (item: any) => any; -+export declare const defaultRenderItem: (item: any) => TemplateResult<1>; - declare class ScrollDirective extends AsyncDirective { -- container: HTMLElement; -- scroller: VirtualScroller; -+ container: HTMLElement | null; -+ scroller: VirtualScroller | null; - first: number; - last: number; - renderItem: (item: any, index?: number) => TemplateResult; -- keyFunction: (item: any) => any; -- items: Array; -+ keyFunction: (item: any) => unknown; -+ items: Array; - constructor(part: PartInfo); -- render(config?: ScrollConfig): unknown; -- update(part: ChildPart, [config]: [ScrollConfig]): unknown; -+ render(config?: ScrollConfig): unknown; -+ update(part: ChildPart, [config]: [ScrollConfig]): unknown; - private _initialize; - } --export declare const scroll: (config?: ScrollConfig) => import("lit/directive.js").DirectiveResult; -+export declare const scroll: (config?: ScrollConfig | undefined) => import("lit-html/directive").DirectiveResult; - export {}; - //# sourceMappingURL=scroll.d.ts.map -\ No newline at end of file -diff --git a/lib/scroll.d.ts.map b/lib/scroll.d.ts.map -index 64ae5c72b0d1342ef2018721bc93362b9ed1c9ff..5278da9ef97db92a43e22caa6a644990a194e313 100644 ---- a/lib/scroll.d.ts.map -+++ b/lib/scroll.d.ts.map -@@ -1 +1 @@ --{"version":3,"file":"scroll.d.ts","sourceRoot":"","sources":["../src/lib/scroll.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAW,SAAS,EAAE,MAAM,KAAK,CAAC;AACzD,OAAO,EAAa,QAAQ,EAAY,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,yCAAyC,CAAC;AACrF,OAAO,EAAE,eAAe,EAAoB,MAAM,0CAA0C,CAAC;AAE7F;;GAEG;AACH,UAAU,YAAY,CAAC,IAAI;IACvB;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,cAAc,CAAC;IAE5D,WAAW,CAAC,EAAE,CAAC,IAAI,EAAC,GAAG,KAAK,GAAG,CAAC;IAGhC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,YAAY,CAAC;IAE9C;;OAEG;IACH,YAAY,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAEhC;;OAEG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IAEpB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,aAAa,CAAC,EAAE;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAC,CAAC;CACpD;AAIH,cAAM,eAAgB,SAAQ,cAAc;IACxC,SAAS,EAAE,WAAW,CAAA;IACtB,QAAQ,EAAE,eAAe,CAAC,OAAO,EAAE,WAAW,CAAC,CAAA;IAC/C,KAAK,EAAE,MAAM,CAAI;IACjB,IAAI,EAAE,MAAM,CAAK;IACjB,UAAU,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,cAAc,CAAA;IACzD,WAAW,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,GAAG,CAAA;IAC/B,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAA;gBAEL,IAAI,EAAE,QAAQ;IAO1B,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC;IAclC,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAetD,OAAO,CAAC,WAAW;CAiBtB;AAED,eAAO,MAAM,MAAM,wGAA6B,CAAC"} -\ No newline at end of file -+{"version":3,"file":"scroll.d.ts","sourceRoot":"","sources":["../src/lib/scroll.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAW,SAAS,EAAQ,MAAM,KAAK,CAAC;AAC/D,OAAO,EAAa,QAAQ,EAAY,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AACrG,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,0CAA0C,CAAC;AAE/F;;GAEG;AACH,UAAU,YAAY;IAClB;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,cAAc,CAAC;IAE3D,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC;IAGrC,MAAM,CAAC,EAAE,MAAM,GAAG,iBAAiB,GAAG,eAAe,GAAG,IAAI,CAAC;IAE7D;;OAEG;IACH,YAAY,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAEhC;;OAEG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAEnB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,aAAa,CAAC,EAAE,kBAAkB,CAAC;CACpC;AAEH,eAAO,MAAM,kBAAkB,SAAU,GAAG,QAAS,CAAC;AACtD,eAAO,MAAM,iBAAiB,SAAU,GAAG,sBAA2C,CAAC;AAEvF,cAAM,eAAgB,SAAQ,cAAc;IACxC,SAAS,EAAE,WAAW,GAAG,IAAI,CAAO;IACpC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAAO;IACvC,KAAK,SAAI;IACT,IAAI,SAAK;IACT,UAAU,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,cAAc,CAAqB;IAC9E,WAAW,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,OAAO,CAAsB;IACzD,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,CAAK;gBAEd,IAAI,EAAE,QAAQ;IAO1B,MAAM,CAAC,MAAM,CAAC,EAAE,YAAY;IAc5B,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC;IAehD,OAAO,CAAC,WAAW;CAiBtB;AAED,eAAO,MAAM,MAAM,6GAA6B,CAAC"} -\ No newline at end of file -diff --git a/lib/scroll.js b/lib/scroll.js -index 9a0a5b4133c8a00def065e2c611db3d93b7208b0..33fd6bb63b352c8a49b057cce761280af45a57b7 100644 ---- a/lib/scroll.js -+++ b/lib/scroll.js -@@ -1,22 +1,28 @@ --import { nothing } from 'lit'; -+import { nothing, html } from 'lit'; - import { directive, PartType } from 'lit/directive.js'; - import { AsyncDirective } from 'lit/async-directive.js'; - import { repeat } from 'lit/directives/repeat.js'; - import { VirtualScroller } from './uni-virtualizer/lib/VirtualScroller.js'; --const defaultKeyFunction = item => item; -+export const defaultKeyFunction = (item) => item; -+export const defaultRenderItem = (item) => html `${JSON.stringify(item, null, 2)}`; - class ScrollDirective extends AsyncDirective { - constructor(part) { - super(part); -+ this.container = null; -+ this.scroller = null; - this.first = 0; - this.last = -1; -+ this.renderItem = defaultRenderItem; -+ this.keyFunction = defaultKeyFunction; -+ this.items = []; - if (part.type !== PartType.CHILD) { - throw new Error('The scroll directive can only be used in child expressions'); - } - } - render(config) { - if (config) { -- this.renderItem = config.renderItem; -- this.keyFunction = config.keyFunction; -+ this.renderItem = config.renderItem || this.renderItem; -+ this.keyFunction = config.keyFunction || this.keyFunction; - } - const itemsToRender = []; - if (this.first >= 0 && this.last >= this.first) { -@@ -30,9 +36,9 @@ class ScrollDirective extends AsyncDirective { - var _a; - if (this.scroller || this._initialize(part, config)) { - const { scroller } = this; -- this.items = scroller.items = config.items; -+ this.items = scroller.items = config.items || []; - scroller.totalItems = config.totalItems || ((_a = config.items) === null || _a === void 0 ? void 0 : _a.length) || 0; -- scroller.layout = config.layout; -+ scroller.layout = config.layout || null; - scroller.scrollTarget = config.scrollTarget || this.container; - if (config.scrollToIndex) { - scroller.scrollToIndex = config.scrollToIndex; -@@ -60,3 +66,4 @@ class ScrollDirective extends AsyncDirective { - } - } - export const scroll = directive(ScrollDirective); -+//# sourceMappingURL=scroll.js.map -\ No newline at end of file -diff --git a/lib/scroll.js.map b/lib/scroll.js.map -new file mode 100644 -index 0000000000000000000000000000000000000000..4cf7e26beb7d9c52e8af93dc789f96a06756123a ---- /dev/null -+++ b/lib/scroll.js.map -@@ -0,0 +1 @@ -+{"version":3,"file":"scroll.js","sourceRoot":"","sources":["../src/lib/scroll.ts"],"names":[],"mappings":"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","sourcesContent":["import { TemplateResult, nothing, ChildPart, html } from 'lit';\nimport { directive, PartInfo, PartType } from 'lit/directive.js';\nimport { AsyncDirective } from 'lit/async-directive.js';\nimport { repeat } from 'lit/directives/repeat.js';\nimport { Layout, LayoutConstructor, LayoutSpecifier } from './uni-virtualizer/lib/layouts/Layout.js';\nimport { VirtualScroller, ScrollToIndexValue } from './uni-virtualizer/lib/VirtualScroller.js';\n\n/**\n * Configuration options for the scroll directive.\n */\ninterface ScrollConfig {\n /**\n * A function that returns a lit-html TemplateResult. It will be used\n * to generate the DOM for each item in the virtual list.\n */\n renderItem?: (item: any, index?: number) => TemplateResult;\n\n keyFunction?: (item: any) => unknown;\n \n // TODO (graynorton): Document...\n layout?: Layout | LayoutConstructor | LayoutSpecifier | null;\n \n /**\n * An element that receives scroll events for the virtual scroller.\n */\n scrollTarget?: Element | Window;\n \n /**\n * The list of items to display via the renderItem function.\n */\n items?: Array;\n \n /**\n * Limit for the number of items to display. Defaults to the length of the\n * items array.\n */\n totalItems?: number;\n \n /**\n * Index and position of the item to scroll to.\n */\n scrollToIndex?: ScrollToIndexValue;\n }\n \nexport const defaultKeyFunction = (item: any) => item;\nexport const defaultRenderItem = (item: any) => html`${JSON.stringify(item, null, 2)}`;\n\nclass ScrollDirective extends AsyncDirective {\n container: HTMLElement | null = null\n scroller: VirtualScroller | null = null\n first = 0\n last = -1\n renderItem: (item: any, index?: number) => TemplateResult = defaultRenderItem;\n keyFunction: (item: any) => unknown = defaultKeyFunction;\n items: Array = []\n\n constructor(part: PartInfo) {\n super(part);\n if (part.type !== PartType.CHILD) {\n throw new Error('The scroll directive can only be used in child expressions');\n }\n }\n \n render(config?: ScrollConfig) {\n if (config) {\n this.renderItem = config.renderItem || this.renderItem;\n this.keyFunction = config.keyFunction || this.keyFunction;\n }\n const itemsToRender = [];\n if (this.first >= 0 && this.last >= this.first) {\n for (let i = this.first; i < this.last + 1; i++) {\n itemsToRender.push(this.items[i]);\n } \n }\n return repeat(itemsToRender, this.keyFunction || defaultKeyFunction, this.renderItem);\n }\n\n update(part: ChildPart, [config]: [ScrollConfig]) {\n if (this.scroller || this._initialize(part, config)) {\n const { scroller } = this;\n this.items = scroller!.items = config.items || [];\n scroller!.totalItems = config.totalItems || config.items?.length || 0;\n scroller!.layout = config.layout || null;\n scroller!.scrollTarget = config.scrollTarget || this.container;\n if (config.scrollToIndex) {\n scroller!.scrollToIndex = config.scrollToIndex;\n }\n return this.render(config); \n }\n return nothing;\n }\n\n private _initialize(part: ChildPart, config: ScrollConfig) {\n const container = this.container = part.parentNode as HTMLElement;\n if (container && container.nodeType === 1) {\n this.scroller = new VirtualScroller({ container });\n container.addEventListener('rangeChanged', (e: Event) => {\n this.first = (e as CustomEvent).detail.first;\n this.last = (e as CustomEvent).detail.last;\n this.setValue(this.render());\n });\n return true;\n }\n // TODO (GN): This seems to be needed in the case where the `scroll`\n // directive is used within the `LitVirtualizer` element. Figure out why\n // and see if there's a cleaner solution.\n Promise.resolve().then(() => this.update(part, [config]));\n return false;\n }\n}\n\nexport const scroll = directive(ScrollDirective);"]} -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/VirtualScroller.d.ts b/lib/uni-virtualizer/lib/VirtualScroller.d.ts -index 5806fc0ad4e5483f01ad7a1cf2451a31c1f0013a..70fd76efc606d2ff36d5635718ed3a172af6a43b 100644 ---- a/lib/uni-virtualizer/lib/VirtualScroller.d.ts -+++ b/lib/uni-virtualizer/lib/VirtualScroller.d.ts -@@ -1,18 +1,28 @@ --import { ItemBox, Type, Layout, LayoutConfig } from './layouts/Layout.js'; -+import { ItemBox, Layout, LayoutConstructor, LayoutSpecifier } from './layouts/Layout.js'; - export declare const scrollerRef: unique symbol; --declare global { -- interface Window { -- ShadyDOM?: any; -- } --} - export declare type RangeChangeEvent = { - first: number; - last: number; - firstVisible: number; - lastVisible: number; - }; --interface VirtualScrollerConfig { -- layout?: Layout | Type | LayoutConfig; -+interface ElementWithOptionalScrollerRef extends Element { -+ [scrollerRef]?: VirtualScroller; -+} -+interface ShadowRootWithOptionalScrollerRef extends ShadowRoot { -+ [scrollerRef]?: VirtualScroller; -+} -+declare type Container = ElementWithOptionalScrollerRef | ShadowRootWithOptionalScrollerRef; -+export declare type ContainerElement = ElementWithOptionalScrollerRef; -+declare type ChildMeasurements = { -+ [key: number]: ItemBox; -+}; -+export declare type ScrollToIndexValue = { -+ index: number; -+ position?: string; -+} | null; -+export interface VirtualScrollerConfig { -+ layout?: Layout | LayoutConstructor | LayoutSpecifier; - /** - * An element that receives scroll events for the virtual scroller. - */ -@@ -30,7 +40,7 @@ interface VirtualScrollerConfig { - * Extensions of this class must also override VirtualRepeater's DOM - * manipulation methods. - */ --export declare class VirtualScroller { -+export declare class VirtualScroller { - private _benchmarkStart; - /** - * Whether the layout should receive an updated viewport size on the next -@@ -70,7 +80,7 @@ export declare class VirtualScroller { - /** - * Containing element. Set by container. - */ -- protected _container: Element | ShadowRoot; -+ protected _container: Container | null; - /** - * The parent of all child nodes to be rendered. Set by container. - */ -@@ -80,11 +90,6 @@ export declare class VirtualScroller { - * restored when container is changed. - */ - private _containerInlineStyle; -- /** -- * Keep track of original container stylesheet, so it can be restored -- * when container is changed. -- */ -- private _containerStylesheet; - /** - * Size of the container. - */ -@@ -137,12 +142,10 @@ export declare class VirtualScroller { - * measured, and their dimensions passed to this callback. Use it to layout - * children as needed. - */ -- protected _measureCallback: (sizes: { -- [key: number]: ItemBox; -- }) => void; -- protected _measureChildOverride: (element: Element, item: object) => object; -+ protected _measureCallback: ((sizes: ChildMeasurements) => void) | null; -+ protected _measureChildOverride: ((element: Element, item: unknown) => ItemBox) | null; - constructor(config?: VirtualScrollerConfig); -- set items(items: any); -+ set items(items: Array | undefined); - /** - * The total number of items, regardless of the range, that can be rendered - * as child nodes. -@@ -152,15 +155,15 @@ export declare class VirtualScroller { - /** - * The parent of all child nodes to be rendered. - */ -- get container(): Element | ShadowRoot; -- set container(container: Element | ShadowRoot); -- get layout(): Layout | Type | LayoutConfig; -- set layout(layout: Layout | Type | LayoutConfig); -+ get container(): Container | null; -+ set container(container: Container | null); -+ get layout(): Layout | LayoutConstructor | LayoutSpecifier | null; -+ set layout(layout: Layout | LayoutConstructor | LayoutSpecifier | null); - startBenchmarking(): void; - stopBenchmarking(): { - timeElapsed: number; - virtualizationTime: number; -- }; -+ } | null; - private _measureChildren; - /** - * Returns the width, height, and margins of the given child. -@@ -177,19 +180,15 @@ export declare class VirtualScroller { - * Index and position of item to scroll to. The scroller will fix to that point - * until the user scrolls. - */ -- set scrollToIndex(newValue: { -- index: number; -- position?: string; -- }); -- protected _schedule(method: any): Promise; -+ set scrollToIndex(newValue: ScrollToIndexValue); -+ protected _schedule(method: Function): Promise; - _updateDOM(): Promise; - _updateLayout(): void; - private _handleScrollEvent; -- handleEvent(event: any): void; -+ handleEvent(event: CustomEvent): void; - private _initResizeObservers; -- private _applyContainerStyles; - private _createContainerSizer; -- get _children(): Array; -+ get _children(): Array; - private _updateView; - /** - * Styles the _sizer element or the container so that its size reflects the -diff --git a/lib/uni-virtualizer/lib/VirtualScroller.d.ts.map b/lib/uni-virtualizer/lib/VirtualScroller.d.ts.map -index fb02bc5f06fb90647f52463f7797a944ec8da8ba..917e1bf68689debf0f5402d5b5409b43302e2955 100644 ---- a/lib/uni-virtualizer/lib/VirtualScroller.d.ts.map -+++ b/lib/uni-virtualizer/lib/VirtualScroller.d.ts.map -@@ -1 +1 @@ --{"version":3,"file":"VirtualScroller.d.ts","sourceRoot":"","sources":["../../../src/lib/uni-virtualizer/lib/VirtualScroller.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAW,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEnF,eAAO,MAAM,WAAW,eAAwB,CAAC;AAGjD,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACZ,QAAQ,CAAC,EAAE,GAAG,CAAC;KAClB;CACF;AAsCD,oBAAY,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAGF,UAAU,qBAAqB;IAC7B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,YAAY,CAAC;IAE9C;;OAEG;IACH,YAAY,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAEhC;;OAEG;IACH,SAAS,EAAE,OAAO,GAAG,UAAU,CAAC;CACjC;AAED;;;;;;;GAOG;AACH,qBAAa,eAAe,CAAC,IAAI,EAAE,KAAK,SAAS,WAAW;IAC1D,OAAO,CAAC,eAAe,CAAQ;IAC/B;;;OAGG;IAGH,OAAO,CAAC,OAAO,CAAgB;IAE/B;;;OAGG;IACH,OAAO,CAAC,aAAa,CAAwB;IAE7C;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAqB;IAEnC;;;OAGG;IACH,OAAO,CAAC,WAAW,CAA4C;IAE/D;;;OAGG;IACH,OAAO,CAAC,UAAU,CAAqC;IAEvD;;OAEG;IACH,OAAO,CAAC,YAAY,CAA4C;IAGhE,OAAO,CAAC,kBAAkB,CAAa;IAEvC,OAAO,CAAC,aAAa,CAAoC;IAEzD,OAAO,CAAC,aAAa,CAAiB;IAEtC,OAAO,CAAC,aAAa,CAAiB;IAEtC,OAAO,CAAC,kBAAkB,CAAiB;IAE3C;;OAEG;IACH,SAAS,CAAC,UAAU,EAAE,OAAO,GAAG,UAAU,CAAQ;IAElD;;OAEG;IACH,OAAO,CAAC,iBAAiB,CAAiB;IAE1C;;;OAGG;IACH,OAAO,CAAC,qBAAqB,CAAQ;IAErC;;;OAGG;IACH,OAAO,CAAC,oBAAoB,CAAQ;IAEpC;;OAEG;IACH,OAAO,CAAC,cAAc,CAAyC;IAE/D;;OAEG;IACH,OAAO,CAAC,YAAY,CAAwB;IAE5C;;OAEG;IACH,OAAO,CAAC,WAAW,CAAwB;IAE3C,OAAO,CAAC,iBAAiB,CAA0B;IACnD,OAAO,CAAC,gBAAgB,CAAuB;IAC/C,OAAO,CAAC,wBAAwB,CAAkB;IAClD,OAAO,CAAC,kBAAkB,CAAkB;IAI5C,OAAO,CAAC,aAAa,CAAgC;IAErD;;OAEG;IACH,OAAO,CAAC,cAAc,CAA4C;IAElE;;OAEG;IACH,OAAO,CAAC,MAAM,CAAmB;IAEjC;;OAEG;IACH,OAAO,CAAC,WAAW,CAAgB;IAEnC;;;OAGG;IACH,SAAS,CAAC,MAAM,EAAE,MAAM,CAAK;IAE7B;;OAEG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,CAAK;IAE5B;;OAEG;IACH,OAAO,CAAC,aAAa,CAAS;IAE9B;;OAEG;IACH,OAAO,CAAC,YAAY,CAAS;IAE7B,SAAS,CAAC,UAAU,kBAAiB;IAErC;;;;OAIG;IACH,SAAS,CAAC,gBAAgB,EAAE,CAAC,KAAK,EAAE;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAC,KAAK,IAAI,CAAQ;IAE7E,SAAS,CAAC,qBAAqB,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,MAAM,CAAQ;gBAEvE,MAAM,CAAC,EAAE,qBAAqB;IAS1C,IAAI,KAAK,CAAC,KAAK,KAAA,EAMd;IAED;;;OAGG;IACH,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED,IAAI,UAAU,CAAC,GAAG,EAAE,MAAM,EAWzB;IAED;;OAEG;IACH,IAAI,SAAS,IAAI,OAAO,GAAG,UAAU,CAEpC;IAED,IAAI,SAAS,CAAC,SAAS,EAAE,OAAO,GAAG,UAAU,EAoE5C;IAID,IAAI,MAAM,IAAI,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,YAAY,CAEjD;IAED,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,YAAY,EAgEtD;IAID,iBAAiB;IAMjB,gBAAgB;;;;IAchB,OAAO,CAAC,gBAAgB;IAgBxB;;OAEG;IACH,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO;IAQxC;;;;OAIG;IACH,IAAI,YAAY,IAAI,OAAO,GAAG,MAAM,GAAG,IAAI,CAE1C;IACD,IAAI,YAAY,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,EAyB/C;IAED;;;OAGG;IACH,IAAI,aAAa,CAAC,QAAQ,EAAE;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAC,EAG7D;cAEe,SAAS,CAAC,MAAM,KAAA,GAAG,OAAO,CAAC,IAAI,CAAC;IAS1C,UAAU;IA0BhB,aAAa;IAoBb,OAAO,CAAC,kBAAkB;IAc1B,WAAW,CAAC,KAAK,KAAA;YA4BH,oBAAoB;IAWlC,OAAO,CAAC,qBAAqB;IAqB7B,OAAO,CAAC,qBAAqB;IAgB7B,IAAI,SAAS,IAAI,KAAK,CAAC,KAAK,CAAC,CAW5B;IAED,OAAO,CAAC,WAAW;IAiDnB;;;OAGG;IACH,OAAO,CAAC,cAAc;IAgBtB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;YAqBX,YAAY;IAkB1B,OAAO,CAAC,mBAAmB;IAS3B;;;OAGG;IACH,OAAO,CAAC,YAAY;IAgBpB,OAAO,CAAC,iBAAiB;IAWzB;;;OAGG;IACH,OAAO,CAAC,qBAAqB;YAMf,iBAAiB;IAc/B,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,oBAAoB;CAO7B"} -\ No newline at end of file -+{"version":3,"file":"VirtualScroller.d.ts","sourceRoot":"","sources":["../../../src/lib/uni-virtualizer/lib/VirtualScroller.ts"],"names":[],"mappings":"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"} -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/VirtualScroller.js b/lib/uni-virtualizer/lib/VirtualScroller.js -index deb122c252aca658b7aeae6edeee9f98de61ee2b..c2cf7431568224fddcd6af51f7352c5a6b116724 100644 ---- a/lib/uni-virtualizer/lib/VirtualScroller.js -+++ b/lib/uni-virtualizer/lib/VirtualScroller.js -@@ -1,28 +1,5 @@ - import getResizeObserver from './polyfillLoaders/ResizeObserver.js'; - export const scrollerRef = Symbol('scrollerRef'); --let nativeShadowDOM = 'attachShadow' in Element.prototype && (!('ShadyDOM' in window) || !window['ShadyDOM'].inUse); --const HOST_CLASSNAME = 'uni-virtualizer-host'; --let globalContainerStylesheet = null; --function containerStyles(hostSel, childSel) { -- return ` -- ${hostSel} { -- display: block; -- position: relative; -- contain: strict; -- height: 150px; -- overflow: auto; -- } -- ${childSel} { -- box-sizing: border-box; -- }`; --} --function attachGlobalContainerStylesheet() { -- if (!globalContainerStylesheet) { -- globalContainerStylesheet = document.createElement('style'); -- globalContainerStylesheet.textContent = containerStyles(`.${HOST_CLASSNAME}`, `.${HOST_CLASSNAME} > *`); -- document.head.appendChild(globalContainerStylesheet); -- } --} - /** - * Provides virtual scrolling boilerplate. - * -@@ -84,11 +61,6 @@ export class VirtualScroller { - * restored when container is changed. - */ - this._containerInlineStyle = null; -- /** -- * Keep track of original container stylesheet, so it can be restored -- * when container is changed. -- */ -- this._containerStylesheet = null; - /** - * Size of the container. - */ -@@ -128,6 +100,14 @@ export class VirtualScroller { - * Index of the last child in the range. - */ - this._last = 0; -+ /** -+ * Index of the first item intersecting the container element. -+ */ -+ this._firstVisible = 0; -+ /** -+ * Index of the last item intersecting the container element. -+ */ -+ this._lastVisible = 0; - this._scheduled = new WeakSet(); - /** - * Invoked at the end of each render cycle: children in the range are -@@ -143,7 +123,7 @@ export class VirtualScroller { - } - } - set items(items) { -- if (items !== this._items) { -+ if (Array.isArray(items) && items !== this._items) { - this._itemsChanged = true; - this._items = items; - this._schedule(this._updateLayout); -@@ -218,7 +198,16 @@ export class VirtualScroller { - this._containerElement = newEl; - if (newEl) { - this._containerInlineStyle = newEl.getAttribute('style') || null; -- this._applyContainerStyles(); -+ // https://github.com/PolymerLabs/uni-virtualizer/issues/104 -+ // Would rather set these CSS properties on the host using Shadow Root -+ // style scoping (and fall back to a global stylesheet where native -+ // Shadow DOM is not available), but this Mobile Safari bug is preventing -+ // that from working: https://bugs.webkit.org/show_bug.cgi?id=226195 -+ const style = newEl.style; -+ style.display = style.display || 'block'; -+ style.position = style.position || 'relative'; -+ style.overflow = style.overflow || 'auto'; -+ style.contain = style.contain || 'strict'; - if (newEl === this._scrollTarget) { - this._sizer = this._sizer || this._createContainerSizer(); - this._container.insertBefore(this._sizer, this._container.firstChild); -@@ -242,11 +231,12 @@ export class VirtualScroller { - if (this._layout === layout) { - return; - } -- let _layout, _config; -+ let _layout = null; -+ let _config = {}; - if (typeof layout === 'object') { - if (layout.type !== undefined) { - _layout = layout.type; -- delete layout.type; -+ // delete (layout as LayoutSpecifier).type; - } - _config = layout; - } -@@ -325,7 +315,7 @@ export class VirtualScroller { - const child = children[i]; - const idx = this._first + i; - if (this._itemsChanged || this._toBeMeasured.has(child)) { -- mm[idx] = fn.call(this, child, this._items[idx]); -+ mm[idx] = fn.call(this, child, this._items[idx] /*as unknown as object*/); - } - } - this._childMeasurements = mm; -@@ -438,7 +428,9 @@ export class VirtualScroller { - try { - window.performance.measure('uv-virtualizing', 'uv-start', 'uv-end'); - } -- catch (e) { } -+ catch (e) { -+ console.warn('Error measuring performance data: ', e); -+ } - window.performance.mark('uv-start'); - } - this._schedule(this._updateLayout); -@@ -479,26 +471,6 @@ export class VirtualScroller { - this._mutationObserver = new MutationObserver(this._observeMutations.bind(this)); - } - } -- _applyContainerStyles() { -- if (nativeShadowDOM) { -- if (this._containerStylesheet === null) { -- const sheet = (this._containerStylesheet = document.createElement('style')); -- sheet.textContent = containerStyles(':host', '::slotted(*)'); -- } -- const root = this._containerElement.shadowRoot || this._containerElement.attachShadow({ mode: 'open' }); -- const slot = root.querySelector('slot:not([name])'); -- root.appendChild(this._containerStylesheet); -- if (!slot) { -- root.appendChild(document.createElement('slot')); -- } -- } -- else { -- attachGlobalContainerStylesheet(); -- if (this._containerElement) { -- this._containerElement.classList.add(HOST_CLASSNAME); -- } -- } -- } - _createContainerSizer() { - const sizer = document.createElement('div'); - // When the scrollHeight is large, the height of this element might be -@@ -600,6 +572,7 @@ export class VirtualScroller { - if (child) { - const { top, left, width, height } = pos[key]; - child.style.position = 'absolute'; -+ child.style.boxSizing = 'border-box'; - child.style.transform = `translate(${left}px, ${top}px)`; - if (width !== undefined) { - child.style.width = width + 'px'; -@@ -683,7 +656,7 @@ export class VirtualScroller { - // this.requestRemeasure(); - } - _childrenSizeChanged(changes) { -- for (let change of changes) { -+ for (const change of changes) { - this._toBeMeasured.set(change.target, change.contentRect); - } - this._measureChildren(); -@@ -703,3 +676,4 @@ function getMarginValue(value) { - const float = value ? parseFloat(value) : NaN; - return Number.isNaN(float) ? 0 : float; - } -+//# sourceMappingURL=VirtualScroller.js.map -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/VirtualScroller.js.map b/lib/uni-virtualizer/lib/VirtualScroller.js.map -new file mode 100644 -index 0000000000000000000000000000000000000000..693e4099af0f40c1ce2589fec061afe13ae23bc2 ---- /dev/null -+++ b/lib/uni-virtualizer/lib/VirtualScroller.js.map -@@ -0,0 +1 @@ -+{"version":3,"file":"VirtualScroller.js","sourceRoot":"","sources":["../../../src/lib/uni-virtualizer/lib/VirtualScroller.ts"],"names":[],"mappings":"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","sourcesContent":["import getResizeObserver from './polyfillLoaders/ResizeObserver.js';\nimport { ItemBox, Margins, Layout, LayoutConstructor, LayoutSpecifier } from './layouts/Layout.js';\n\nexport const scrollerRef = Symbol('scrollerRef');\n\ninterface Range {\n first: number;\n last: number;\n num: number;\n remeasure: boolean;\n stable: boolean;\n firstVisible: number;\n lastVisible: number;\n}\n\nexport type RangeChangeEvent = {\n first: number;\n last: number;\n firstVisible: number;\n lastVisible: number;\n};\n\ninterface ElementWithOptionalScrollerRef extends Element {\n [scrollerRef]?: VirtualScroller\n}\n\ninterface ShadowRootWithOptionalScrollerRef extends ShadowRoot {\n [scrollerRef]?: VirtualScroller\n}\n\ntype Container = ElementWithOptionalScrollerRef | ShadowRootWithOptionalScrollerRef;\nexport type ContainerElement = ElementWithOptionalScrollerRef;\n\ntype VerticalScrollSize = {height: number};\ntype HorizontalScrollSize = {width: number};\ntype ScrollSize = VerticalScrollSize | HorizontalScrollSize;\n\ntype ChildMeasurements = {[key: number]: ItemBox};\n\nexport type ScrollToIndexValue = {index: number, position?: string} | null;\n\nexport interface VirtualScrollerConfig {\n layout?: Layout | LayoutConstructor | LayoutSpecifier;\n\n /**\n * An element that receives scroll events for the virtual scroller.\n */\n scrollTarget?: Element | Window;\n\n /**\n * The parent of all child nodes to be rendered.\n */\n container: Element | ShadowRoot;\n}\n\n/**\n * Provides virtual scrolling boilerplate.\n *\n * Extensions of this class must set container, layout, and scrollTarget.\n *\n * Extensions of this class must also override VirtualRepeater's DOM\n * manipulation methods.\n */\nexport class VirtualScroller {\n private _benchmarkStart: number | null = null;\n /**\n * Whether the layout should receive an updated viewport size on the next\n * render.\n */\n // private _needsUpdateView: boolean = false;\n\n private _layout: Layout | null = null;\n\n /**\n * The element that generates scroll events and defines the container\n * viewport. Set by scrollTarget.\n */\n private _scrollTarget: Element | null = null;\n\n /**\n * A sentinel element that sizes the container when it is a scrolling\n * element. This ensures the scroll bar accurately reflects the total\n * size of the list.\n */\n private _sizer: HTMLElement | null = null;\n\n /**\n * Layout provides these values, we set them on _render().\n * TODO @straversi: Can we find an XOR type, usable for the key here?\n */\n private _scrollSize: ScrollSize | null = null;\n\n /**\n * Difference between scroll target's current and required scroll offsets.\n * Provided by layout.\n */\n private _scrollErr: {left: number, top: number} | null = null;\n\n /**\n * A list of the positions (top, left) of the children in the current range.\n */\n private _childrenPos: Array<{top: number, left: number}> | null = null;\n\n // TODO: (graynorton): type\n private _childMeasurements: ChildMeasurements | null = null;\n\n private _toBeMeasured: Map = new Map();\n\n private _rangeChanged = true;\n\n private _itemsChanged = true;\n\n private _visibilityChanged = true;\n\n /**\n * Containing element. Set by container.\n */\n protected _container: Container | null = null;\n\n /**\n * The parent of all child nodes to be rendered. Set by container.\n */\n private _containerElement: ContainerElement | null = null;\n\n /**\n * Keep track of original inline style of the container, so it can be\n * restored when container is changed.\n */\n private _containerInlineStyle: string | null = null;\n\n /**\n * Size of the container.\n */\n private _containerSize: {width: number, height: number} | null = null;\n\n /**\n * Resize observer attached to container.\n */\n private _containerRO: ResizeObserver | null = null;\n\n /**\n * Resize observer attached to children.\n */\n private _childrenRO: ResizeObserver | null = null;\n\n private _mutationObserver: MutationObserver | null = null;\n private _mutationPromise: Promise | null = null;\n private _mutationPromiseResolver: Function | null = null;\n private _mutationsObserved = false;\n\n // TODO (graynorton): Rethink, per longer comment below\n\n private _loadListener = this._childLoaded.bind(this);\n\n /**\n * Index and position of item to scroll to.\n */\n private _scrollToIndex: ScrollToIndexValue = null;\n\n /**\n * Items to render. Set by items.\n */\n private _items: Array = [];\n\n /**\n * Total number of items to render. Set by totalItems.\n */\n private _totalItems: number | null = null;\n\n /**\n * Index of the first child in the range, not necessarily the first visible child.\n * TODO @straversi: Consider renaming these.\n */\n protected _first = 0;\n\n /**\n * Index of the last child in the range.\n */\n protected _last = 0;\n\n /**\n * Index of the first item intersecting the container element.\n */\n private _firstVisible = 0;\n\n /**\n * Index of the last item intersecting the container element.\n */\n private _lastVisible = 0;\n\n protected _scheduled = new WeakSet();\n\n /**\n * Invoked at the end of each render cycle: children in the range are\n * measured, and their dimensions passed to this callback. Use it to layout\n * children as needed.\n */\n protected _measureCallback: ((sizes: ChildMeasurements) => void) | null = null;\n\n protected _measureChildOverride: ((element: Element, item: unknown) => ItemBox) | null = null;\n\n constructor(config?: VirtualScrollerConfig) {\n this._first = -1;\n this._last = -1;\n\n if (config) {\n Object.assign(this, config);\n }\n }\n\n set items(items: Array | undefined) {\n if (Array.isArray(items) && items !== this._items) {\n this._itemsChanged = true;\n this._items = items;\n this._schedule(this._updateLayout);\n }\n }\n\n /**\n * The total number of items, regardless of the range, that can be rendered\n * as child nodes.\n */\n get totalItems(): number {\n return (this._totalItems === null ? this._items.length : this._totalItems);\n }\n\n set totalItems(num: number) {\n if (typeof num !== 'number' && num !== null) {\n throw new Error('New value must be a number.');\n }\n\n // TODO(valdrin) should we check if it is a finite number?\n // Technically, Infinity would break Layout, not VirtualRepeater.\n if (num !== this._totalItems) {\n this._totalItems = num;\n this._schedule(this._updateLayout);\n }\n }\n\n /**\n * The parent of all child nodes to be rendered.\n */\n get container(): Container | null {\n return this._container;\n }\n\n set container(container: Container | null) {\n if (container === this._container) {\n return;\n }\n\n if (this._container) {\n // Remove children from old container.\n // TODO (graynorton): Decide whether we'd rather fire an event to clear\n // the range and let the renderer take care of removing the DOM children\n this._children.forEach(child => child.parentNode!.removeChild(child));\n }\n\n this._container = container;\n\n this._schedule(this._updateLayout);\n\n this._initResizeObservers().then(() => {\n const oldEl = this._containerElement as HTMLElement;\n // Consider document fragments as shadowRoots.\n const newEl =\n (container && container.nodeType === Node.DOCUMENT_FRAGMENT_NODE) ?\n (container as ShadowRoot).host as HTMLElement :\n container as HTMLElement;\n if (oldEl === newEl) {\n return;\n }\n \n this._containerRO!.disconnect();\n this._containerSize = null;\n \n if (oldEl) {\n if (this._containerInlineStyle) {\n oldEl.setAttribute('style', this._containerInlineStyle!);\n } else {\n oldEl.removeAttribute('style');\n }\n this._containerInlineStyle = null;\n if (oldEl === this._scrollTarget) {\n oldEl.removeEventListener('scroll', this, {passive: true} as EventListenerOptions);\n this._sizer && this._sizer.remove();\n }\n oldEl.removeEventListener('load', this._loadListener, true);\n\n this._mutationObserver!.disconnect();\n } else {\n // First time container was setup, add listeners only now.\n addEventListener('scroll', this, {passive: true});\n }\n \n this._containerElement = newEl;\n \n if (newEl) {\n this._containerInlineStyle = newEl.getAttribute('style') || null;\n // https://github.com/PolymerLabs/uni-virtualizer/issues/104\n // Would rather set these CSS properties on the host using Shadow Root\n // style scoping (and fall back to a global stylesheet where native\n // Shadow DOM is not available), but this Mobile Safari bug is preventing\n // that from working: https://bugs.webkit.org/show_bug.cgi?id=226195\n const style = newEl.style as CSSStyleDeclaration & { contain: string };\n style.display = style.display || 'block';\n style.position = style.position || 'relative';\n style.overflow = style.overflow || 'auto';\n style.contain = style.contain || 'strict';\n if (newEl === this._scrollTarget) {\n this._sizer = this._sizer || this._createContainerSizer();\n this._container!.insertBefore(this._sizer, this._container!.firstChild);\n }\n this._schedule(this._updateLayout);\n this._containerRO!.observe(newEl);\n this._mutationObserver!.observe(newEl, { childList: true });\n this._mutationPromise = new Promise(resolve => this._mutationPromiseResolver = resolve);\n \n if (this._layout && this._layout.listenForChildLoadEvents) {\n newEl.addEventListener('load', this._loadListener, true);\n }\n }\n }); \n }\n\n // This will always actually return a layout instance,\n // but TypeScript wants the getter and setter types to be the same\n get layout(): Layout | LayoutConstructor | LayoutSpecifier | null {\n return this._layout;\n }\n\n set layout(layout: Layout | LayoutConstructor | LayoutSpecifier | null) {\n if (this._layout === layout) {\n return;\n }\n\n let _layout: LayoutConstructor | Layout | null = null;\n let _config: object = {};\n\n if (typeof layout === 'object') {\n if ((layout as LayoutSpecifier).type !== undefined) {\n _layout = (layout as LayoutSpecifier).type;\n // delete (layout as LayoutSpecifier).type;\n }\n _config = layout as object;\n }\n else {\n _layout = layout;\n }\n\n if (typeof _layout === 'function') {\n if (this._layout instanceof _layout) {\n if (_config) {\n this._layout!.config = _config;\n }\n return;\n }\n else {\n _layout = new _layout(_config);\n }\n }\n\n if (this._layout) {\n this._measureCallback = null;\n this._measureChildOverride = null;\n this._layout.removeEventListener('scrollsizechange', this);\n this._layout.removeEventListener('scrollerrorchange', this);\n this._layout.removeEventListener('itempositionchange', this);\n this._layout.removeEventListener('rangechange', this);\n delete this.container![scrollerRef];\n this.container!.removeEventListener('load', this._loadListener, true);\n // Reset container size so layout can get correct viewport size.\n if (this._containerElement) {\n this._sizeContainer(undefined);\n }\n }\n\n this._layout = _layout as Layout | null;\n\n if (this._layout) {\n if (this._layout.measureChildren && typeof this._layout.updateItemSizes === 'function') {\n if (typeof this._layout.measureChildren === 'function') {\n this._measureChildOverride = this._layout.measureChildren;\n }\n this._measureCallback = this._layout.updateItemSizes.bind(this._layout);\n }\n this._layout.addEventListener('scrollsizechange', this);\n this._layout.addEventListener('scrollerrorchange', this);\n this._layout.addEventListener('itempositionchange', this);\n this._layout.addEventListener('rangechange', this);\n this._container![scrollerRef] = this;\n if (this._layout.listenForChildLoadEvents) {\n this._container!.addEventListener('load', this._loadListener, true);\n }\n this._schedule(this._updateLayout);\n }\n }\n\n // TODO (graynorton): Rework benchmarking so that it has no API and\n // instead is always on except in production builds\n startBenchmarking() {\n if (this._benchmarkStart === null) {\n this._benchmarkStart = window.performance.now();\n }\n }\n\n stopBenchmarking() {\n if (this._benchmarkStart !== null) {\n const now = window.performance.now();\n const timeElapsed = now - this._benchmarkStart;\n const entries = performance.getEntriesByName('uv-virtualizing', 'measure');\n const virtualizationTime = entries\n .filter(e => e.startTime >= this._benchmarkStart! && e.startTime < now)\n .reduce((t, m) => t + m.duration, 0);\n this._benchmarkStart = null;\n return { timeElapsed, virtualizationTime };\n }\n return null;\n }\n\n private _measureChildren(): void {\n const mm: ChildMeasurements = {};\n const children = this._children;\n const fn = this._measureChildOverride || this._measureChild;\n for (let i = 0; i < children.length; i++) {\n const child = children[i];\n const idx = this._first + i;\n if (this._itemsChanged || this._toBeMeasured.has(child)) {\n mm[idx] = fn.call(this, child, this._items[idx] /*as unknown as object*/);\n }\n }\n this._childMeasurements = mm;\n this._schedule(this._updateLayout);\n this._toBeMeasured.clear();\n }\n\n /**\n * Returns the width, height, and margins of the given child.\n */\n _measureChild(element: Element): ItemBox {\n // offsetWidth doesn't take transforms in consideration, so we use\n // getBoundingClientRect which does.\n const {width, height} = element.getBoundingClientRect();\n return Object.assign({width, height}, getMargins(element));\n }\n\n\n /**\n * The element that generates scroll events and defines the container\n * viewport. The value `null` (default) corresponds to `window` as scroll\n * target.\n */\n get scrollTarget(): Element | Window | null {\n return this._scrollTarget;\n }\n set scrollTarget(target: Element | Window | null) {\n // Consider window as null.\n if (target === window) {\n target = null;\n }\n if (this._scrollTarget === target) {\n return;\n }\n this._sizeContainer(undefined);\n if (this._scrollTarget) {\n this._scrollTarget.removeEventListener('scroll', this, {passive: true} as EventListenerOptions);\n if (this._sizer && this._scrollTarget === this._containerElement) {\n this._sizer.remove();\n }\n }\n\n this._scrollTarget = target as (Element | null);\n\n if (target) {\n target.addEventListener('scroll', this, {passive: true});\n if (target === this._containerElement) {\n this._sizer = this._sizer || this._createContainerSizer();\n this._container!.insertBefore(this._sizer, this._container!.firstChild);\n }\n }\n }\n\n /**\n * Index and position of item to scroll to. The scroller will fix to that point\n * until the user scrolls.\n */\n set scrollToIndex(newValue: ScrollToIndexValue) {\n this._scrollToIndex = newValue;\n this._schedule(this._updateLayout);\n }\n\n protected async _schedule(method: Function): Promise {\n if (!this._scheduled.has(method)) {\n this._scheduled.add(method);\n await Promise.resolve();\n this._scheduled.delete(method);\n method.call(this);\n }\n }\n\n async _updateDOM() {\n const {_rangeChanged, _itemsChanged} = this;\n if (this._visibilityChanged) {\n this._notifyVisibility();\n this._visibilityChanged = false;\n }\n if (_rangeChanged || _itemsChanged) {\n this._notifyRange();\n this._rangeChanged = false;\n this._itemsChanged = false;\n await this._mutationPromise;\n }\n if (this._layout!.measureChildren) {\n this._children.forEach((child) => this._childrenRO!.observe(child));\n }\n this._positionChildren(this._childrenPos!);\n this._sizeContainer(this._scrollSize);\n if (this._scrollErr) {\n this._correctScrollError(this._scrollErr);\n this._scrollErr = null;\n }\n if (this._benchmarkStart && 'mark' in window.performance) {\n window.performance.mark('uv-end');\n }\n }\n\n _updateLayout() {\n this._layout!.totalItems = this._totalItems!;\n if (this._scrollToIndex !== null) {\n this._layout!.scrollToIndex(this._scrollToIndex.index, this._scrollToIndex!.position!);\n this._scrollToIndex = null;\n }\n this._updateView();\n if (this._childMeasurements !== null) {\n // If the layout has been changed, we may have measurements but no callback\n if (this._measureCallback) {\n this._measureCallback(this._childMeasurements);\n }\n this._childMeasurements = null;\n }\n this._layout!.reflowIfNeeded(this._itemsChanged);\n if (this._benchmarkStart && 'mark' in window.performance) {\n window.performance.mark('uv-end');\n }\n }\n\n private _handleScrollEvent() {\n if (this._benchmarkStart && 'mark' in window.performance) {\n try {\n window.performance.measure(\n 'uv-virtualizing',\n 'uv-start',\n 'uv-end'\n );\n } catch(e) {\n console.warn('Error measuring performance data: ', e);\n }\n window.performance.mark('uv-start');\n }\n this._schedule(this._updateLayout);\n }\n\n handleEvent(event: CustomEvent) {\n switch (event.type) {\n case 'scroll':\n if (!this._scrollTarget || event.target === this._scrollTarget) {\n this._handleScrollEvent();\n }\n break;\n case 'scrollsizechange':\n this._scrollSize = event.detail;\n this._schedule(this._updateDOM);\n break;\n case 'scrollerrorchange':\n this._scrollErr = event.detail;\n this._schedule(this._updateDOM);\n break;\n case 'itempositionchange':\n this._childrenPos = event.detail;\n this._schedule(this._updateDOM);\n break;\n case 'rangechange':\n this._adjustRange(event.detail);\n this._schedule(this._updateDOM);\n break;\n default:\n console.warn('event not handled', event);\n }\n }\n\n private async _initResizeObservers() {\n if (this._containerRO === null) {\n const ResizeObserver = await getResizeObserver();\n this._containerRO = new ResizeObserver(\n (entries: ResizeObserverEntry[]) => this._containerSizeChanged(entries[0].contentRect));\n this._childrenRO =\n new ResizeObserver(this._childrenSizeChanged.bind(this));\n this._mutationObserver = new MutationObserver(this._observeMutations.bind(this));\n }\n }\n\n private _createContainerSizer(): HTMLDivElement {\n const sizer = document.createElement('div');\n // When the scrollHeight is large, the height of this element might be\n // ignored. Setting content and font-size ensures the element has a size.\n Object.assign(sizer.style, {\n position: 'absolute',\n margin: '-2px 0 0 0',\n padding: 0,\n visibility: 'hidden',\n fontSize: '2px',\n });\n sizer.innerHTML = ' ';\n sizer.id = 'uni-virtualizer-spacer';\n return sizer;\n }\n\n get _children(): Array {\n const arr = [];\n let next = this.container!.firstElementChild as HTMLElement;\n while (next) {\n // Skip our spacer. TODO (graynorton): Feels a bit hacky. Anything better?\n if (next.id !== 'uni-virtualizer-spacer') {\n arr.push(next);\n }\n next = next.nextElementSibling as HTMLElement;\n }\n return arr;\n }\n\n private _updateView() {\n if (!this.container || !this._containerElement || !this._layout) {\n return;\n }\n let width, height, top, left;\n if (this._scrollTarget === this._containerElement && this._containerSize !== null) {\n width = this._containerSize.width;\n height = this._containerSize.height;\n left = this._containerElement.scrollLeft;\n top = this._containerElement.scrollTop;\n } else {\n const containerBounds = this._containerElement.getBoundingClientRect();\n const scrollBounds = this._scrollTarget ?\n this._scrollTarget.getBoundingClientRect() :\n {\n top: containerBounds.top + window.pageYOffset,\n left: containerBounds.left + window.pageXOffset,\n width: innerWidth,\n height: innerHeight\n };\n const scrollerWidth = scrollBounds.width;\n const scrollerHeight = scrollBounds.height;\n const xMin = Math.max(\n 0, Math.min(scrollerWidth, containerBounds.left - scrollBounds.left));\n const yMin = Math.max(\n 0, Math.min(scrollerHeight, containerBounds.top - scrollBounds.top));\n // TODO (graynorton): Direction is intended to be a layout-level concept, not a scroller-level concept,\n // so this feels like a factoring problem\n const xMax = this._layout.direction === 'vertical' ?\n Math.max(\n 0,\n Math.min(\n scrollerWidth, containerBounds.right - scrollBounds.left)) :\n scrollerWidth;\n const yMax = this._layout.direction === 'vertical' ?\n scrollerHeight :\n Math.max(\n 0,\n Math.min(\n scrollerHeight, containerBounds.bottom - scrollBounds.top));\n width = xMax - xMin;\n height = yMax - yMin;\n left = Math.max(0, -(containerBounds.left - scrollBounds.left));\n top = Math.max(0, -(containerBounds.top - scrollBounds.top));\n }\n this._layout.viewportSize = {width, height};\n this._layout.viewportScroll = {top, left};\n }\n\n /**\n * Styles the _sizer element or the container so that its size reflects the\n * total size of all items.\n */\n private _sizeContainer(size?: ScrollSize | null) {\n if (this._scrollTarget === this._containerElement) {\n const left = size && (size as HorizontalScrollSize).width ? (size as HorizontalScrollSize).width - 1 : 0;\n const top = size && (size as VerticalScrollSize).height ? (size as VerticalScrollSize).height - 1 : 0;\n if (this._sizer) {\n this._sizer.style.transform = `translate(${left}px, ${top}px)`;\n }\n } else {\n if (this._containerElement) {\n const style = (this._containerElement as HTMLElement).style;\n (style.minWidth as string | null) = size && (size as HorizontalScrollSize).width ? (size as HorizontalScrollSize).width + 'px' : null;\n (style.minHeight as string | null) = size && (size as VerticalScrollSize).height ? (size as VerticalScrollSize).height + 'px' : null; \n }\n }\n }\n\n /**\n * Sets the top and left transform style of the children from the values in\n * pos.\n */\n private _positionChildren(pos: Array<{top: number, left: number, width?: number, height?: number}>) {\n if (pos) {\n const children = this._children;\n Object.keys(pos).forEach((key) => {\n const idx = (key as unknown as number) - this._first;\n const child = children[idx];\n if (child) {\n const {top, left, width, height} = pos[key as unknown as number];\n child.style.position = 'absolute';\n child.style.boxSizing = 'border-box';\n child.style.transform = `translate(${left}px, ${top}px)`;\n if (width !== undefined) {\n child.style.width = width + 'px';\n }\n if (height !== undefined) {\n child.style.height = height + 'px';\n }\n }\n }); \n }\n }\n\n private async _adjustRange(range: Range) {\n const {_first, _last, _firstVisible, _lastVisible} = this;\n this._first = range.first;\n this._last = range.last;\n this._firstVisible = range.firstVisible;\n this._lastVisible = range.lastVisible;\n this._rangeChanged = (\n this._rangeChanged ||\n this._first !== _first ||\n this._last !== _last\n );\n this._visibilityChanged = (\n this._visibilityChanged ||\n this._firstVisible !== _firstVisible ||\n this._lastVisible !== _lastVisible\n );\n }\n\n private _correctScrollError(err: {top: number, left: number}) {\n if (this._scrollTarget) {\n this._scrollTarget.scrollTop -= err.top;\n this._scrollTarget.scrollLeft -= err.left;\n } else {\n window.scroll(window.pageXOffset - err.left, window.pageYOffset - err.top);\n }\n }\n\n /**\n * Emits a rangechange event with the current first, last, firstVisible, and\n * lastVisible.\n */\n private _notifyRange() {\n // TODO (graynorton): Including visibility here for backward compat, but \n // may decide to remove at some point. The rationale for separating is that\n // range change events are mainly intended for \"internal\" consumption by the\n // renderer, whereas visibility change events are mainly intended for \"external\"\n // consumption by application code.\n this._container!.dispatchEvent(\n new CustomEvent('rangeChanged', {detail:{\n first: this._first,\n last: this._last,\n firstVisible: this._firstVisible,\n lastVisible: this._lastVisible,\n }})\n );\n }\n\n private _notifyVisibility() {\n this._container!.dispatchEvent(\n new CustomEvent('visibilityChanged', {detail:{\n first: this._first,\n last: this._last,\n firstVisible: this._firstVisible,\n lastVisible: this._lastVisible,\n }})\n );\n }\n\n /**\n * Render and update the view at the next opportunity with the given\n * container size.\n */\n private _containerSizeChanged(size: {width: number, height: number}) {\n const {width, height} = size;\n this._containerSize = {width, height};\n this._schedule(this._updateLayout);\n }\n\n private async _observeMutations() {\n if (!this._mutationsObserved) {\n this._mutationsObserved = true;\n this._mutationPromiseResolver!();\n this._mutationPromise = new Promise(resolve => this._mutationPromiseResolver = resolve);\n this._mutationsObserved = false;\n }\n }\n\n // TODO (graynorton): Rethink how this works. Probably child loading is too specific\n // to have dedicated support for; might want some more generic lifecycle hooks for\n // layouts to use. Possibly handle measurement this way, too, or maybe that remains\n // a first-class feature?\n\n private _childLoaded() {\n // this.requestRemeasure();\n }\n\n private _childrenSizeChanged(changes: ResizeObserverEntry[]) {\n for (const change of changes) {\n this._toBeMeasured.set(change.target as HTMLElement, change.contentRect);\n }\n this._measureChildren();\n this._schedule(this._updateLayout);\n }\n}\n\nfunction getMargins(el: Element): Margins {\n const style = window.getComputedStyle(el);\n return {\n marginTop: getMarginValue(style.marginTop),\n marginRight: getMarginValue(style.marginRight),\n marginBottom: getMarginValue(style.marginBottom),\n marginLeft: getMarginValue(style.marginLeft),\n };\n}\n\nfunction getMarginValue(value: string): number {\n const float = value ? parseFloat(value) : NaN;\n return Number.isNaN(float) ? 0 : float;\n}"]} -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/layouts/Layout.d.ts b/lib/uni-virtualizer/lib/layouts/Layout.d.ts -index 42d282ba7ee5d75b7ba85afcf985c8ea47112ea4..425e2d89301a051c6eecb4e00b4dbdeec7032b2b 100644 ---- a/lib/uni-virtualizer/lib/layouts/Layout.d.ts -+++ b/lib/uni-virtualizer/lib/layouts/Layout.d.ts -@@ -8,7 +8,7 @@ export declare type Margins = { - marginBottom: number; - marginLeft: number; - }; --export declare type ItemBox = Size | Size & Margins; -+export declare type ItemBox = Size | (Size & Margins); - export declare type position = 'left' | 'top'; - export declare type Positions = { - left: number; -@@ -16,29 +16,28 @@ export declare type Positions = { - width?: number; - height?: number; - }; --export interface Type extends Function { -- new (...args: any[]): T; --} --export interface LayoutConfig { -- type?: Type; -+export declare type LayoutConstructor = new (config?: object) => Layout; -+export interface LayoutSpecifier { -+ type: LayoutConstructor; - } -+export declare type LayoutSpecifierFactory = (config?: object) => LayoutSpecifier; - export declare type ScrollDirection = 'vertical' | 'horizontal'; - /** - * Interface for layouts consumed by VirtualScroller or VirtualRepeater. - */ - export interface Layout { -- config: LayoutConfig; -+ config?: object; - totalItems: number; - direction: ScrollDirection; - viewportSize: Size; - viewportScroll: Positions; -- readonly measureChildren?: boolean | ((e: Element, i: object) => object); -+ readonly measureChildren?: boolean | ((e: Element, i: unknown) => ItemBox); - readonly listenForChildLoadEvents?: boolean; - updateItemSizes?: (sizes: { - [key: number]: ItemBox; - }) => void; -- addEventListener: any; -- removeEventListener: any; -+ addEventListener: Function; -+ removeEventListener: Function; - scrollToIndex: (index: number, position: string) => void; - /** - * Called by a VirtualRepeater or VirtualScroller when an update that -diff --git a/lib/uni-virtualizer/lib/layouts/Layout.d.ts.map b/lib/uni-virtualizer/lib/layouts/Layout.d.ts.map -index 470e5b26be4db353e4a9bafb815d41bcf208832f..13497a406bb9144e3f8b5c729bca351916c831bb 100644 ---- a/lib/uni-virtualizer/lib/layouts/Layout.d.ts.map -+++ b/lib/uni-virtualizer/lib/layouts/Layout.d.ts.map -@@ -1 +1 @@ --{"version":3,"file":"Layout.d.ts","sourceRoot":"","sources":["../../../../src/lib/uni-virtualizer/lib/layouts/Layout.ts"],"names":[],"mappings":"AAAA,oBAAY,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;AAC3C,oBAAY,IAAI,GAAG;KAChB,GAAG,IAAI,SAAS,GAAG,MAAM;CAC3B,CAAC;AAEF,oBAAY,OAAO,GAAG;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAA;CACnB,CAAC;AAEF,oBAAY,OAAO,GAAG,IAAI,GAAG,IAAI,GAAG,OAAO,CAAC;AAE5C,oBAAY,QAAQ,GAAG,MAAM,GAAG,KAAK,CAAC;AACtC,oBAAY,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAC;AAEF,MAAM,WAAW,IAAI,CAAC,CAAC,CAAE,SAAQ,QAAQ;IACvC,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;CACpB;AAED,oBAAY,eAAe,GAAG,UAAU,GAAG,YAAY,CAAC;AAExD;;GAEG;AACH,MAAM,WAAW,MAAM;IACrB,MAAM,EAAE,YAAY,CAAC;IAErB,UAAU,EAAE,MAAM,CAAC;IAEnB,SAAS,EAAE,eAAe,CAAC;IAE3B,YAAY,EAAE,IAAI,CAAC;IAEnB,cAAc,EAAE,SAAS,CAAC;IAE1B,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC,CAAC;IAEzE,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,CAAC;IAE5C,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE;QACxB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KACvB,KAAK,IAAI,CAAC;IAEX,gBAAgB,MAAC;IAEjB,mBAAmB,MAAC;IAEpB,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAEzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6CG;IACH,cAAc,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC1C"} -\ No newline at end of file -+{"version":3,"file":"Layout.d.ts","sourceRoot":"","sources":["../../../../src/lib/uni-virtualizer/lib/layouts/Layout.ts"],"names":[],"mappings":"AAAA,oBAAY,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;AAC3C,oBAAY,IAAI,GAAG;KAChB,GAAG,IAAI,SAAS,GAAG,MAAM;CAC3B,CAAC;AAEF,oBAAY,OAAO,GAAG;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAA;CACnB,CAAC;AAEF,oBAAY,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,GAAG,OAAO,CAAC,CAAC;AAE9C,oBAAY,QAAQ,GAAG,MAAM,GAAG,KAAK,CAAC;AACtC,oBAAY,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAC;AAGF,oBAAY,iBAAiB,GAAG,KAAI,MAAM,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;AAE/D,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,iBAAiB,CAAA;CACxB;AAED,oBAAY,sBAAsB,GAAG,CAAC,MAAM,CAAC,EAAE,MAAM,KAAK,eAAe,CAAC;AAG1E,oBAAY,eAAe,GAAG,UAAU,GAAG,YAAY,CAAC;AAExD;;GAEG;AACF,MAAM,WAAW,MAAM;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,UAAU,EAAE,MAAM,CAAC;IAEnB,SAAS,EAAE,eAAe,CAAC;IAE3B,YAAY,EAAE,IAAI,CAAC;IAEnB,cAAc,EAAE,SAAS,CAAC;IAE1B,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC;IAE3E,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,CAAC;IAE5C,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE;QACxB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KACvB,KAAK,IAAI,CAAC;IAEX,gBAAgB,EAAE,QAAQ,CAAC;IAE3B,mBAAmB,EAAE,QAAQ,CAAC;IAE9B,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAEzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6CG;IACH,cAAc,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC1C"} -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/layouts/Layout.js b/lib/uni-virtualizer/lib/layouts/Layout.js -index cb0ff5c3b541f646105198ee23ac0fc3d805023e..215b2c9498b60901b4bf2dd7c4a18803a4e4562c 100644 ---- a/lib/uni-virtualizer/lib/layouts/Layout.js -+++ b/lib/uni-virtualizer/lib/layouts/Layout.js -@@ -1 +1,2 @@ - export {}; -+//# sourceMappingURL=Layout.js.map -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/layouts/Layout.js.map b/lib/uni-virtualizer/lib/layouts/Layout.js.map -new file mode 100644 -index 0000000000000000000000000000000000000000..a95ae35dbf53896f9eba8ccbeeea0b06f9c8d3c0 ---- /dev/null -+++ b/lib/uni-virtualizer/lib/layouts/Layout.js.map -@@ -0,0 +1 @@ -+{"version":3,"file":"Layout.js","sourceRoot":"","sources":["../../../../src/lib/uni-virtualizer/lib/layouts/Layout.ts"],"names":[],"mappings":"","sourcesContent":["export type dimension = 'height' | 'width';\nexport type Size = {\n [key in dimension]: number\n};\n\nexport type Margins = {\n marginTop: number,\n marginRight: number,\n marginBottom: number,\n marginLeft: number\n};\n\nexport type ItemBox = Size | (Size & Margins);\n\nexport type position = 'left' | 'top';\nexport type Positions = {\n left: number,\n top: number,\n width?: number,\n height?: number\n};\n\n\nexport type LayoutConstructor = new(config?: object) => Layout;\n\nexport interface LayoutSpecifier {\n type: LayoutConstructor\n}\n\nexport type LayoutSpecifierFactory = (config?: object) => LayoutSpecifier;\n\n\nexport type ScrollDirection = 'vertical' | 'horizontal';\n\n/**\n * Interface for layouts consumed by VirtualScroller or VirtualRepeater.\n */\n export interface Layout {\n config?: object;\n \n totalItems: number;\n\n direction: ScrollDirection;\n\n viewportSize: Size;\n\n viewportScroll: Positions;\n\n readonly measureChildren?: boolean | ((e: Element, i: unknown) => ItemBox);\n\n readonly listenForChildLoadEvents?: boolean;\n\n updateItemSizes?: (sizes: {\n [key: number]: ItemBox\n }) => void;\n\n addEventListener: Function;\n\n removeEventListener: Function;\n\n scrollToIndex: (index: number, position: string) => void;\n\n /**\n * Called by a VirtualRepeater or VirtualScroller when an update that\n * potentially affects layout has occurred. For example, a viewport size\n * change.\n *\n * The layout is in turn responsible for dispatching events, as necessary,\n * to the VirtualRepeater or VirtualScroller. Each of the following events\n * represents an update that should be determined during a reflow. Dispatch\n * each event at maximum once during a single reflow.\n *\n * Events that should be dispatched:\n * - scrollsizechange\n * Dispatch when the total length of all items in the scrolling direction,\n * including spacing, changes.\n * detail: {\n * 'height' | 'width': number\n * }\n * - rangechange\n * Dispatch when the range of children that should be displayed changes\n * (based on layout calculations and the size of the container) or when\n * the first or last item to intersect the container changes.\n * detail: {\n * first: number,\n * last: number,\n * num: number,\n * stable: boolean,\n * remeasure: boolean,\n * firstVisible: number,\n * lastVisible: number,\n * }\n * - itempositionchange\n * Dispatch when the child positions change, for example due to a range\n * change.\n * detail {\n * [number]: {\n * left: number,\n * top: number\n * }\n * }\n * - scrollerrorchange\n * Dispatch when the set viewportScroll offset is not what it should be.\n * detail {\n * height: number,\n * width: number,\n * }\n */\n reflowIfNeeded: (force: boolean) => void;\n}"]} -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/layouts/Layout1d.d.ts b/lib/uni-virtualizer/lib/layouts/Layout1d.d.ts -index 843bab970bceb48331dbc28b30ec11c56f9940f2..cafaca7a6b9f0d7789eb17d2c753149dc5c99e60 100644 ---- a/lib/uni-virtualizer/lib/layouts/Layout1d.d.ts -+++ b/lib/uni-virtualizer/lib/layouts/Layout1d.d.ts -@@ -1,10 +1,19 @@ --import { Layout1dBase } from './Layout1dBase.js'; -+import { Layout1dBase, Layout1dBaseConfig } from './Layout1dBase.js'; - import { ItemBox, Positions, Size } from './Layout.js'; - declare type ItemBounds = { - pos: number; - size: number; - }; --export declare class Layout1d extends Layout1dBase { -+declare type Layout1dConstructor = { -+ prototype: Layout1d; -+ new (config?: Layout1dBaseConfig): Layout1d; -+}; -+declare type Layout1dSpecifier = Layout1dBaseConfig & { -+ type: Layout1dConstructor; -+}; -+declare type Layout1dSpecifierFactory = (config?: Layout1dBaseConfig) => Layout1dSpecifier; -+export declare const layout1d: Layout1dSpecifierFactory; -+export declare class Layout1d extends Layout1dBase { - /** - * Indices of children mapped to their (position and length) in the scrolling - * direction. Used to keep track of children that are in range. -@@ -24,11 +33,11 @@ export declare class Layout1d extends Layout1dBase { - * jumping to any point of the scroll size. We choose it once and stick with - * it until stable. _first and _last are deduced around it. - */ -- _anchorIdx: number; -+ _anchorIdx: number | null; - /** - * Position in the scrolling direction of the anchor child. - */ -- _anchorPos: number; -+ _anchorPos: number | null; - /** - * Whether all children in range were in range during the previous reflow. - */ -@@ -47,7 +56,6 @@ export declare class Layout1d extends Layout1dBase { - private _tMeasured; - private _measureChildren; - _estimate: boolean; -- constructor(config: any); - get measureChildren(): boolean; - /** - * Determine the average size of all children represented in the sizes -@@ -62,13 +70,13 @@ export declare class Layout1d extends Layout1dBase { - */ - _updateItemSize(): void; - _getMetrics(idx: number): ItemBox; -- _getPhysicalItem(idx: number): ItemBounds; -+ _getPhysicalItem(idx: number): ItemBounds | undefined; - _getSize(idx: number): number | undefined; - /** - * Returns the position in the scrolling direction of the item at idx. - * Estimates it if the item at idx is not in the DOM. - */ -- _getPosition(idx: any): number; -+ _getPosition(idx: number): number; - _calculateAnchor(lower: number, upper: number): number; - _getAnchor(lower: number, upper: number): number; - /** -diff --git a/lib/uni-virtualizer/lib/layouts/Layout1d.d.ts.map b/lib/uni-virtualizer/lib/layouts/Layout1d.d.ts.map -index 511045deb4569007c11e36a74ffdd4ca0dbb4931..16037434e706663ab3bc9fadb9e5c19090b5342f 100644 ---- a/lib/uni-virtualizer/lib/layouts/Layout1d.d.ts.map -+++ b/lib/uni-virtualizer/lib/layouts/Layout1d.d.ts.map -@@ -1 +1 @@ --{"version":3,"file":"Layout1d.d.ts","sourceRoot":"","sources":["../../../../src/lib/uni-virtualizer/lib/layouts/Layout1d.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAC,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAC,OAAO,EAAE,SAAS,EAAE,IAAI,EAAC,MAAM,aAAa,CAAC;AAErD,aAAK,UAAU,GAAG;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAA;CACb,CAAC;AAEF,qBAAa,QAAS,SAAQ,YAAY;IACxC;;;OAGG;IACH,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAa;IAEpD;;;OAGG;IACH,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAa;IAEvD;;OAEG;IACH,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAa;IAExC;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAQ;IAE1B;;OAEG;IACH,UAAU,EAAE,MAAM,CAAQ;IAE1B;;OAEG;IACH,OAAO,EAAE,OAAO,CAAQ;IAExB;;OAEG;IACH,eAAe,EAAE,OAAO,CAAS;IAEjC;;OAEG;IACH,OAAO,CAAC,UAAU,CAAa;IAE/B;;OAEG;IACH,OAAO,CAAC,UAAU,CAAa;IAE/B,OAAO,CAAC,gBAAgB,CAAQ;IAEhC,SAAS,EAAE,OAAO,CAAQ;gBAEd,MAAM,KAAA;IAIlB,IAAI,eAAe,YAElB;IAED;;;OAGG;IACH,eAAe,CAAC,KAAK,EAAE;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAC;IAmC/C;;;OAGG;IACH,eAAe;IAMf,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAIjC,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU;IAIzC,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAKzC;;;OAGG;IACH,YAAY,CAAC,GAAG,KAAA,GAAG,MAAM;IAKzB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM;IActD,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM;IAqDhD;;;OAGG;IACH,eAAe;IAQf;;OAEG;IACH,WAAW;IAeX,SAAS;IA+GT,eAAe,IAAI,MAAM;IAezB,iBAAiB;IAQjB,OAAO;IA0BP,iBAAiB;IAMjB;;OAEG;IACH,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS;IAOxC;;OAEG;IACH,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAO/B,gBAAgB;IAKhB,UAAU;CAMX"} -\ No newline at end of file -+{"version":3,"file":"Layout1d.d.ts","sourceRoot":"","sources":["../../../../src/lib/uni-virtualizer/lib/layouts/Layout1d.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAE,kBAAkB,EAAC,MAAM,mBAAmB,CAAC;AACnE,OAAO,EAAC,OAAO,EAAE,SAAS,EAAE,IAAI,EAAU,MAAM,aAAa,CAAC;AAE9D,aAAK,UAAU,GAAG;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAA;CACb,CAAC;AAEF,aAAK,mBAAmB,GAAG;IACzB,SAAS,EAAE,QAAQ,CAAC;IACpB,KAAI,MAAM,CAAC,EAAE,kBAAkB,GAAG,QAAQ,CAAA;CAC3C,CAAA;AAED,aAAK,iBAAiB,GAAG,kBAAkB,GAAG;IAC5C,IAAI,EAAE,mBAAmB,CAAA;CAC1B,CAAA;AAED,aAAK,wBAAwB,GAAG,CAAC,MAAM,CAAC,EAAE,kBAAkB,KAAK,iBAAiB,CAAC;AAEnF,eAAO,MAAM,QAAQ,EAAE,wBAEb,CAAC;AAGX,qBAAa,QAAS,SAAQ,YAAY,CAAC,kBAAkB,CAAC;IAC5D;;;OAGG;IACH,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAa;IAEpD;;;OAGG;IACH,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAa;IAEvD;;OAEG;IACH,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAa;IAExC;;;;OAIG;IACH,UAAU,EAAE,MAAM,GAAG,IAAI,CAAQ;IAEjC;;OAEG;IACH,UAAU,EAAE,MAAM,GAAG,IAAI,CAAQ;IAEjC;;OAEG;IACH,OAAO,UAAQ;IAEf;;OAEG;IACH,eAAe,UAAS;IAExB;;OAEG;IACH,OAAO,CAAC,UAAU,CAAK;IAEvB;;OAEG;IACH,OAAO,CAAC,UAAU,CAAK;IAEvB,OAAO,CAAC,gBAAgB,CAAQ;IAEhC,SAAS,UAAQ;IAUjB,IAAI,eAAe,YAElB;IAED;;;OAGG;IACH,eAAe,CAAC,KAAK,EAAE;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAC;IAmC/C;;;OAGG;IACH,eAAe;IAMf,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IASjC,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IAIrD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAKzC;;;OAGG;IACH,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;IAKjC,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM;IActD,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM;IAqDhD;;;OAGG;IACH,eAAe;IAQf;;OAEG;IACH,WAAW;IAeX,SAAS;IA+GT,eAAe,IAAI,MAAM;IAezB,iBAAiB;IAQjB,OAAO;IA0BP,iBAAiB;IAMjB;;OAEG;IACH,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS;IAOxC;;OAEG;IACH,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAO/B,gBAAgB;IAKhB,UAAU;CAMX"} -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/layouts/Layout1d.js b/lib/uni-virtualizer/lib/layouts/Layout1d.js -index 24e296295ce03663bdb90b19925ccb70e3586ddb..7a261b8b7d3a3bce975eef81a6558dc5ad561e66 100644 ---- a/lib/uni-virtualizer/lib/layouts/Layout1d.js -+++ b/lib/uni-virtualizer/lib/layouts/Layout1d.js -@@ -1,7 +1,10 @@ - import { Layout1dBase } from './Layout1dBase.js'; -+export const layout1d = (config) => Object.assign({ -+ type: Layout1d -+}, config); - export class Layout1d extends Layout1dBase { -- constructor(config) { -- super(config); -+ constructor() { -+ super(...arguments); - /** - * Indices of children mapped to their (position and length) in the scrolling - * direction. Used to keep track of children that are in range. -@@ -45,6 +48,11 @@ export class Layout1d extends Layout1dBase { - this._measureChildren = true; - this._estimate = true; - } -+ // protected _defaultConfig: Layout1dBaseConfig = Object.assign({}, super._defaultConfig, { -+ // }) -+ // constructor(config: Layout1dConfig) { -+ // super(config); -+ // } - get measureChildren() { - return this._measureChildren; - } -@@ -54,7 +62,7 @@ export class Layout1d extends Layout1dBase { - */ - updateItemSizes(sizes) { - Object.keys(sizes).forEach((key) => { -- const metrics = sizes[key], mi = this._getMetrics(Number(key)), prevSize = mi[this._sizeDim]; -+ const metrics = sizes[Number(key)], mi = this._getMetrics(Number(key)), prevSize = mi[this._sizeDim]; - // TODO(valdrin) Handle margin collapsing. - // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Box_Model/Mastering_margin_collapsing - mi.width = metrics.width + (metrics.marginLeft || 0) + -@@ -64,10 +72,10 @@ export class Layout1d extends Layout1dBase { - const size = mi[this._sizeDim]; - const item = this._getPhysicalItem(Number(key)); - if (item) { -- let delta; -+ let delta = 0; - if (size !== undefined) { - item.size = size; -- if (prevSize === undefined) { -+ if (prevSize === -1) { - delta = size; - this._nMeasured++; - } -@@ -93,7 +101,12 @@ export class Layout1d extends Layout1dBase { - Math.round(this._tMeasured / this._nMeasured); - } - _getMetrics(idx) { -- return (this._metrics[idx] = this._metrics[idx] || {}); -+ let metrics = this._metrics.get(idx); -+ if (metrics === undefined) { -+ metrics = { height: -1, width: -1 }; -+ this._metrics.set(idx, metrics); -+ } -+ return metrics; - } - _getPhysicalItem(idx) { - return this._newPhysicalItems.get(idx) || this._physicalItems.get(idx); -@@ -365,3 +378,4 @@ export class Layout1d extends Layout1dBase { - super._emitRange({ remeasure, stable }); - } - } -+//# sourceMappingURL=Layout1d.js.map -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/layouts/Layout1d.js.map b/lib/uni-virtualizer/lib/layouts/Layout1d.js.map -new file mode 100644 -index 0000000000000000000000000000000000000000..53e2375c71a8f5e1aa116c5d3152e392569607ef ---- /dev/null -+++ b/lib/uni-virtualizer/lib/layouts/Layout1d.js.map -@@ -0,0 +1 @@ -+{"version":3,"file":"Layout1d.js","sourceRoot":"","sources":["../../../../src/lib/uni-virtualizer/lib/layouts/Layout1d.ts"],"names":[],"mappings":"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","sourcesContent":["import {Layout1dBase, Layout1dBaseConfig} from './Layout1dBase.js';\nimport {ItemBox, Positions, Size, Margins} from './Layout.js';\n\ntype ItemBounds = {\n pos: number,\n size: number\n};\n\ntype Layout1dConstructor = {\n prototype: Layout1d,\n new(config?: Layout1dBaseConfig): Layout1d\n}\n\ntype Layout1dSpecifier = Layout1dBaseConfig & {\n type: Layout1dConstructor\n}\n\ntype Layout1dSpecifierFactory = (config?: Layout1dBaseConfig) => Layout1dSpecifier;\n\nexport const layout1d: Layout1dSpecifierFactory = (config?: Layout1dBaseConfig) => Object.assign({\n type: Layout1d\n}, config);\n\n\nexport class Layout1d extends Layout1dBase {\n /**\n * Indices of children mapped to their (position and length) in the scrolling\n * direction. Used to keep track of children that are in range.\n */\n _physicalItems: Map = new Map();\n\n /**\n * Used in tandem with _physicalItems to track children in range across\n * reflows.\n */\n _newPhysicalItems: Map = new Map();\n\n /**\n * Width and height of children by their index.\n */\n _metrics: Map = new Map();\n\n /**\n * anchorIdx is the anchor around which we reflow. It is designed to allow\n * jumping to any point of the scroll size. We choose it once and stick with\n * it until stable. _first and _last are deduced around it.\n */\n _anchorIdx: number | null = null;\n\n /**\n * Position in the scrolling direction of the anchor child.\n */\n _anchorPos: number | null = null;\n\n /**\n * Whether all children in range were in range during the previous reflow.\n */\n _stable = true;\n\n /**\n * Whether to remeasure children during the next reflow.\n */\n _needsRemeasure = false;\n\n /**\n * Number of children to lay out.\n */\n private _nMeasured = 0;\n\n /**\n * Total length in the scrolling direction of the laid out children.\n */\n private _tMeasured = 0;\n\n private _measureChildren = true;\n\n _estimate = true;\n\n // protected _defaultConfig: Layout1dBaseConfig = Object.assign({}, super._defaultConfig, {\n\n // })\n\n // constructor(config: Layout1dConfig) {\n // super(config);\n // }\n\n get measureChildren() {\n return this._measureChildren;\n }\n\n /**\n * Determine the average size of all children represented in the sizes\n * argument.\n */\n updateItemSizes(sizes: {[key: number]: ItemBox}) {\n Object.keys(sizes).forEach((key) => {\n const metrics = sizes[Number(key)], mi = this._getMetrics(Number(key)),\n prevSize = mi[this._sizeDim];\n\n // TODO(valdrin) Handle margin collapsing.\n // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Box_Model/Mastering_margin_collapsing\n mi.width = metrics.width + ((metrics as Margins).marginLeft || 0) +\n ((metrics as Margins).marginRight || 0);\n mi.height = metrics.height + ((metrics as Margins).marginTop || 0) +\n ((metrics as Margins).marginBottom || 0);\n\n const size = mi[this._sizeDim];\n const item = this._getPhysicalItem(Number(key));\n if (item) {\n let delta = 0;\n\n if (size !== undefined) {\n item.size = size;\n if (prevSize === -1) {\n delta = size;\n this._nMeasured++;\n } else {\n delta = size - prevSize;\n }\n }\n this._tMeasured = this._tMeasured + delta;\n }\n });\n if (this._nMeasured) {\n this._updateItemSize();\n this._scheduleReflow();\n }\n }\n\n /**\n * Set the average item size based on the total length and number of children\n * in range.\n */\n _updateItemSize() {\n // Keep integer values.\n this._itemSize[this._sizeDim] =\n Math.round(this._tMeasured / this._nMeasured);\n }\n\n _getMetrics(idx: number): ItemBox {\n let metrics = this._metrics.get(idx);\n if (metrics === undefined) {\n metrics = {height: -1, width: -1};\n this._metrics.set(idx, metrics);\n }\n return metrics;\n }\n\n _getPhysicalItem(idx: number): ItemBounds | undefined {\n return this._newPhysicalItems.get(idx) || this._physicalItems.get(idx);\n }\n\n _getSize(idx: number): number | undefined {\n const item = this._getPhysicalItem(idx);\n return item && item.size;\n }\n\n /**\n * Returns the position in the scrolling direction of the item at idx.\n * Estimates it if the item at idx is not in the DOM.\n */\n _getPosition(idx: number): number {\n const item = this._getPhysicalItem(idx);\n return item ? item.pos : (idx * (this._delta)) + this._spacing;\n }\n\n _calculateAnchor(lower: number, upper: number): number {\n if (lower === 0) {\n return 0;\n }\n if (upper > this._scrollSize - this._viewDim1) {\n return this._totalItems - 1;\n }\n return Math.max(\n 0,\n Math.min(\n this._totalItems - 1,\n Math.floor(((lower + upper) / 2) / this._delta)));\n }\n\n _getAnchor(lower: number, upper: number): number {\n if (this._physicalItems.size === 0) {\n return this._calculateAnchor(lower, upper);\n }\n if (this._first < 0) {\n console.error('_getAnchor: negative _first');\n return this._calculateAnchor(lower, upper);\n }\n if (this._last < 0) {\n console.error('_getAnchor: negative _last');\n return this._calculateAnchor(lower, upper);\n }\n\n const firstItem = this._getPhysicalItem(this._first),\n lastItem = this._getPhysicalItem(this._last),\n firstMin = firstItem!.pos, firstMax = firstMin + firstItem!.size,\n lastMin = lastItem!.pos, lastMax = lastMin + lastItem!.size;\n\n if (lastMax < lower) {\n // Window is entirely past physical items, calculate new anchor\n return this._calculateAnchor(lower, upper);\n }\n if (firstMin > upper) {\n // Window is entirely before physical items, calculate new anchor\n return this._calculateAnchor(lower, upper);\n }\n if (firstMin >= lower || firstMax >= lower) {\n // First physical item overlaps window, choose it\n return this._first;\n }\n if (lastMax <= upper || lastMin <= upper) {\n // Last physical overlaps window, choose it\n return this._last;\n }\n // Window contains a physical item, but not the first or last\n let maxIdx = this._last, minIdx = this._first;\n\n while (true) {\n const candidateIdx = Math.round((maxIdx + minIdx) / 2),\n candidate = this._physicalItems.get(candidateIdx),\n cMin = candidate!.pos, cMax = cMin + candidate!.size;\n\n if ((cMin >= lower && cMin <= upper) ||\n (cMax >= lower && cMax <= upper)) {\n return candidateIdx;\n } else if (cMax < lower) {\n minIdx = candidateIdx + 1;\n } else if (cMin > upper) {\n maxIdx = candidateIdx - 1;\n }\n }\n }\n\n /**\n * Updates _first and _last based on items that should be in the current\n * viewed range.\n */\n _getActiveItems() {\n if (this._viewDim1 === 0 || this._totalItems === 0) {\n this._clearItems();\n } else {\n this._getItems();\n }\n }\n\n /**\n * Sets the range to empty.\n */\n _clearItems() {\n this._first = -1;\n this._last = -1;\n this._physicalMin = 0;\n this._physicalMax = 0;\n const items = this._newPhysicalItems;\n this._newPhysicalItems = this._physicalItems;\n this._newPhysicalItems.clear();\n this._physicalItems = items;\n this._stable = true;\n }\n\n /*\n * Updates _first and _last based on items that should be in the given range.\n */\n _getItems() {\n const items = this._newPhysicalItems;\n let lower, upper;\n\n // The anchorIdx is the anchor around which we reflow. It is designed to\n // allow jumping to any point of the scroll size. We choose it once and\n // stick with it until stable. first and last are deduced around it.\n\n if (this._scrollToIndex >= 0) {\n // If we have a scrollToIndex, we anchor on the given\n // index and set the scroll position accordingly\n this._anchorIdx = this._scrollToIndex;\n this._anchorPos = this._getPosition(this._anchorIdx);\n this._scrollIfNeeded();\n lower = Math.max(0, this._scrollPosition - this._overhang);\n upper = Math.min(this._scrollSize, this._scrollPosition + this._viewDim1 + this._overhang);\n }\n else {\n // Otherwise, we find an appropriate index to anchor on\n // given the current scroll position\n upper = Math.min(\n this._scrollSize,\n this._scrollPosition + this._viewDim1 + this._overhang\n );\n lower = Math.max(0, upper - this._viewDim1 - (2 * this._overhang));\n\n if (this._anchorIdx === null || this._anchorPos === null) {\n this._anchorIdx = this._getAnchor(lower, upper);\n this._anchorPos = this._getPosition(this._anchorIdx); \n }\n }\n\n let anchorSize = this._getSize(this._anchorIdx);\n if (anchorSize === undefined) {\n anchorSize = this._itemDim1;\n }\n\n // Anchor might be outside bounds, so prefer correcting the error and keep\n // that anchorIdx.\n let anchorErr = 0;\n\n if (this._anchorPos + anchorSize + this._spacing < lower) {\n anchorErr = lower - (this._anchorPos + anchorSize + this._spacing);\n }\n\n if (this._anchorPos > upper) {\n anchorErr = upper - this._anchorPos;\n }\n\n if (anchorErr) {\n this._scrollPosition -= anchorErr;\n lower -= anchorErr;\n upper -= anchorErr;\n this._scrollError += anchorErr;\n }\n\n // TODO @straversi: If size is always itemDim1, then why keep track of it?\n items.set(this._anchorIdx, {pos: this._anchorPos, size: anchorSize});\n\n this._first = (this._last = this._anchorIdx);\n this._physicalMin = (this._physicalMax = this._anchorPos);\n\n this._stable = true;\n\n while (this._physicalMin > lower && this._first > 0) {\n let size = this._getSize(--this._first);\n if (size === undefined) {\n this._stable = false;\n size = this._itemDim1;\n }\n const pos = (this._physicalMin -= size + this._spacing);\n items.set(this._first, {pos, size});\n if (this._stable === false && this._estimate === false) {\n break;\n }\n }\n\n while (this._physicalMax < upper && this._last < this._totalItems) {\n let size = this._getSize(this._last);\n if (size === undefined) {\n this._stable = false;\n size = this._itemDim1;\n }\n items.set(this._last++, {pos: this._physicalMax, size});\n if (this._stable === false && this._estimate === false) {\n break;\n } else {\n this._physicalMax += size + this._spacing;\n }\n }\n\n this._last--;\n\n // This handles the cases where we were relying on estimated sizes.\n const extentErr = this._calculateError();\n if (extentErr) {\n this._physicalMin -= extentErr;\n this._physicalMax -= extentErr;\n this._anchorPos -= extentErr;\n this._scrollPosition -= extentErr;\n items.forEach((item) => item.pos -= extentErr);\n this._scrollError += extentErr;\n }\n\n if (this._stable) {\n this._newPhysicalItems = this._physicalItems;\n this._newPhysicalItems.clear();\n this._physicalItems = items;\n }\n }\n\n _calculateError(): number {\n if (this._first === 0) {\n return this._physicalMin;\n } else if (this._physicalMin <= 0) {\n return this._physicalMin - (this._first * this._delta);\n } else if (this._last === this._totalItems - 1) {\n return this._physicalMax - this._scrollSize;\n } else if (this._physicalMax >= this._scrollSize) {\n return (\n (this._physicalMax - this._scrollSize) +\n ((this._totalItems - 1 - this._last) * this._delta));\n }\n return 0;\n }\n\n _updateScrollSize() {\n // Reuse previously calculated physical max, as it might be higher than the\n // estimated size.\n super._updateScrollSize();\n this._scrollSize = Math.max(this._physicalMax, this._scrollSize);\n }\n\n // TODO: Can this be made to inherit from base, with proper hooks?\n _reflow() {\n const {_first, _last, _scrollSize} = this;\n\n this._updateScrollSize();\n this._getActiveItems();\n\n if (this._scrollSize !== _scrollSize) {\n this._emitScrollSize();\n }\n\n this._updateVisibleIndices();\n this._emitRange();\n if (this._first === -1 && this._last === -1) {\n this._resetReflowState();\n } else if (\n this._first !== _first || this._last !== _last ||\n this._needsRemeasure) {\n this._emitChildPositions();\n this._emitScrollError();\n } else {\n this._emitChildPositions();\n this._emitScrollError();\n this._resetReflowState();\n }\n }\n\n _resetReflowState() {\n this._anchorIdx = null;\n this._anchorPos = null;\n this._stable = true;\n }\n\n /**\n * Returns the top and left positioning of the item at idx.\n */\n _getItemPosition(idx: number): Positions {\n return {\n [this._positionDim]: this._getPosition(idx),\n [this._secondaryPositionDim]: 0,\n } as Positions;\n }\n\n /**\n * Returns the height and width of the item at idx.\n */\n _getItemSize(idx: number): Size {\n return {\n [this._sizeDim]: this._getSize(idx) || this._itemDim1,\n [this._secondarySizeDim]: this._itemDim2,\n } as Size;\n }\n\n _viewDim2Changed() {\n this._needsRemeasure = true;\n this._scheduleReflow();\n }\n\n _emitRange() {\n const remeasure = this._needsRemeasure;\n const stable = this._stable;\n this._needsRemeasure = false;\n super._emitRange({remeasure, stable});\n }\n}\n"]} -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/layouts/Layout1dBase.d.ts b/lib/uni-virtualizer/lib/layouts/Layout1dBase.d.ts -index 1ec5e4adbfbaa9ea3e158dd0ee59cdecf46b943c..35db11a215e139c4527c726fa219dc3fae044ddc 100644 ---- a/lib/uni-virtualizer/lib/layouts/Layout1dBase.d.ts -+++ b/lib/uni-virtualizer/lib/layouts/Layout1dBase.d.ts -@@ -1,5 +1,12 @@ --import { Layout, Positions, ScrollDirection, Size, dimension, position, LayoutConfig } from './Layout.js'; --export declare abstract class Layout1dBase implements Layout { -+import { Layout, Positions, ScrollDirection, Size, dimension, position } from './Layout.js'; -+declare type UpdateVisibleIndicesOptions = { -+ emit?: boolean; -+}; -+export interface Layout1dBaseConfig { -+ direction?: ScrollDirection; -+ totalItems?: number; -+} -+export declare abstract class Layout1dBase implements Layout { - /** - * The last set viewport scroll position. - */ -@@ -100,11 +107,11 @@ export declare abstract class Layout1dBase implements Layout { - */ - protected _overhang: number; - private _eventTarget; -- protected _spacingChanged: any; -- protected static _defaultConfig: LayoutConfig; -- constructor(config: any); -- set config(config: LayoutConfig); -- get config(): LayoutConfig; -+ protected _spacingChanged: boolean; -+ protected _defaultConfig: C; -+ constructor(config?: C); -+ set config(config: C); -+ get config(): C; - /** - * Maximum index of children + 1, to help estimate total height of the scroll - * space. -@@ -139,15 +146,15 @@ export declare abstract class Layout1dBase implements Layout { - /** - * Perform a reflow if one has been scheduled. - */ -- reflowIfNeeded(force: any): void; -+ reflowIfNeeded(force: boolean): void; - /** - * Scroll to the child at the given index, and the given position within that - * child. - */ -- scrollToIndex(index: any, position?: string): void; -- dispatchEvent(...args: any[]): Promise; -- addEventListener(...args: any[]): Promise; -- removeEventListener(...args: any[]): Promise; -+ scrollToIndex(index: number, position?: string): void; -+ dispatchEvent(evt: Event): Promise; -+ addEventListener(type: string, listener: EventListener | EventListenerObject | null, options?: boolean | AddEventListenerOptions | undefined): Promise; -+ removeEventListener(type: string, callback: EventListener | EventListenerObject | null, options?: boolean | EventListenerOptions | undefined): Promise; - /** - * Get the top and left positioning of the item at idx. - */ -@@ -156,7 +163,7 @@ export declare abstract class Layout1dBase implements Layout { - * Update _first and _last based on items that should be in the current - * range. - */ -- abstract _getActiveItems(): any; -+ abstract _getActiveItems(): void; - protected _itemDim2Changed(): void; - protected _viewDim2Changed(): void; - protected _updateLayout(): void; -@@ -189,7 +196,7 @@ export declare abstract class Layout1dBase implements Layout { - */ - protected _updateScrollSize(): void; - protected _scrollIfNeeded(): void; -- protected _emitRange(inProps?: any): void; -+ protected _emitRange(inProps?: unknown): void; - protected _emitScrollSize(): void; - protected _emitScrollError(): void; - /** -@@ -206,7 +213,8 @@ export declare abstract class Layout1dBase implements Layout { - * Find the indices of the first and last items to intersect the viewport. - * Emit a visibleindiceschange event when either index changes. - */ -- protected _updateVisibleIndices(options?: any): void; -+ protected _updateVisibleIndices(options?: UpdateVisibleIndicesOptions): void; - private _scrollPositionChanged; - } -+export {}; - //# sourceMappingURL=Layout1dBase.d.ts.map -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/layouts/Layout1dBase.d.ts.map b/lib/uni-virtualizer/lib/layouts/Layout1dBase.d.ts.map -index 29480cad4fb2479f471e00315614dcfacb36b07b..96c99bef00039d9fb14dc10e76bef00b227b912a 100644 ---- a/lib/uni-virtualizer/lib/layouts/Layout1dBase.d.ts.map -+++ b/lib/uni-virtualizer/lib/layouts/Layout1dBase.d.ts.map -@@ -1 +1 @@ --{"version":3,"file":"Layout1dBase.d.ts","sourceRoot":"","sources":["../../../../src/lib/uni-virtualizer/lib/layouts/Layout1dBase.ts"],"names":[],"mappings":"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"} -\ No newline at end of file -+{"version":3,"file":"Layout1dBase.d.ts","sourceRoot":"","sources":["../../../../src/lib/uni-virtualizer/lib/layouts/Layout1dBase.ts"],"names":[],"mappings":"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"} -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/layouts/Layout1dBase.js b/lib/uni-virtualizer/lib/layouts/Layout1dBase.js -index 94efc6df12444102eb08c9ed14dda66c8a1d7d72..995c860ad93bf52395ebef55532a9f7ae43e73fb 100644 ---- a/lib/uni-virtualizer/lib/layouts/Layout1dBase.js -+++ b/lib/uni-virtualizer/lib/layouts/Layout1dBase.js -@@ -28,6 +28,14 @@ export class Layout1dBase { - * top of the viewport. Value is a proportion of the item size. - */ - this._scrollToAnchor = 0; -+ /** -+ * The index of the first item intersecting the viewport. -+ */ -+ this._firstVisible = 0; -+ /** -+ * The index of the last item intersecting the viewport. -+ */ -+ this._lastVisible = 0; - this._eventTargetPromise = (EventTarget().then((Ctor) => { - this._eventTarget = new Ctor(); - })); -@@ -96,19 +104,20 @@ export class Layout1dBase { - // TODO (graynorton): Probably want to make this something we calculate based - // on viewport size, item size, other factors, possibly still with a dial of some kind - this._overhang = 1000; -- if (config) { -- this.config = config; -- } -+ this._eventTarget = null; -+ this._spacingChanged = false; -+ this._defaultConfig = { -+ direction: 'vertical' -+ }; -+ this.config = config || this._defaultConfig; - } - set config(config) { -- Object.assign(this, Object.assign({}, this.constructor._defaultConfig, config)); -+ Object.assign(this, Object.assign({}, this._defaultConfig, config)); - } - get config() { -- const config = {}; -- for (let key in this.constructor._defaultConfig) { -- config[key] = this[key]; -- } -- return config; -+ return { -+ direction: this.direction -+ }; - } - /** - * Maximum index of children + 1, to help estimate total height of the scroll -@@ -241,17 +250,17 @@ export class Layout1dBase { - } - this._scheduleReflow(); - } -- async dispatchEvent(...args) { -+ async dispatchEvent(evt) { - await this._eventTargetPromise; -- this._eventTarget.dispatchEvent(...args); -+ this._eventTarget.dispatchEvent(evt); - } -- async addEventListener(...args) { -+ async addEventListener(type, listener, options) { - await this._eventTargetPromise; -- this._eventTarget.addEventListener(...args); -+ this._eventTarget.addEventListener(type, listener, options); - } -- async removeEventListener(...args) { -+ async removeEventListener(type, callback, options) { - await this._eventTargetPromise; -- this._eventTarget.removeEventListener(...args); -+ this._eventTarget.removeEventListener(type, callback, options); - } - _itemDim2Changed() { - // Override -@@ -449,4 +458,4 @@ export class Layout1dBase { - } - } - } --Layout1dBase._defaultConfig = {}; -+//# sourceMappingURL=Layout1dBase.js.map -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/layouts/Layout1dBase.js.map b/lib/uni-virtualizer/lib/layouts/Layout1dBase.js.map -new file mode 100644 -index 0000000000000000000000000000000000000000..be6a74593fea802779d275aef73f85858458c307 ---- /dev/null -+++ b/lib/uni-virtualizer/lib/layouts/Layout1dBase.js.map -@@ -0,0 +1 @@ -+{"version":3,"file":"Layout1dBase.js","sourceRoot":"","sources":["../../../../src/lib/uni-virtualizer/lib/layouts/Layout1dBase.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,mCAAmC,CAAC;AAY5D,MAAM,OAAgB,YAAY;IAwIhC,YAAY,MAAU;QAvItB;;WAEG;QACK,kBAAa,GAAc,EAAC,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAC,CAAC;QAErD;;WAEG;QACK,eAAU,GAAoB,UAAU,CAAC;QAEjD;;WAEG;QACK,kBAAa,GAAS,EAAC,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAC,CAAC;QAEpD;;WAEG;QACK,mBAAc,GAAG,KAAK,CAAC;QAEvB,yBAAoB,GAAG,KAAK,CAAC;QAErC;;;WAGG;QACO,mBAAc,GAAG,CAAC,CAAC,CAAC;QAE9B;;;WAGG;QACK,oBAAe,GAAG,CAAC,CAAC;QAE5B;;WAEG;QACK,kBAAa,GAAG,CAAC,CAAC;QAE1B;;WAEG;QACK,iBAAY,GAAG,CAAC,CAAC;QAEjB,wBAAmB,GAAkB,CAAC,WAAW,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;YACxE,IAAI,CAAC,YAAY,GAAG,IAAI,IAAI,EAAE,CAAC;QACjC,CAAC,CAAC,CAAC,CAAC;QAEJ;;WAEG;QACO,iBAAY,GAAG,CAAC,CAAC;QAE3B;;WAEG;QACO,iBAAY,GAAG,CAAC,CAAC;QAE3B;;WAEG;QACO,WAAM,GAAG,CAAC,CAAC,CAAC;QAEtB;;WAEG;QACO,UAAK,GAAG,CAAC,CAAC,CAAC;QAErB;;WAEG;QACO,cAAS,GAAS,EAAC,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAC,CAAC;QAEtD;;WAEG;QACO,aAAQ,GAAG,CAAC,CAAC;QAEvB;;WAEG;QACO,aAAQ,GAAc,QAAQ,CAAC;QAEzC;;WAEG;QACO,sBAAiB,GAAc,OAAO,CAAC;QAEjD;;WAEG;QACO,iBAAY,GAAa,KAAK,CAAC;QAEzC;;WAEG;QACO,0BAAqB,GAAa,MAAM,CAAC;QAEnD;;WAEG;QACO,oBAAe,GAAG,CAAC,CAAC;QAE9B;;;WAGG;QACO,iBAAY,GAAG,CAAC,CAAC;QAE3B;;;WAGG;QACO,gBAAW,GAAG,CAAC,CAAC;QAE1B;;WAEG;QACO,gBAAW,GAAG,CAAC,CAAC;QAE1B;;;WAGG;QACH,6EAA6E;QAC7E,sFAAsF;QAC5E,cAAS,GAAG,IAAI,CAAC;QAEnB,iBAAY,GAAuB,IAAI,CAAC;QACtC,oBAAe,GAAG,KAAK,CAAC;QAExB,mBAAc,GAAM;YAC5B,SAAS,EAAE,UAAU;SACjB,CAAA;QAGJ,IAAI,CAAC,MAAM,GAAG,MAAM,IAAI,IAAI,CAAC,cAAc,CAAC;IAC9C,CAAC;IAED,IAAI,MAAM,CAAC,MAAS;QAClB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC,CAAC;IACtE,CAAC;IAED,IAAI,MAAM;QACR,OAAO;YACL,SAAS,EAAE,IAAI,CAAC,SAAS;SACrB,CAAC;IACT,CAAC;IAED;;;OAGG;IACH,IAAI,UAAU;QACZ,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;IACD,IAAI,UAAU,CAAC,GAAG;QAChB,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QACzB,IAAI,IAAI,KAAK,IAAI,CAAC,WAAW,EAAE;YAC7B,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;YACxB,IAAI,CAAC,eAAe,EAAE,CAAC;SACxB;IACH,CAAC;IAED;;OAEG;IACH,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IACD,IAAI,SAAS,CAAC,GAAG;QACf,gDAAgD;QAChD,GAAG,GAAG,CAAC,GAAG,KAAK,YAAY,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC;QAChD,IAAI,GAAG,KAAK,IAAI,CAAC,UAAU,EAAE;YAC3B,IAAI,CAAC,UAAU,GAAG,GAAG,CAAC;YACtB,IAAI,CAAC,QAAQ,GAAG,CAAC,GAAG,KAAK,YAAY,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC;YAC5D,IAAI,CAAC,iBAAiB,GAAG,CAAC,GAAG,KAAK,YAAY,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC;YACrE,IAAI,CAAC,YAAY,GAAG,CAAC,GAAG,KAAK,YAAY,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;YAC5D,IAAI,CAAC,qBAAqB,GAAG,CAAC,GAAG,KAAK,YAAY,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC;YACrE,IAAI,CAAC,qBAAqB,EAAE,CAAC;SAC9B;IACH,CAAC;IAED;;OAEG;IACH,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,SAAS,CAAC;IACxB,CAAC;IACD,IAAI,QAAQ,CAAC,IAAI;QACf,MAAM,EAAC,SAAS,EAAE,SAAS,EAAC,GAAG,IAAI,CAAC;QACpC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QACpC,IAAI,SAAS,KAAK,IAAI,CAAC,SAAS,IAAI,SAAS,KAAK,IAAI,CAAC,SAAS,EAAE;YAChE,IAAI,SAAS,KAAK,IAAI,CAAC,SAAS,EAAE;gBAChC,IAAI,CAAC,gBAAgB,EAAE,CAAC;aACzB;iBAAM;gBACL,IAAI,CAAC,qBAAqB,EAAE,CAAC;aAC9B;SACF;IACH,CAAC;IAED;;OAEG;IACH,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;IACD,IAAI,OAAO,CAAC,EAAE;QACZ,MAAM,GAAG,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC;QACvB,IAAI,GAAG,KAAK,IAAI,CAAC,QAAQ,EAAE;YACzB,IAAI,CAAC,QAAQ,GAAG,GAAG,CAAC;YACpB,IAAI,CAAC,qBAAqB,EAAE,CAAC;SAC9B;IACH,CAAC;IAED;;OAEG;IACH,IAAI,YAAY;QACd,OAAO,IAAI,CAAC,aAAa,CAAC;IAC5B,CAAC;IACD,IAAI,YAAY,CAAC,IAAI;QACnB,MAAM,EAAC,SAAS,EAAE,SAAS,EAAC,GAAG,IAAI,CAAC;QACpC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;QACxC,IAAI,SAAS,KAAK,IAAI,CAAC,SAAS,EAAE;YAChC,IAAI,CAAC,gBAAgB,EAAE,CAAC;SACzB;aAAM,IAAI,SAAS,KAAK,IAAI,CAAC,SAAS,EAAE;YACvC,IAAI,CAAC,gBAAgB,EAAE,CAAC;SACzB;IACH,CAAC;IAED;;OAEG;IACH,IAAI,cAAc;QAChB,OAAO,IAAI,CAAC,aAAa,CAAC;IAC5B,CAAC;IACD,IAAI,cAAc,CAAC,MAAM;QACvB,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;QAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC;QACpC,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAC7D,IAAI,MAAM,KAAK,IAAI,CAAC,eAAe,EAAE;YACnC,IAAI,CAAC,sBAAsB,CAAC,MAAM,EAAE,IAAI,CAAC,eAAe,CAAC,CAAC;YAC1D,IAAI,CAAC,qBAAqB,CAAC,EAAC,IAAI,EAAE,IAAI,EAAC,CAAC,CAAC;SAC1C;QACD,IAAI,CAAC,gBAAgB,EAAE,CAAC;IAC1B,CAAC;IAED;;OAEG;IACH,cAAc,CAAC,KAAc;QAC3B,IAAI,KAAK,IAAI,IAAI,CAAC,cAAc,EAAE;YAChC,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC;YAC5B,IAAI,CAAC,OAAO,EAAE,CAAC;SAChB;IACH,CAAC;IAED;;;OAGG;IACH,aAAa,CAAC,KAAa,EAAE,QAAQ,GAAG,OAAO;QAC7C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;YACzB,OAAO;QACT,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;QACtD,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC;QAC5B,IAAI,QAAQ,KAAK,SAAS,EAAE;YAC1B,QAAQ,GAAG,KAAK,GAAG,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC;SAClE;QACD,QAAQ,QAAQ,EAAE;YAChB,KAAK,OAAO;gBACV,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC;gBACzB,MAAM;YACR,KAAK,QAAQ;gBACX,IAAI,CAAC,eAAe,GAAG,GAAG,CAAC;gBAC3B,MAAM;YACR,KAAK,KAAK;gBACR,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC;gBACzB,MAAM;YACR;gBACE,MAAM,IAAI,SAAS,CACf,sDAAsD,CAAC,CAAC;SAC/D;QACD,IAAI,CAAC,eAAe,EAAE,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,GAAU;QAC5B,MAAM,IAAI,CAAC,mBAAmB,CAAC;QAC/B,IAAI,CAAC,YAAa,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;IACxC,CAAC;IAED,KAAK,CAAC,gBAAgB,CAAC,IAAY,EAAE,QAAoD,EAAE,OAAuD;QAChJ,MAAM,IAAI,CAAC,mBAAmB,CAAC;QAC/B,IAAI,CAAC,YAAa,CAAC,gBAAgB,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC/D,CAAC;IAED,KAAK,CAAC,mBAAmB,CAAC,IAAY,EAAE,QAAoD,EAAE,OAAoD;QAChJ,MAAM,IAAI,CAAC,mBAAmB,CAAC;QAC/B,IAAI,CAAC,YAAa,CAAC,mBAAmB,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;IAClE,CAAC;IAaS,gBAAgB;QACxB,WAAW;IACb,CAAC;IAES,gBAAgB;QACxB,WAAW;IACb,CAAC;IAES,aAAa;QACrB,WAAW;IACb,CAAC;IAES,YAAY,CAAC,IAAY;QACjC,OAAO;YACL,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,SAAS;YAC/B,CAAC,IAAI,CAAC,iBAAiB,CAAC,EAAE,IAAI,CAAC,SAAS;SACtB,CAAC;IACvB,CAAC;IAED;;OAEG;IACH,IAAc,MAAM;QAClB,OAAO,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,QAAQ,CAAC;IACxC,CAAC;IAED;;OAEG;IACH,IAAc,SAAS;QACrB,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACvC,CAAC;IAED;;OAEG;IACH,IAAc,SAAS;QACrB,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;IAChD,CAAC;IAED;;OAEG;IACH,IAAc,SAAS;QACrB,OAAO,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC3C,CAAC;IAED;;OAEG;IACH,IAAc,SAAS;QACrB,OAAO,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;IACpD,CAAC;IAES,eAAe;QACvB,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;IAC7B,CAAC;IAES,qBAAqB;QAC7B,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC;QACjC,IAAI,CAAC,eAAe,EAAE,CAAC;IACzB,CAAC;IAES,OAAO;QACf,MAAM,EAAC,MAAM,EAAE,KAAK,EAAE,WAAW,EAAC,GAAG,IAAI,CAAC;QAE1C,IAAI,IAAI,CAAC,oBAAoB,EAAE;YAC7B,IAAI,CAAC,aAAa,EAAE,CAAC;YACrB,IAAI,CAAC,oBAAoB,GAAG,KAAK,CAAC;SACnC;QACD,IAAI,CAAC,iBAAiB,EAAE,CAAC;QACzB,IAAI,CAAC,eAAe,EAAE,CAAC;QACvB,IAAI,CAAC,eAAe,EAAE,CAAC;QACvB,IAAI,CAAC,qBAAqB,EAAE,CAAC;QAE7B,IAAI,IAAI,CAAC,WAAW,KAAK,WAAW,EAAE;YACpC,IAAI,CAAC,eAAe,EAAE,CAAC;SACxB;QAED,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,KAAK,CAAC,CAAC,EAAE;YAC3C,wDAAwD;YACxD,IAAI,CAAC,UAAU,EAAE,CAAC;SACnB;aAAM,IACH,IAAI,CAAC,MAAM,KAAK,MAAM,IAAI,IAAI,CAAC,KAAK,KAAK,KAAK;YAC9C,IAAI,CAAC,eAAe,EAAE;YACxB,wDAAwD;YACxD,IAAI,CAAC,UAAU,EAAE,CAAC;YAClB,IAAI,CAAC,mBAAmB,EAAE,CAAC;SAC5B;QACD,IAAI,CAAC,gBAAgB,EAAE,CAAC;IAC1B,CAAC;IAED;;OAEG;IACO,iBAAiB;QACzB,0EAA0E;QAC1E,YAAY;QACZ,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;IACjE,CAAC;IAES,eAAe;QACvB,IAAI,IAAI,CAAC,cAAc,KAAK,CAAC,CAAC,EAAE;YAC9B,OAAO;SACR;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,cAAc,CAAC;QAClC,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC;QACpC,MAAM,GAAG,GAAG,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAC5D,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAErD,MAAM,YAAY,GAAG,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC;QACpE,MAAM,YAAY,GAAG,GAAG,GAAG,IAAI,GAAG,MAAM,CAAC;QACzC,6DAA6D;QAC7D,MAAM,cAAc,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CACtC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,SAAS,EACjC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,eAAe,GAAG,YAAY,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;QACtE,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,eAAe,GAAG,cAAc,CAAC;QAC3D,IAAI,CAAC,eAAe,GAAG,cAAc,CAAC;IACxC,CAAC;IAES,UAAU,CAAC,UAAmB,SAAS;QAC/C,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CACxB;YACE,KAAK,EAAE,IAAI,CAAC,MAAM;YAClB,IAAI,EAAE,IAAI,CAAC,KAAK;YAChB,GAAG,EAAE,IAAI,CAAC,IAAI;YACd,MAAM,EAAE,IAAI;YACZ,YAAY,EAAE,IAAI,CAAC,aAAa;YAChC,WAAW,EAAE,IAAI,CAAC,YAAY;SAC/B,EACD,OAAO,CAAC,CAAC;QACb,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,aAAa,EAAE,EAAC,MAAM,EAAC,CAAC,CAAC,CAAC;IAC/D,CAAC;IAES,eAAe;QACvB,MAAM,MAAM,GAAG;YACb,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,WAAW;SAClC,CAAC;QACF,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,kBAAkB,EAAE,EAAC,MAAM,EAAC,CAAC,CAAC,CAAC;IACpE,CAAC;IAES,gBAAgB;QACxB,IAAI,IAAI,CAAC,YAAY,EAAE;YACrB,MAAM,MAAM,GAAG;gBACb,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,IAAI,CAAC,YAAY;gBACtC,CAAC,IAAI,CAAC,qBAAqB,CAAC,EAAE,CAAC;aAChC,CAAC;YACF,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,mBAAmB,EAAE,EAAC,MAAM,EAAC,CAAC,CAAC,CAAC;YACnE,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC;SACvB;IACH,CAAC;IAED;;;OAGG;IACO,mBAAmB;QAC3B,MAAM,MAAM,GAA+B,EAAE,CAAC;QAC9C,KAAK,IAAI,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,IAAI,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;YACpD,MAAM,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;SAC1C;QACD,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,oBAAoB,EAAE,EAAC,MAAM,EAAC,CAAC,CAAC,CAAC;IACtE,CAAC;IAED;;OAEG;IACH,IAAY,IAAI;QACd,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,KAAK,CAAC,CAAC,EAAE;YAC3C,OAAO,CAAC,CAAC;SACV;QACD,OAAO,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;IACtC,CAAC;IAEO,gBAAgB;QACtB,IAAI,IAAI,CAAC,SAAS,KAAK,CAAC,IAAI,IAAI,CAAC,IAAI,GAAG,CAAC,EAAE;YACzC,IAAI,CAAC,eAAe,EAAE,CAAC;SACxB;aAAM;YACL,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC;YAC/D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAChB,IAAI,CAAC,WAAW,EAChB,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC;YAC5D,IAAI,IAAI,CAAC,YAAY,GAAG,GAAG,IAAI,IAAI,CAAC,YAAY,GAAG,GAAG,EAAE;gBACtD,IAAI,CAAC,eAAe,EAAE,CAAC;aACxB;SACF;IACH,CAAC;IAED;;;OAGG;IACQ,qBAAqB,CAAC,OAAqC;QACpE,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,KAAK,CAAC,CAAC;YAAE,OAAO;QAEpD,IAAI,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC;QAC/B,OACE,IAAI,CAAC,KAAK,CACR,IAAI,CAAC,gBAAgB,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC;YACtD,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAC/C;;gBAED,IAAI,CAAC,KAAK,CAAE,IAAI,CAAC,eAAe,CAAC,EAChC;YACD,YAAY,EAAE,CAAC;SAChB;QAED,IAAI,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC;QAC7B,OACE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;;gBAEjE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,SAAS,CAAC,EACjD;YACA,WAAW,EAAE,CAAC;SACf;QAED,IAAI,YAAY,KAAK,IAAI,CAAC,aAAa,IAAI,WAAW,KAAK,IAAI,CAAC,YAAY,EAAE;YAC5E,IAAI,CAAC,aAAa,GAAG,YAAY,CAAC;YAClC,IAAI,CAAC,YAAY,GAAG,WAAW,CAAC;YAChC,IAAI,OAAO,IAAI,OAAO,CAAC,IAAI,EAAE;gBAC3B,IAAI,CAAC,UAAU,EAAE,CAAC;aACnB;SACF;IACH,CAAC;IAEO,sBAAsB,CAAC,MAAc,EAAE,MAAc;QAC3D,qEAAqE;QACrE,mDAAmD;QACnD,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,SAAS,CAAC;QACjD,IAAI,MAAM,GAAG,MAAM,IAAI,MAAM,GAAG,MAAM,EAAE;YACtC,IAAI,CAAC,cAAc,GAAG,CAAC,CAAC,CAAC;SAC1B;IACH,CAAC;CACF","sourcesContent":["import EventTarget from '../polyfillLoaders/EventTarget.js';\nimport {Layout, Positions, ScrollDirection, Size, dimension, position} from './Layout.js';\n\ntype UpdateVisibleIndicesOptions = {\n emit?: boolean\n}\n\nexport interface Layout1dBaseConfig {\n direction?: ScrollDirection,\n totalItems?: number\n}\n\nexport abstract class Layout1dBase implements Layout {\n /**\n * The last set viewport scroll position.\n */\n private _latestCoords: Positions = {left: 0, top: 0};\n\n /**\n * Scrolling direction.\n */\n private _direction: ScrollDirection = 'vertical';\n\n /**\n * Dimensions of the viewport.\n */\n private _viewportSize: Size = {width: 0, height: 0};\n\n /**\n * Flag for debouncing asynchnronous reflow requests.\n */\n private _pendingReflow = false;\n\n private _pendingLayoutUpdate = false;\n\n /**\n * Index of the item that has been scrolled to via the public API. When the\n * container is otherwise scrolled, this value is set back to -1.\n */\n protected _scrollToIndex = -1;\n\n /**\n * When a child is scrolled to, the offset from the top of the child and the\n * top of the viewport. Value is a proportion of the item size.\n */\n private _scrollToAnchor = 0;\n\n /**\n * The index of the first item intersecting the viewport.\n */\n private _firstVisible = 0;\n\n /**\n * The index of the last item intersecting the viewport.\n */\n private _lastVisible = 0;\n\n private _eventTargetPromise: Promise = (EventTarget().then((Ctor) => {\n this._eventTarget = new Ctor();\n }));\n\n /**\n * Pixel offset in the scroll direction of the first child.\n */\n protected _physicalMin = 0;\n\n /**\n * Pixel offset in the scroll direction of the last child.\n */\n protected _physicalMax = 0;\n\n /**\n * Index of the first child.\n */\n protected _first = -1;\n\n /**\n * Index of the last child.\n */\n protected _last = -1;\n\n /**\n * The _estimated_ size of a child.\n */\n protected _itemSize: Size = {width: 100, height: 100};\n\n /**\n * Space in pixels between children.\n */\n protected _spacing = 0;\n\n /**\n * Length in the scrolling direction.\n */\n protected _sizeDim: dimension = 'height';\n\n /**\n * Length in the non-scrolling direction.\n */\n protected _secondarySizeDim: dimension = 'width';\n\n /**\n * Position in the scrolling direction.\n */\n protected _positionDim: position = 'top';\n\n /**\n * Position in the non-scrolling direction.\n */\n protected _secondaryPositionDim: position = 'left';\n\n /**\n * Current scroll offset in pixels.\n */\n protected _scrollPosition = 0;\n\n /**\n * Difference between current scroll offset and scroll offset calculated due\n * to a reflow.\n */\n protected _scrollError = 0;\n\n /**\n * Total number of items that could possibly be displayed. Used to help\n * calculate the scroll size.\n */\n protected _totalItems = 0;\n\n /**\n * The total (estimated) length of all items in the scrolling direction.\n */\n protected _scrollSize = 1;\n\n /**\n * Number of pixels beyond the visible size of the container to still include\n * in the active range of items.\n */\n // TODO (graynorton): Probably want to make this something we calculate based\n // on viewport size, item size, other factors, possibly still with a dial of some kind\n protected _overhang = 1000;\n\n private _eventTarget: EventTarget | null = null;\n protected _spacingChanged = false;\n\n protected _defaultConfig: C = {\n direction: 'vertical'\n } as C\n\n constructor(config?: C) {\n this.config = config || this._defaultConfig;\n }\n\n set config(config: C) {\n Object.assign(this, Object.assign({}, this._defaultConfig, config));\n }\n\n get config(): C {\n return {\n direction: this.direction\n } as C;\n }\n\n /**\n * Maximum index of children + 1, to help estimate total height of the scroll\n * space.\n */\n get totalItems(): number {\n return this._totalItems;\n }\n set totalItems(num) {\n const _num = Number(num);\n if (_num !== this._totalItems) {\n this._totalItems = _num;\n this._scheduleReflow();\n }\n }\n\n /**\n * Primary scrolling direction.\n */\n get direction(): ScrollDirection {\n return this._direction;\n }\n set direction(dir) {\n // Force it to be either horizontal or vertical.\n dir = (dir === 'horizontal') ? dir : 'vertical';\n if (dir !== this._direction) {\n this._direction = dir;\n this._sizeDim = (dir === 'horizontal') ? 'width' : 'height';\n this._secondarySizeDim = (dir === 'horizontal') ? 'height' : 'width';\n this._positionDim = (dir === 'horizontal') ? 'left' : 'top';\n this._secondaryPositionDim = (dir === 'horizontal') ? 'top' : 'left';\n this._scheduleLayoutUpdate();\n }\n }\n\n /**\n * Estimate of the dimensions of a single child.\n */\n get itemSize(): Size {\n return this._itemSize;\n }\n set itemSize(dims) {\n const {_itemDim1, _itemDim2} = this;\n Object.assign(this._itemSize, dims);\n if (_itemDim1 !== this._itemDim1 || _itemDim2 !== this._itemDim2) {\n if (_itemDim2 !== this._itemDim2) {\n this._itemDim2Changed();\n } else {\n this._scheduleLayoutUpdate();\n }\n }\n }\n\n /**\n * Amount of space in between items.\n */\n get spacing(): number {\n return this._spacing;\n }\n set spacing(px) {\n const _px = Number(px);\n if (_px !== this._spacing) {\n this._spacing = _px;\n this._scheduleLayoutUpdate();\n }\n }\n\n /**\n * Height and width of the viewport.\n */\n get viewportSize(): Size {\n return this._viewportSize;\n }\n set viewportSize(dims) {\n const {_viewDim1, _viewDim2} = this;\n Object.assign(this._viewportSize, dims);\n if (_viewDim2 !== this._viewDim2) {\n this._viewDim2Changed();\n } else if (_viewDim1 !== this._viewDim1) {\n this._checkThresholds();\n }\n }\n\n /**\n * Scroll offset of the viewport.\n */\n get viewportScroll(): Positions {\n return this._latestCoords;\n }\n set viewportScroll(coords) {\n Object.assign(this._latestCoords, coords);\n const oldPos = this._scrollPosition;\n this._scrollPosition = this._latestCoords[this._positionDim];\n if (oldPos !== this._scrollPosition) {\n this._scrollPositionChanged(oldPos, this._scrollPosition);\n this._updateVisibleIndices({emit: true});\n }\n this._checkThresholds();\n }\n\n /**\n * Perform a reflow if one has been scheduled.\n */\n reflowIfNeeded(force: boolean) {\n if (force || this._pendingReflow) {\n this._pendingReflow = false;\n this._reflow();\n }\n }\n\n /**\n * Scroll to the child at the given index, and the given position within that\n * child.\n */\n scrollToIndex(index: number, position = 'start') {\n if (!Number.isFinite(index))\n return;\n index = Math.min(this.totalItems, Math.max(0, index));\n this._scrollToIndex = index;\n if (position === 'nearest') {\n position = index > this._first + this._num / 2 ? 'end' : 'start';\n }\n switch (position) {\n case 'start':\n this._scrollToAnchor = 0;\n break;\n case 'center':\n this._scrollToAnchor = 0.5;\n break;\n case 'end':\n this._scrollToAnchor = 1;\n break;\n default:\n throw new TypeError(\n 'position must be one of: start, center, end, nearest');\n }\n this._scheduleReflow();\n }\n\n async dispatchEvent(evt: Event) {\n await this._eventTargetPromise;\n this._eventTarget!.dispatchEvent(evt);\n }\n\n async addEventListener(type: string, listener: EventListener | EventListenerObject | null, options?: boolean | AddEventListenerOptions | undefined) {\n await this._eventTargetPromise;\n this._eventTarget!.addEventListener(type, listener, options);\n }\n\n async removeEventListener(type: string, callback: EventListener | EventListenerObject | null, options?: boolean | EventListenerOptions | undefined) {\n await this._eventTargetPromise;\n this._eventTarget!.removeEventListener(type, callback, options);\n }\n\n /**\n * Get the top and left positioning of the item at idx.\n */\n abstract _getItemPosition(idx: number): Positions;\n\n /**\n * Update _first and _last based on items that should be in the current\n * range.\n */\n abstract _getActiveItems(): void;\n\n protected _itemDim2Changed() {\n // Override\n }\n\n protected _viewDim2Changed() {\n // Override\n }\n\n protected _updateLayout() {\n // Override\n }\n\n protected _getItemSize(_idx: number): Size {\n return {\n [this._sizeDim]: this._itemDim1,\n [this._secondarySizeDim]: this._itemDim2,\n } as unknown as Size;\n }\n\n /**\n * The size of an item in the scrolling direction + space between items.\n */\n protected get _delta(): number {\n return this._itemDim1 + this._spacing;\n }\n\n /**\n * The height or width of an item, whichever corresponds to the scrolling direction.\n */\n protected get _itemDim1(): number {\n return this._itemSize[this._sizeDim];\n }\n\n /**\n * The height or width of an item, whichever does NOT correspond to the scrolling direction.\n */\n protected get _itemDim2(): number {\n return this._itemSize[this._secondarySizeDim];\n }\n\n /**\n * The height or width of the viewport, whichever corresponds to the scrolling direction.\n */\n protected get _viewDim1(): number {\n return this._viewportSize[this._sizeDim];\n }\n\n /**\n * The height or width of the viewport, whichever does NOT correspond to the scrolling direction.\n */\n protected get _viewDim2(): number {\n return this._viewportSize[this._secondarySizeDim];\n }\n\n protected _scheduleReflow() {\n this._pendingReflow = true;\n }\n\n protected _scheduleLayoutUpdate() {\n this._pendingLayoutUpdate = true;\n this._scheduleReflow();\n }\n\n protected _reflow() {\n const {_first, _last, _scrollSize} = this;\n\n if (this._pendingLayoutUpdate) {\n this._updateLayout();\n this._pendingLayoutUpdate = false;\n }\n this._updateScrollSize();\n this._getActiveItems();\n this._scrollIfNeeded();\n this._updateVisibleIndices();\n\n if (this._scrollSize !== _scrollSize) {\n this._emitScrollSize();\n }\n\n if (this._first === -1 && this._last === -1) {\n // TODO: have default empty object for emitRange instead\n this._emitRange();\n } else if (\n this._first !== _first || this._last !== _last ||\n this._spacingChanged) {\n // TODO: have default empty object for emitRange instead\n this._emitRange();\n this._emitChildPositions();\n }\n this._emitScrollError();\n }\n\n /**\n * Estimates the total length of all items in the scrolling direction, including spacing.\n */\n protected _updateScrollSize() {\n // Ensure we have at least 1px - this allows getting at least 1 item to be\n // rendered.\n this._scrollSize = Math.max(1, this._totalItems * this._delta);\n }\n\n protected _scrollIfNeeded() {\n if (this._scrollToIndex === -1) {\n return;\n }\n const index = this._scrollToIndex;\n const anchor = this._scrollToAnchor;\n const pos = this._getItemPosition(index)[this._positionDim];\n const size = this._getItemSize(index)[this._sizeDim];\n\n const curAnchorPos = this._scrollPosition + this._viewDim1 * anchor;\n const newAnchorPos = pos + size * anchor;\n // Ensure scroll position is an integer within scroll bounds.\n const scrollPosition = Math.floor(Math.min(\n this._scrollSize - this._viewDim1,\n Math.max(0, this._scrollPosition - curAnchorPos + newAnchorPos)));\n this._scrollError += this._scrollPosition - scrollPosition;\n this._scrollPosition = scrollPosition;\n }\n\n protected _emitRange(inProps: unknown = undefined) {\n const detail = Object.assign(\n {\n first: this._first,\n last: this._last,\n num: this._num,\n stable: true,\n firstVisible: this._firstVisible,\n lastVisible: this._lastVisible,\n },\n inProps);\n this.dispatchEvent(new CustomEvent('rangechange', {detail}));\n }\n\n protected _emitScrollSize() {\n const detail = {\n [this._sizeDim]: this._scrollSize,\n };\n this.dispatchEvent(new CustomEvent('scrollsizechange', {detail}));\n }\n\n protected _emitScrollError() {\n if (this._scrollError) {\n const detail = {\n [this._positionDim]: this._scrollError,\n [this._secondaryPositionDim]: 0,\n };\n this.dispatchEvent(new CustomEvent('scrollerrorchange', {detail}));\n this._scrollError = 0;\n }\n }\n\n /**\n * Get or estimate the top and left positions of items in the current range.\n * Emit an itempositionchange event with these positions.\n */\n protected _emitChildPositions() {\n const detail: {[key: number]: Positions} = {};\n for (let idx = this._first; idx <= this._last; idx++) {\n detail[idx] = this._getItemPosition(idx);\n }\n this.dispatchEvent(new CustomEvent('itempositionchange', {detail}));\n }\n\n /**\n * Number of items to display.\n */\n private get _num(): number {\n if (this._first === -1 || this._last === -1) {\n return 0;\n }\n return this._last - this._first + 1;\n }\n\n private _checkThresholds() {\n if (this._viewDim1 === 0 && this._num > 0) {\n this._scheduleReflow();\n } else {\n const min = Math.max(0, this._scrollPosition - this._overhang);\n const max = Math.min(\n this._scrollSize,\n this._scrollPosition + this._viewDim1 + this._overhang);\n if (this._physicalMin > min || this._physicalMax < max) {\n this._scheduleReflow();\n }\n }\n }\n\n /**\n * Find the indices of the first and last items to intersect the viewport.\n * Emit a visibleindiceschange event when either index changes.\n */\n protected _updateVisibleIndices(options?: UpdateVisibleIndicesOptions) {\n if (this._first === -1 || this._last === -1) return;\n\n let firstVisible = this._first;\n while (\n Math.round(\n this._getItemPosition(firstVisible)[this._positionDim] +\n this._getItemSize(firstVisible)[this._sizeDim]\n )\n <=\n Math.round (this._scrollPosition)\n ) {\n firstVisible++;\n }\n\n let lastVisible = this._last;\n while (\n Math.round(this._getItemPosition(lastVisible)[this._positionDim])\n >=\n Math.round(this._scrollPosition + this._viewDim1)\n ) {\n lastVisible--;\n }\n\n if (firstVisible !== this._firstVisible || lastVisible !== this._lastVisible) {\n this._firstVisible = firstVisible;\n this._lastVisible = lastVisible;\n if (options && options.emit) {\n this._emitRange();\n }\n }\n }\n\n private _scrollPositionChanged(oldPos: number, newPos: number) {\n // When both values are bigger than the max scroll position, keep the\n // current _scrollToIndex, otherwise invalidate it.\n const maxPos = this._scrollSize - this._viewDim1;\n if (oldPos < maxPos || newPos < maxPos) {\n this._scrollToIndex = -1;\n }\n }\n}\n"]} -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/layouts/Layout1dFlex.d.ts b/lib/uni-virtualizer/lib/layouts/Layout1dFlex.d.ts -index b4f6e54f539aef2a2ecd0399237e3d59a57a8a1f..e6aadd4b26ce97a862ca1051c6ecb08467347162 100644 ---- a/lib/uni-virtualizer/lib/layouts/Layout1dFlex.d.ts -+++ b/lib/uni-virtualizer/lib/layouts/Layout1dFlex.d.ts -@@ -1,11 +1,14 @@ --import { Layout1dBase } from './Layout1dBase'; --import { ItemBox, Positions, Size, LayoutConfig, Type } from './Layout'; --export interface Layout1dFlexConfig extends LayoutConfig { -- type?: Type; -- direction?: "horizontal" | "vertical"; -+import { Layout1dBase, Layout1dBaseConfig } from './Layout1dBase'; -+import { ItemBox, Positions, Size } from './Layout'; -+interface Layout1dFlexConfig extends Layout1dBaseConfig { - spacing?: number; - idealSize?: number; - } -+declare type Layout1dFlexSpecifier = Layout1dFlexConfig & { -+ type: new (config?: Layout1dFlexConfig) => Layout1dFlex; -+}; -+declare type Layout1dFlexSpecifierFactory = (config?: Layout1dFlexConfig) => Layout1dFlexSpecifier; -+export declare const layout1dFlex: Layout1dFlexSpecifierFactory; - interface Rolumn { - _startIdx: number; - _endIdx: number; -@@ -21,35 +24,33 @@ interface Chunk { - /** - * TODO @straversi: document and test this Layout. - */ --export declare class Layout1dFlex extends Layout1dBase { -+export declare class Layout1dFlex extends Layout1dBase { - private _itemSizes; - private _chunkSize; - private _chunks; - private _aspectRatios; - private _numberOfAspectRatiosMeasured; -- protected _idealSize: number; -+ protected _idealSize: number | null; - protected _config: Layout1dFlexConfig; -- protected static _defaultConfig: Layout1dFlexConfig; -+ protected _defaultConfig: Layout1dFlexConfig; - listenForChildLoadEvents: boolean; -- measureChildren: ((e: Element, i: object) => object); -- set idealSize(px: number); -- get idealSize(): number; -+ /** -+ * TODO graynorton@ Don't hard-code Flickr - probably need a config option -+ */ -+ measureChildren: ((e: Element, i: unknown) => (ItemBox)); -+ set idealSize(px: number | null); -+ get idealSize(): number | null; - updateItemSizes(sizes: { - [key: number]: ItemBox; - }): void; - _newChunk(): { -- _rolumns: any[]; -- _itemPositions: any[]; -+ _rolumns: never[]; -+ _itemPositions: never[]; - _size: number; - _dirty: boolean; - }; -- _getChunk(idx: number | string): { -- _rolumns: any[]; -- _itemPositions: any[]; -- _size: number; -- _dirty: boolean; -- }; -- _recordAspectRatio(dims: any): void; -+ _getChunk(idx: number | string): Chunk; -+ _recordAspectRatio(dims: ItemBox): void; - _getRandomAspectRatio(): Size; - _viewDim2Changed(): void; - _getActiveItems(): void; -diff --git a/lib/uni-virtualizer/lib/layouts/Layout1dFlex.d.ts.map b/lib/uni-virtualizer/lib/layouts/Layout1dFlex.d.ts.map -index a899016165645dce6a8326b41733b024ed5d4e3d..de4bacef32254ea5dd92cfff32da4d1c83d31950 100644 ---- a/lib/uni-virtualizer/lib/layouts/Layout1dFlex.d.ts.map -+++ b/lib/uni-virtualizer/lib/layouts/Layout1dFlex.d.ts.map -@@ -1 +1 @@ --{"version":3,"file":"Layout1dFlex.d.ts","sourceRoot":"","sources":["../../../../src/lib/uni-virtualizer/lib/layouts/Layout1dFlex.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAC,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAC,MAAM,UAAU,CAAC;AAEtE,MAAM,WAAW,kBAAmB,SAAQ,YAAY;IACtD,IAAI,CAAC,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;IAC1B,SAAS,CAAC,EAAE,YAAY,GAAG,UAAU,CAAC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,UAAU,MAAM;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAA;CACd;AAED,UAAU,KAAK;IACb,cAAc,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IACjC,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAA;CAChB;AAED;;GAEG;AACH,qBAAa,YAAa,SAAQ,YAAY;IAC5C,OAAO,CAAC,UAAU,CAAmB;IAIrC,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,OAAO,CAAoB;IACnC,OAAO,CAAC,aAAa,CAAc;IACnC,OAAO,CAAC,6BAA6B,CAAa;IAClD,SAAS,CAAC,UAAU,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,OAAO,EAAE,kBAAkB,CAAM;IAC3C,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,kBAAkB,CAIlD;IAED,wBAAwB,UAAQ;IAEhC,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC,CAKnD;IAED,IAAI,SAAS,CAAC,EAAE,QAAA,EAMf;IAED,IAAI,SAAS,WAEZ;IAED,eAAe,CAAC,KAAK,EAAE;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAC;IAoB/C,SAAS;;;;;;IAST,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;;;;;;IAI9B,kBAAkB,CAAC,IAAI,KAAA;IAavB,qBAAqB,IAAI,IAAI;IAa7B,gBAAgB;IAIhB,eAAe;IA2Bf,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS;IAKxC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAM/B,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAStC,YAAY,CAAC,QAAQ,EAAE,MAAM;IA+D7B,aAAa,IAAI,IAAI;IAYrB,iBAAiB;CAOlB"} -\ No newline at end of file -+{"version":3,"file":"Layout1dFlex.d.ts","sourceRoot":"","sources":["../../../../src/lib/uni-virtualizer/lib/layouts/Layout1dFlex.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAE,kBAAkB,EAAC,MAAM,gBAAgB,CAAC;AAChE,OAAO,EAAC,OAAO,EAAE,SAAS,EAAE,IAAI,EAAC,MAAM,UAAU,CAAC;AAElD,UAAU,kBAAmB,SAAQ,kBAAkB;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,aAAK,qBAAqB,GAAG,kBAAkB,GAAG;IAChD,IAAI,EAAE,KAAI,MAAM,CAAC,EAAE,kBAAkB,KAAK,YAAY,CAAA;CACvD,CAAA;AAED,aAAK,4BAA4B,GAAG,CAAC,MAAM,CAAC,EAAE,kBAAkB,KAAK,qBAAqB,CAAC;AAE3F,eAAO,MAAM,YAAY,EAAE,4BAEjB,CAAC;AAEX,UAAU,MAAM;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAA;CACd;AAED,UAAU,KAAK;IACb,cAAc,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IACjC,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAA;CAChB;AAeD;;GAEG;AACH,qBAAa,YAAa,SAAQ,YAAY,CAAC,kBAAkB,CAAC;IAChE,OAAO,CAAC,UAAU,CAAmB;IAIrC,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,OAAO,CAAoB;IACnC,OAAO,CAAC,aAAa,CAAoB;IACzC,OAAO,CAAC,6BAA6B,CAAa;IAClD,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAQ;IAC3C,SAAS,CAAC,OAAO,EAAE,kBAAkB,CAAM;IAC3C,SAAS,CAAC,cAAc,EAAE,kBAAkB,CAGzC;IAEH,wBAAwB,UAAQ;IAElC;;OAEG;IACD,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,CAUvD;IAED,IAAI,SAAS,CAAC,EAAE,eAAA,EAMf;IAED,IAAI,SAAS,kBAEZ;IAED,eAAe,CAAC,KAAK,EAAE;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAC;IAqB/C,SAAS;;;;;;IAST,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;IAI9B,kBAAkB,CAAC,IAAI,EAAE,OAAO;IAahC,qBAAqB,IAAI,IAAI;IAa7B,gBAAgB;IAIhB,eAAe;IA2Bf,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS;IAKxC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAM/B,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAStC,YAAY,CAAC,QAAQ,EAAE,MAAM;IA+D7B,aAAa,IAAI,IAAI;IAYrB,iBAAiB;CAOlB"} -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/layouts/Layout1dFlex.js b/lib/uni-virtualizer/lib/layouts/Layout1dFlex.js -index 567ee9bc28f6b65eff52d1d853d150f6ca99fb12..00b54b50b3e4977d706846e4f139a6e91f15c615 100644 ---- a/lib/uni-virtualizer/lib/layouts/Layout1dFlex.js -+++ b/lib/uni-virtualizer/lib/layouts/Layout1dFlex.js -@@ -1,4 +1,7 @@ - import { Layout1dBase } from './Layout1dBase'; -+export const layout1dFlex = (config) => Object.assign({ -+ type: Layout1dFlex -+}, config); - /** - * TODO @straversi: document and test this Layout. - */ -@@ -6,16 +9,33 @@ export class Layout1dFlex extends Layout1dBase { - constructor() { - super(...arguments); - this._itemSizes = []; -+ // private _itemPositions: Array = []; -+ // private _rolumnStartIdx: Array = []; -+ // private _rolumnStartPos: Array = []; -+ this._chunkSize = null; - this._chunks = []; - this._aspectRatios = {}; - this._numberOfAspectRatiosMeasured = 0; -+ this._idealSize = null; - this._config = {}; -+ this._defaultConfig = Object.assign({}, super._defaultConfig, { -+ spacing: 0, -+ idealSize: 200 -+ }); - this.listenForChildLoadEvents = true; -+ /** -+ * TODO graynorton@ Don't hard-code Flickr - probably need a config option -+ */ - this.measureChildren = function (e, i) { -- return { -- width: i['o_width'] || e.naturalWidth || undefined, -- height: i['o_height'] || e.naturalHeight || undefined -- }; -+ const { naturalWidth, naturalHeight } = e; -+ if (naturalWidth !== undefined && naturalHeight != undefined) { -+ return { width: naturalWidth, height: naturalHeight }; -+ } -+ const { o_width, o_height } = i; -+ if (o_width !== undefined && o_height !== undefined) { -+ return { width: o_width, height: o_height }; -+ } -+ return { width: -1, height: -1 }; - }; - } - set idealSize(px) { -@@ -31,15 +51,16 @@ export class Layout1dFlex extends Layout1dBase { - updateItemSizes(sizes) { - let dirty; - Object.keys(sizes).forEach((key) => { -- const chunk = this._getChunk(key); -- const dims = sizes[key]; -- const prevDims = this._itemSizes[key]; -+ const n = Number(key); -+ const chunk = this._getChunk(n); -+ const dims = sizes[n]; -+ const prevDims = this._itemSizes[n]; - if (dims.width && dims.height) { - if (!prevDims || prevDims.width !== dims.width || prevDims.height !== dims.height) { - chunk._dirty = true; - dirty = true; -- this._itemSizes[Number(key)] = sizes[key]; -- this._recordAspectRatio(sizes[key]); -+ this._itemSizes[n] = sizes[n]; -+ this._recordAspectRatio(sizes[n]); - } - } - }); -@@ -121,7 +142,7 @@ export class Layout1dFlex extends Layout1dBase { - } - _getNaturalItemDims(idx) { - let itemDims = this._itemSizes[idx]; -- if (itemDims === undefined || itemDims.width === undefined || itemDims.height === undefined) { -+ if (itemDims === undefined || itemDims.width === -1 || itemDims.height === -1) { - itemDims = this._getRandomAspectRatio(); - } - return itemDims; -@@ -208,8 +229,4 @@ export class Layout1dFlex extends Layout1dBase { - // (this._spacing * 2); - } - } --Layout1dFlex._defaultConfig = { -- direction: 'vertical', -- spacing: 0, -- idealSize: 200 --}; -+//# sourceMappingURL=Layout1dFlex.js.map -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/layouts/Layout1dFlex.js.map b/lib/uni-virtualizer/lib/layouts/Layout1dFlex.js.map -new file mode 100644 -index 0000000000000000000000000000000000000000..1d93aa6da4cc4a62ce585a3d6261549773c8d855 ---- /dev/null -+++ b/lib/uni-virtualizer/lib/layouts/Layout1dFlex.js.map -@@ -0,0 +1 @@ -+{"version":3,"file":"Layout1dFlex.js","sourceRoot":"","sources":["../../../../src/lib/uni-virtualizer/lib/layouts/Layout1dFlex.ts"],"names":[],"mappings":"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","sourcesContent":["import {Layout1dBase, Layout1dBaseConfig} from './Layout1dBase';\nimport {ItemBox, Positions, Size} from './Layout';\n\ninterface Layout1dFlexConfig extends Layout1dBaseConfig {\n spacing?: number,\n idealSize?: number\n}\n\ntype Layout1dFlexSpecifier = Layout1dFlexConfig & {\n type: new(config?: Layout1dFlexConfig) => Layout1dFlex\n}\n\ntype Layout1dFlexSpecifierFactory = (config?: Layout1dFlexConfig) => Layout1dFlexSpecifier;\n\nexport const layout1dFlex: Layout1dFlexSpecifierFactory = (config?: Layout1dFlexConfig) => Object.assign({\n type: Layout1dFlex\n}, config);\n\ninterface Rolumn {\n _startIdx: number,\n _endIdx: number,\n _startPos: number,\n _size: number\n}\n\ninterface Chunk {\n _itemPositions: Array,\n _rolumns: Array,\n _size: number,\n _dirty: boolean\n}\n\ninterface AspectRatios {\n // conceptually, key is a number, but strictly speaking it's a string\n [key: string]: number\n}\n\n/**\n * TODO graynorton@ Don't hard-code Flickr - probably need a config option\n */\n interface FlickrImageData {\n o_width: number,\n o_height: number\n}\n\n/**\n * TODO @straversi: document and test this Layout.\n */\nexport class Layout1dFlex extends Layout1dBase {\n private _itemSizes: Array = [];\n // private _itemPositions: Array = [];\n // private _rolumnStartIdx: Array = [];\n // private _rolumnStartPos: Array = [];\n private _chunkSize: number | null = null;\n private _chunks: Array = [];\n private _aspectRatios: AspectRatios = {};\n private _numberOfAspectRatiosMeasured: number = 0;\n protected _idealSize: number | null = null;\n protected _config: Layout1dFlexConfig = {};\n protected _defaultConfig: Layout1dFlexConfig = Object.assign({}, super._defaultConfig, {\n spacing: 0,\n idealSize: 200\n });\n\n listenForChildLoadEvents = true;\n\n/**\n * TODO graynorton@ Don't hard-code Flickr - probably need a config option\n */\n measureChildren: ((e: Element, i: unknown) => (ItemBox)) = function (e, i) {\n const { naturalWidth, naturalHeight } = e as HTMLImageElement;\n if (naturalWidth !== undefined && naturalHeight != undefined) {\n return { width: naturalWidth, height: naturalHeight };\n }\n const { o_width, o_height } = i as FlickrImageData;\n if (o_width !== undefined && o_height !== undefined) {\n return { width: o_width, height: o_height };\n }\n return { width: -1, height: -1 };\n }\n\n set idealSize(px) {\n const _px = Number(px);\n if (_px !== this._idealSize) {\n this._idealSize = _px;\n this._scheduleLayoutUpdate();\n }\n }\n\n get idealSize() {\n return this._idealSize;\n }\n\n updateItemSizes(sizes: {[key: number]: ItemBox}) {\n let dirty;\n Object.keys(sizes).forEach((key) => {\n const n = Number(key);\n const chunk = this._getChunk(n);\n const dims = sizes[n];\n const prevDims = this._itemSizes[n];\n if (dims.width && dims.height) {\n if (!prevDims || prevDims.width !== dims.width || prevDims.height !== dims.height) {\n chunk._dirty = true;\n dirty = true;\n this._itemSizes[n] = sizes[n];\n this._recordAspectRatio(sizes[n]);\n }\n }\n });\n if (dirty) {\n this._scheduleLayoutUpdate();\n }\n }\n\n _newChunk() {\n return {\n ['_rolumns']: [],\n _itemPositions: [],\n _size: 0,\n _dirty: false \n }\n }\n\n _getChunk(idx: number | string) {\n return this._chunks[Math.floor(Number(idx) / this._chunkSize!)] || this._newChunk();\n }\n\n _recordAspectRatio(dims: ItemBox) {\n if (dims.width && dims.height) {\n const bucket = Math.round(dims.width / dims.height * 10) / 10;\n if (this._aspectRatios[bucket]) {\n this._aspectRatios[bucket]++;\n }\n else {\n this._aspectRatios[bucket] = 1;\n }\n this._numberOfAspectRatiosMeasured++; \n }\n }\n\n _getRandomAspectRatio(): Size {\n if (this._numberOfAspectRatiosMeasured === 0) {\n return {width: 1, height: 1};\n }\n const n = Math.random() * this._numberOfAspectRatiosMeasured;\n const buckets = Object.keys(this._aspectRatios);\n let i = -1, m = 0;\n while (m < n && i < buckets.length) {\n m += this._aspectRatios[buckets[++i]];\n }\n return {width: Number(buckets[i]), height: 1};\n}\n\n _viewDim2Changed() {\n this._scheduleLayoutUpdate();\n }\n\n _getActiveItems() {\n const chunk = this._getChunk(0);\n if (chunk._rolumns.length === 0) return;\n const scrollPos = Math.max(0, Math.min(this._scrollPosition, this._scrollSize - this._viewDim1));\n const min = Math.max(0, scrollPos - this._overhang);\n const max = Math.min(\n this._scrollSize,\n scrollPos + this._viewDim1 + this._overhang);\n const mid = (min + max) / 2;\n const estMidRolumn = Math.round((mid / this._scrollSize) * chunk._rolumns.length);\n let idx = estMidRolumn;\n while (chunk._rolumns[idx]._startPos < min) {\n idx++;\n }\n while (chunk._rolumns[idx]._startPos > min) {\n idx--;\n }\n this._first = chunk._rolumns[idx]._startIdx;\n this._physicalMin = chunk._rolumns[idx]._startPos;\n let rolumnMax;\n while ((rolumnMax = chunk._rolumns[idx]._startPos + chunk._rolumns[idx]._size + (this._spacing * 2)) < max) {\n idx++;\n }\n this._last = chunk._rolumns[idx]._endIdx;\n this._physicalMax = rolumnMax;\n }\n\n _getItemPosition(idx: number): Positions {\n const chunk = this._getChunk(0);\n return chunk._itemPositions[idx];\n }\n\n _getItemSize(idx: number): Size {\n const chunk = this._getChunk(0);\n const {width, height} = chunk._itemPositions[idx];\n return {width, height} as Size;\n }\n\n _getNaturalItemDims(idx: number): Size {\n let itemDims = this._itemSizes[idx];\n if (itemDims === undefined || itemDims.width === -1 || itemDims.height === -1) {\n itemDims = this._getRandomAspectRatio();\n }\n return itemDims;\n }\n\n\n _layOutChunk(startIdx: number) {\n const chunk: Chunk = this._newChunk();\n let startPos = this._spacing;\n let idx = 0;\n let rolumnSize2 = 0;\n let lastRatio = Infinity;\n const finishRolumn = (lastIdx: number) => {\n const rolumn = {\n _startIdx: startIdx,\n _endIdx: lastIdx,\n _startPos: startPos - this._spacing,\n _size: 0\n }\n chunk._rolumns.push(rolumn);\n let itemStartPos = this._spacing;\n for (let i = startIdx; i <= lastIdx; i++) {\n const pos = chunk._itemPositions[i];\n pos.width = pos.width! * lastRatio;\n pos.height = pos.height! * lastRatio;\n pos.left = this._positionDim === 'left' ? startPos : itemStartPos;\n pos.top = this._positionDim === 'top' ? startPos : itemStartPos;\n itemStartPos += pos[this._secondarySizeDim]! + this._spacing;\n }\n rolumn._size = chunk._itemPositions[lastIdx][this._sizeDim]!;\n }\n while (idx < this._chunkSize!) {\n const itemDims = this._getNaturalItemDims(idx);\n const availableSpace = this._viewDim2 - (this._spacing * (idx - startIdx + 2));\n const itemSize = itemDims[this._sizeDim];\n const itemSize2 = itemDims[this._secondarySizeDim];\n const idealScaleFactor = this._idealSize! / itemSize;\n const adjItemSize = idealScaleFactor * itemSize;\n const adjItemSize2 = idealScaleFactor * itemSize2;\n chunk._itemPositions[idx] = {\n left: 0,\n top: 0,\n width: (this._sizeDim === 'width' ? adjItemSize : adjItemSize2),\n height: (this._sizeDim === 'height' ? adjItemSize : adjItemSize2)\n };\n const ratio = availableSpace / (rolumnSize2 + adjItemSize2);\n if (Math.abs(1 - ratio) > Math.abs(1 - lastRatio)) {\n // rolumn is better without adding this item\n finishRolumn(idx - 1);\n startIdx = idx;\n startPos += (this._idealSize! * lastRatio) + this._spacing;\n lastRatio = (this._viewDim2 - (2 * this._spacing)) / adjItemSize2;\n rolumnSize2 = adjItemSize2;\n }\n else {\n // add this item and continue\n rolumnSize2 += adjItemSize2;\n lastRatio = ratio;\n }\n if (idx === this._chunkSize! - 1) {\n finishRolumn(idx);\n }\n idx++;\n }\n const lastRolumn = chunk._rolumns[chunk._rolumns.length - 1];\n chunk._size = lastRolumn._startPos + lastRolumn._size;\n return chunk; \n }\n\n _updateLayout(): void {\n if (/*this._rolumnStartIdx === undefined ||*/ this._viewDim2 === 0) return;\n this._chunkSize = Math.ceil(2 * (this._viewDim1 * this._viewDim2) / (this._idealSize! * this._idealSize!));\n console.log('chunkSize', this._chunkSize);\n // TODO: An odd place to do this, need to think through the logistics of getting size info to the layout\n // in all cases\n // this._itemSizes.length = 100;//this._totalItems;\n this._chunks[0] = this._layOutChunk(0);\n // TODO (graynorton): This is a hack to force reflow\n this._spacingChanged = true;\n}\n\n _updateScrollSize() {\n const chunk = this._chunks[0];\n this._scrollSize = !chunk || chunk._rolumns.length === 0 ? 1 : chunk._size + (2 * this._spacing);\n // chunk._rolumns[chunk._rolumns.length - 1]._startPos +\n // chunk._itemPositions[chunk._rolumns.length - 1][this._sizeDim] +\n // (this._spacing * 2);\n }\n}"]} -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/layouts/Layout1dGrid.d.ts b/lib/uni-virtualizer/lib/layouts/Layout1dGrid.d.ts -index c8f7a5c894287d49c6855481a653259422131db1..39633e64c311e105edf9bd9c8ed63e8ab73d26f5 100644 ---- a/lib/uni-virtualizer/lib/layouts/Layout1dGrid.d.ts -+++ b/lib/uni-virtualizer/lib/layouts/Layout1dGrid.d.ts -@@ -2,9 +2,8 @@ import { Layout1dBase } from './Layout1dBase.js'; - /** - * TODO @straversi: document and test this Layout. - */ --export declare abstract class Layout1dGrid extends Layout1dBase { -- protected _rolumns: any; -- constructor(config: any); -+export declare abstract class Layout1dGrid extends Layout1dBase { -+ protected _rolumns: number; - _viewDim2Changed(): void; - _itemDim2Changed(): void; - _getActiveItems(): void; -diff --git a/lib/uni-virtualizer/lib/layouts/Layout1dGrid.d.ts.map b/lib/uni-virtualizer/lib/layouts/Layout1dGrid.d.ts.map -index 748cb292f3329dbbcebac625ab7955a6b8683e66..ed14c15459b9a49e0a8b7d28c09fde366ed3d32b 100644 ---- a/lib/uni-virtualizer/lib/layouts/Layout1dGrid.d.ts.map -+++ b/lib/uni-virtualizer/lib/layouts/Layout1dGrid.d.ts.map -@@ -1 +1 @@ --{"version":3,"file":"Layout1dGrid.d.ts","sourceRoot":"","sources":["../../../../src/lib/uni-virtualizer/lib/layouts/Layout1dGrid.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAC,MAAM,mBAAmB,CAAC;AAE/C;;GAEG;AACH,8BAAsB,YAAa,SAAQ,YAAY;IACrD,SAAS,CAAC,QAAQ,MAAC;gBAEP,MAAM,KAAA;IAKlB,gBAAgB;IAIhB,gBAAgB;IAIhB,eAAe;IAef,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAC;IAQ1D,iBAAiB;CAIlB"} -\ No newline at end of file -+{"version":3,"file":"Layout1dGrid.d.ts","sourceRoot":"","sources":["../../../../src/lib/uni-virtualizer/lib/layouts/Layout1dGrid.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAC,MAAM,mBAAmB,CAAC;AAE/C;;GAEG;AACH,8BAAsB,YAAY,CAAC,MAAM,CAAE,SAAQ,YAAY,CAAC,MAAM,CAAC;IACrE,SAAS,CAAC,QAAQ,EAAE,MAAM,CAAK;IAO/B,gBAAgB;IAIhB,gBAAgB;IAIhB,eAAe;IAef,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAC;IAQ1D,iBAAiB;CAIlB"} -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/layouts/Layout1dGrid.js b/lib/uni-virtualizer/lib/layouts/Layout1dGrid.js -index 24db275ef90173740b73488f8816fbf3344dff78..f9fdf72e948f74f384cb655887b12862afde5547 100644 ---- a/lib/uni-virtualizer/lib/layouts/Layout1dGrid.js -+++ b/lib/uni-virtualizer/lib/layouts/Layout1dGrid.js -@@ -3,10 +3,14 @@ import { Layout1dBase } from './Layout1dBase.js'; - * TODO @straversi: document and test this Layout. - */ - export class Layout1dGrid extends Layout1dBase { -- constructor(config) { -- super(config); -+ constructor() { -+ super(...arguments); - this._rolumns = 1; - } -+ // constructor(config) { -+ // super(config); -+ // this._rolumns = 1; -+ // } - _viewDim2Changed() { - this._scheduleLayoutUpdate(); - } -@@ -36,3 +40,4 @@ export class Layout1dGrid extends Layout1dBase { - Math.max(1, Math.ceil(this._totalItems / this._rolumns) * this._delta + this._spacing); - } - } -+//# sourceMappingURL=Layout1dGrid.js.map -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/layouts/Layout1dGrid.js.map b/lib/uni-virtualizer/lib/layouts/Layout1dGrid.js.map -new file mode 100644 -index 0000000000000000000000000000000000000000..d5b5318d4d7ba2efa45ae788e5ef20563707af2d ---- /dev/null -+++ b/lib/uni-virtualizer/lib/layouts/Layout1dGrid.js.map -@@ -0,0 +1 @@ -+{"version":3,"file":"Layout1dGrid.js","sourceRoot":"","sources":["../../../../src/lib/uni-virtualizer/lib/layouts/Layout1dGrid.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAC,MAAM,mBAAmB,CAAC;AAE/C;;GAEG;AACH,MAAM,OAAgB,YAAqB,SAAQ,YAAoB;IAAvE;;QACY,aAAQ,GAAW,CAAC,CAAC;IA0CjC,CAAC;IAxCC,wBAAwB;IACxB,mBAAmB;IACnB,uBAAuB;IACvB,IAAI;IAEJ,gBAAgB;QACd,IAAI,CAAC,qBAAqB,EAAE,CAAC;IAC/B,CAAC;IAED,gBAAgB;QACd,IAAI,CAAC,qBAAqB,EAAE,CAAC;IAC/B,CAAC;IAED,eAAe;QACb,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC;QAC/D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAChB,IAAI,CAAC,WAAW,EAChB,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC;QAC5D,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;QAC/C,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAEjD,IAAI,CAAC,MAAM,GAAG,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;QACvC,IAAI,CAAC,KAAK;YACN,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC;QACxE,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC;QAC3C,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC;IAClD,CAAC;IAED,gBAAgB,CAAC,GAAW;QAC1B,OAAO;YACL,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC,MAAM;YAC9E,CAAC,IAAI,CAAC,qBAAqB,CAAC,EAAE,IAAI,CAAC,QAAQ;gBAC3C,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC;SACrB,CAAC;IAC9C,CAAC;IAED,iBAAiB;QACf,IAAI,CAAC,WAAW;YACZ,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC7F,CAAC;CACF","sourcesContent":["import {Layout1dBase} from './Layout1dBase.js';\n\n/**\n * TODO @straversi: document and test this Layout.\n */\nexport abstract class Layout1dGrid extends Layout1dBase {\n protected _rolumns: number = 1;\n\n // constructor(config) {\n // super(config);\n // this._rolumns = 1;\n // }\n\n _viewDim2Changed() {\n this._scheduleLayoutUpdate();\n }\n\n _itemDim2Changed() {\n this._scheduleLayoutUpdate();\n }\n\n _getActiveItems() {\n const min = Math.max(0, this._scrollPosition - this._overhang);\n const max = Math.min(\n this._scrollSize,\n this._scrollPosition + this._viewDim1 + this._overhang);\n const firstCow = Math.floor(min / this._delta);\n const lastCow = Math.ceil(max / this._delta) - 1;\n\n this._first = firstCow * this._rolumns;\n this._last =\n Math.min(((lastCow + 1) * this._rolumns) - 1, this._totalItems - 1);\n this._physicalMin = this._delta * firstCow;\n this._physicalMax = this._delta * (lastCow + 1);\n }\n\n _getItemPosition(idx: number): {top: number, left: number} {\n return {\n [this._positionDim]: this._spacing + Math.floor(idx / this._rolumns) * this._delta,\n [this._secondaryPositionDim]: this._spacing +\n ((idx % this._rolumns) * (this._spacing + this._itemDim2))\n } as unknown as {top: number, left: number};\n }\n\n _updateScrollSize() {\n this._scrollSize =\n Math.max(1, Math.ceil(this._totalItems / this._rolumns) * this._delta + this._spacing);\n }\n}\n"]} -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/layouts/Layout1dNaturalSizeGrid.d.ts b/lib/uni-virtualizer/lib/layouts/Layout1dNaturalSizeGrid.d.ts -index aeb4125400544350993c7c2b5c934b5931b9d163..4e5324c76353f7af1f7f4d8f1981ff2684b258ab 100644 ---- a/lib/uni-virtualizer/lib/layouts/Layout1dNaturalSizeGrid.d.ts -+++ b/lib/uni-virtualizer/lib/layouts/Layout1dNaturalSizeGrid.d.ts -@@ -1,6 +1,7 @@ - import { Layout1dGrid } from './Layout1dGrid.js'; -+import { Layout1dBaseConfig } from './Layout1dBase.js'; - import { ItemBox } from './Layout'; --export declare class Layout1dNaturalSizeGrid extends Layout1dGrid { -+export declare class Layout1dNaturalSizeGrid extends Layout1dGrid { - updateItemSizes(sizes: { - [key: number]: ItemBox; - }): void; -diff --git a/lib/uni-virtualizer/lib/layouts/Layout1dNaturalSizeGrid.d.ts.map b/lib/uni-virtualizer/lib/layouts/Layout1dNaturalSizeGrid.d.ts.map -index 602b691726e6fb10dd3c8e430253267e8f0bafcf..5b45aac00d294a71c6a6a6ba4a6fb75ff4530a98 100644 ---- a/lib/uni-virtualizer/lib/layouts/Layout1dNaturalSizeGrid.d.ts.map -+++ b/lib/uni-virtualizer/lib/layouts/Layout1dNaturalSizeGrid.d.ts.map -@@ -1 +1 @@ --{"version":3,"file":"Layout1dNaturalSizeGrid.d.ts","sourceRoot":"","sources":["../../../../src/lib/uni-virtualizer/lib/layouts/Layout1dNaturalSizeGrid.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAC,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAC,OAAO,EAAC,MAAM,UAAU,CAAC;AAEjC,qBAAa,uBAAwB,SAAQ,YAAY;IACrD,eAAe,CAAC,KAAK,EAAE;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAC;IAQ7C,aAAa;CAYlB"} -\ No newline at end of file -+{"version":3,"file":"Layout1dNaturalSizeGrid.d.ts","sourceRoot":"","sources":["../../../../src/lib/uni-virtualizer/lib/layouts/Layout1dNaturalSizeGrid.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAC,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAC,kBAAkB,EAAC,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAC,OAAO,EAAC,MAAM,UAAU,CAAC;AAEjC,qBAAa,uBAAwB,SAAQ,YAAY,CAAC,kBAAkB,CAAC;IACzE,eAAe,CAAC,KAAK,EAAE;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAC;IAQ7C,aAAa;CAYlB"} -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/layouts/Layout1dNaturalSizeGrid.js b/lib/uni-virtualizer/lib/layouts/Layout1dNaturalSizeGrid.js -index 7ba0132588ff7979f3b004188241fa8334abaead..b188b16d0fe9a2889e18fa40f2a18377682632a4 100644 ---- a/lib/uni-virtualizer/lib/layouts/Layout1dNaturalSizeGrid.js -+++ b/lib/uni-virtualizer/lib/layouts/Layout1dNaturalSizeGrid.js -@@ -20,3 +20,4 @@ export class Layout1dNaturalSizeGrid extends Layout1dGrid { - this._spacingChanged = !(_spacing === this._spacing); - } - } -+//# sourceMappingURL=Layout1dNaturalSizeGrid.js.map -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/layouts/Layout1dNaturalSizeGrid.js.map b/lib/uni-virtualizer/lib/layouts/Layout1dNaturalSizeGrid.js.map -new file mode 100644 -index 0000000000000000000000000000000000000000..cf506e8d0dcce673437abc94de413141f2cf4df8 ---- /dev/null -+++ b/lib/uni-virtualizer/lib/layouts/Layout1dNaturalSizeGrid.js.map -@@ -0,0 +1 @@ -+{"version":3,"file":"Layout1dNaturalSizeGrid.js","sourceRoot":"","sources":["../../../../src/lib/uni-virtualizer/lib/layouts/Layout1dNaturalSizeGrid.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAC,MAAM,mBAAmB,CAAC;AAI/C,MAAM,OAAO,uBAAwB,SAAQ,YAAgC;IACzE,eAAe,CAAC,KAA+B;QAC3C,uCAAuC;QACvC,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QACrC,IAAI,IAAI,EAAE;YACR,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;SACtB;IACH,CAAC;IAED,aAAa;QACX,MAAM,EAAC,QAAQ,EAAC,GAAG,IAAI,CAAC;QACxB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;QACzE,IAAI,IAAI,CAAC,QAAQ,GAAG,CAAC,EAAE;YACrB,IAAI,CAAC,QAAQ,GAAG,CAAC,IAAI,CAAC,SAAS,GAAG,CAAC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC;gBAC/D,CAAC,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC;SACzB;aACI;YACH,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC;SACnB;QACD,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC,QAAQ,KAAK,IAAI,CAAC,QAAQ,CAAC,CAAC;IACvD,CAAC;CACN","sourcesContent":["import {Layout1dGrid} from './Layout1dGrid.js';\nimport {Layout1dBaseConfig} from './Layout1dBase.js';\nimport {ItemBox} from './Layout';\n\nexport class Layout1dNaturalSizeGrid extends Layout1dGrid {\n updateItemSizes(sizes: {[key: number]: ItemBox}) {\n // Assume all items have the same size.\n const size = Object.values(sizes)[0];\n if (size) {\n this.itemSize = size;\n }\n }\n \n _updateLayout() {\n const {_spacing} = this;\n this._rolumns = Math.max(1, Math.floor(this._viewDim2 / this._itemDim2));\n if (this._rolumns > 1) {\n this._spacing = (this._viewDim2 % (this._rolumns * this._itemDim2)) /\n (this._rolumns + 1);\n }\n else {\n this._spacing = 0;\n }\n this._spacingChanged = !(_spacing === this._spacing);\n } \n}"]} -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/layouts/Layout1dSquareGrid.d.ts b/lib/uni-virtualizer/lib/layouts/Layout1dSquareGrid.d.ts -index 6aea3ca7ab177d3750be3c5e0179cff8930dbf0b..f7d46726678162fc91735c45111fce60687c1dca 100644 ---- a/lib/uni-virtualizer/lib/layouts/Layout1dSquareGrid.d.ts -+++ b/lib/uni-virtualizer/lib/layouts/Layout1dSquareGrid.d.ts -@@ -1,10 +1,20 @@ -+import { Layout1dBaseConfig } from './Layout1dBase.js'; - import { Layout1dGrid } from './Layout1dGrid.js'; - import { Positions } from './Layout.js'; --export declare class Layout1dSquareGrid extends Layout1dGrid { -+interface Layout1dSquareGridConfig extends Layout1dBaseConfig { -+ spacing?: number; -+ idealSize?: number; -+} -+declare type Layout1dSquareGridSpecifier = Layout1dSquareGridConfig & { -+ type: new (config?: Layout1dSquareGridConfig) => Layout1dSquareGrid; -+}; -+declare type Layout1dSquareGridSpecifierFactory = (config?: Layout1dSquareGridConfig) => Layout1dSquareGridSpecifier; -+export declare const layout1dSquareGrid: Layout1dSquareGridSpecifierFactory; -+export declare class Layout1dSquareGrid extends Layout1dGrid { - protected _idealSize: number; -- constructor(config: any); -- set idealSize(px: any); -+ set idealSize(px: number); - _getItemPosition(idx: number): Positions; - _updateLayout(): void; - } -+export {}; - //# sourceMappingURL=Layout1dSquareGrid.d.ts.map -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/layouts/Layout1dSquareGrid.d.ts.map b/lib/uni-virtualizer/lib/layouts/Layout1dSquareGrid.d.ts.map -index a283b0668bda6eac853d0d53ad7d675f770ecfad..9fcaf8aa40d8116114cd4785986408c7ff0b0feb 100644 ---- a/lib/uni-virtualizer/lib/layouts/Layout1dSquareGrid.d.ts.map -+++ b/lib/uni-virtualizer/lib/layouts/Layout1dSquareGrid.d.ts.map -@@ -1 +1 @@ --{"version":3,"file":"Layout1dSquareGrid.d.ts","sourceRoot":"","sources":["../../../../src/lib/uni-virtualizer/lib/layouts/Layout1dSquareGrid.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAC,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAC,SAAS,EAAC,MAAM,aAAa,CAAC;AAEtC,qBAAa,kBAAmB,SAAQ,YAAY;IAClD,SAAS,CAAC,UAAU,EAAE,MAAM,CAAC;gBAEjB,MAAM,KAAA;IAOlB,IAAI,SAAS,CAAC,EAAE,KAAA,EAKf;IAED,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS;IAIxC,aAAa;CASd"} -\ No newline at end of file -+{"version":3,"file":"Layout1dSquareGrid.d.ts","sourceRoot":"","sources":["../../../../src/lib/uni-virtualizer/lib/layouts/Layout1dSquareGrid.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,kBAAkB,EAAC,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAC,YAAY,EAAC,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAC,SAAS,EAAC,MAAM,aAAa,CAAC;AAEtC,UAAU,wBAAyB,SAAQ,kBAAkB;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,aAAK,2BAA2B,GAAG,wBAAwB,GAAG;IAC5D,IAAI,EAAE,KAAI,MAAM,CAAC,EAAE,wBAAwB,KAAK,kBAAkB,CAAA;CACnE,CAAA;AAED,aAAK,kCAAkC,GAAG,CAAC,MAAM,CAAC,EAAE,wBAAwB,KAAK,2BAA2B,CAAC;AAE7G,eAAO,MAAM,kBAAkB,EAAE,kCAEvB,CAAC;AAEX,qBAAa,kBAAmB,SAAQ,YAAY,CAAC,wBAAwB,CAAC;IAC5E,SAAS,CAAC,UAAU,EAAE,MAAM,CAAO;IASnC,IAAI,SAAS,CAAC,EAAE,EAAE,MAAM,EAKvB;IAED,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS;IAIxC,aAAa;CASd"} -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/layouts/Layout1dSquareGrid.js b/lib/uni-virtualizer/lib/layouts/Layout1dSquareGrid.js -index 27636003a5baa3d56b60693222ed0e84561ff796..ce7b984d5b60e2ae09942df097049732d949390f 100644 ---- a/lib/uni-virtualizer/lib/layouts/Layout1dSquareGrid.js -+++ b/lib/uni-virtualizer/lib/layouts/Layout1dSquareGrid.js -@@ -1,11 +1,18 @@ - import { Layout1dGrid } from './Layout1dGrid.js'; -+export const layout1dSquareGrid = (config) => Object.assign({ -+ type: Layout1dSquareGrid -+}, config); - export class Layout1dSquareGrid extends Layout1dGrid { -- constructor(config) { -- super(config); -- if (config.idealSize === undefined) { -- this._idealSize = 200; -- } -+ constructor() { -+ super(...arguments); -+ this._idealSize = 200; - } -+ // constructor(config) { -+ // super(config); -+ // if (config.idealSize === undefined) { -+ // this._idealSize = 200; -+ // } -+ // } - set idealSize(px) { - if (px !== this._idealSize) { - this._idealSize = px; -@@ -25,3 +32,4 @@ export class Layout1dSquareGrid extends Layout1dGrid { - } - } - } -+//# sourceMappingURL=Layout1dSquareGrid.js.map -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/layouts/Layout1dSquareGrid.js.map b/lib/uni-virtualizer/lib/layouts/Layout1dSquareGrid.js.map -new file mode 100644 -index 0000000000000000000000000000000000000000..701d12281865e1459666458e9418a96696813f3b ---- /dev/null -+++ b/lib/uni-virtualizer/lib/layouts/Layout1dSquareGrid.js.map -@@ -0,0 +1 @@ -+{"version":3,"file":"Layout1dSquareGrid.js","sourceRoot":"","sources":["../../../../src/lib/uni-virtualizer/lib/layouts/Layout1dSquareGrid.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,YAAY,EAAC,MAAM,mBAAmB,CAAC;AAc/C,MAAM,CAAC,MAAM,kBAAkB,GAAuC,CAAC,MAAiC,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC;IACzH,IAAI,EAAE,kBAAkB;CACzB,EAAE,MAAM,CAAC,CAAC;AAEX,MAAM,OAAO,kBAAmB,SAAQ,YAAsC;IAA9E;;QACY,eAAU,GAAW,GAAG,CAAC;IA6BrC,CAAC;IA3BC,wBAAwB;IACxB,mBAAmB;IACnB,0CAA0C;IAC1C,+BAA+B;IAC/B,MAAM;IACN,IAAI;IAEJ,IAAI,SAAS,CAAC,EAAU;QACtB,IAAI,EAAE,KAAK,IAAI,CAAC,UAAU,EAAE;YAC1B,IAAI,CAAC,UAAU,GAAG,EAAE,CAAC;YACrB,IAAI,CAAC,qBAAqB,EAAE,CAAC;SAC9B;IACH,CAAC;IAED,gBAAgB,CAAC,GAAW;QAC1B,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,gBAAgB,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IACpE,CAAC;IAED,aAAa;QACX,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC;QAClD,IAAI,CAAC,QAAQ,GAAG,QAAQ,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAChF,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC;QACzF,IAAI,OAAO,KAAK,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE;YACpC,IAAI,CAAC,SAAS,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;YACrD,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;SAC7B;IACH,CAAC;CACF","sourcesContent":["import {Layout1dBaseConfig} from './Layout1dBase.js';\nimport {Layout1dGrid} from './Layout1dGrid.js';\nimport {Positions} from './Layout.js';\n\ninterface Layout1dSquareGridConfig extends Layout1dBaseConfig {\n spacing?: number,\n idealSize?: number\n}\n\ntype Layout1dSquareGridSpecifier = Layout1dSquareGridConfig & {\n type: new(config?: Layout1dSquareGridConfig) => Layout1dSquareGrid\n}\n\ntype Layout1dSquareGridSpecifierFactory = (config?: Layout1dSquareGridConfig) => Layout1dSquareGridSpecifier;\n\nexport const layout1dSquareGrid: Layout1dSquareGridSpecifierFactory = (config?: Layout1dSquareGridConfig) => Object.assign({\n type: Layout1dSquareGrid\n}, config);\n\nexport class Layout1dSquareGrid extends Layout1dGrid {\n protected _idealSize: number = 200;\n\n // constructor(config) {\n // super(config);\n // if (config.idealSize === undefined) {\n // this._idealSize = 200;\n // }\n // }\n\n set idealSize(px: number) {\n if (px !== this._idealSize) {\n this._idealSize = px;\n this._scheduleLayoutUpdate();\n }\n }\n\n _getItemPosition(idx: number): Positions {\n return Object.assign(super._getItemPosition(idx), this._itemSize);\n }\n\n _updateLayout() {\n const frolumns = this._viewDim2 / this._idealSize;\n this._rolumns = frolumns % 1 < 0.5 ? Math.floor(frolumns) : Math.ceil(frolumns);\n const adjSize = (this._viewDim2 - ((this._rolumns + 1) * this._spacing)) / this._rolumns;\n if (adjSize !== this._itemSize.width) {\n this._itemSize = { width: adjSize, height: adjSize };\n this._spacingChanged = true;\n }\n } \n}"]} -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.d.ts b/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.d.ts -index d9f205869ab7ecc5e93b11c8310941611bbc18fa..da9f9dc7cdee3c8af63f235aec6913cde5362f1d 100644 ---- a/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.d.ts -+++ b/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.d.ts -@@ -1,2 +1,6 @@ --export default function EventTarget(): Promise; -+interface EventTargetConstructor { -+ new (): EventTarget; -+} -+export default function EventTarget(): Promise; -+export {}; - //# sourceMappingURL=EventTarget.d.ts.map -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.d.ts.map b/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.d.ts.map -index ff50f3f8186e7b5ffc8ccf6c656dd39a0d56d9f3..7e5f49dc04b97a08ca30dc564ae41bacf8baa143 100644 ---- a/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.d.ts.map -+++ b/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.d.ts.map -@@ -1 +1 @@ --{"version":3,"file":"EventTarget.d.ts","sourceRoot":"","sources":["../../../../src/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.ts"],"names":[],"mappings":"AAEA,wBAA8B,WAAW,iBAExC"} -\ No newline at end of file -+{"version":3,"file":"EventTarget.d.ts","sourceRoot":"","sources":["../../../../src/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.ts"],"names":[],"mappings":"AAEA,UAAU,sBAAsB;IAC5B,QAAO,WAAW,CAAA;CACrB;AAKD,wBAA8B,WAAW,oCAExC"} -\ No newline at end of file -diff --git a/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js b/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js -index d92179f7fd5315203f870a6963e871dc8ddf6c0c..362e284121b97e0fba0925225777aebc32e26b8d 100644 ---- a/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js -+++ b/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js -@@ -1,14 +1,15 @@ --let _ET, ET; -+let _ET; -+let ET; - export default async function EventTarget() { -- return ET || init(); -+ return ET || init(); - } - async function init() { -- _ET = window.EventTarget; -- try { -- new _ET(); -- } -- catch (_a) { -- _ET = (await import('event-target-shim')).EventTarget; -- } -- return (ET = _ET); -+ _ET = window.EventTarget; -+ try { -+ new _ET(); -+ } catch (_a) { -+ _ET = (await import("event-target-shim")).default.EventTarget; -+ } -+ return (ET = _ET); - } -+//# sourceMappingURL=EventTarget.js.map -diff --git a/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js.map b/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js.map -new file mode 100644 -index 0000000000000000000000000000000000000000..4ce7a8b18f1f07910bae2b2fd08b8b9cddf2d9ca ---- /dev/null -+++ b/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js.map -@@ -0,0 +1 @@ -+{"version":3,"file":"EventTarget.js","sourceRoot":"","sources":["../../../../src/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.ts"],"names":[],"mappings":"AAMA,IAAI,GAA+C,CAAC;AACpD,IAAI,EAA0B,CAAC;AAE/B,MAAM,CAAC,OAAO,CAAC,KAAK,UAAU,WAAW;IACrC,OAAO,EAAE,IAAI,IAAI,EAAE,CAAC;AACxB,CAAC;AAED,KAAK,UAAU,IAAI;IACf,GAAG,GAAG,MAAM,CAAC,WAAW,CAAC;IACzB,IAAI;QACA,IAAI,GAAG,EAAE,CAAC;KACb;IACD,WAAM;QACF,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC,CAAC,WAAW,CAAC;KACzD;IACD,OAAO,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC;AACtB,CAAC","sourcesContent":["type EventTargetModule = typeof import('event-target-shim');\n\ninterface EventTargetConstructor {\n new(): EventTarget\n}\n\nlet _ET: EventTargetModule | EventTargetConstructor;\nlet ET: EventTargetConstructor;\n\nexport default async function EventTarget() {\n return ET || init();\n}\n\nasync function init() {\n _ET = window.EventTarget;\n try {\n new _ET();\n }\n catch {\n _ET = (await import('event-target-shim')).EventTarget;\n }\n return (ET = _ET);\n}"]} -\ No newline at end of file -diff --git a/lib/uni-virtualizer/uni-virtualizer.d.ts b/lib/uni-virtualizer/uni-virtualizer.d.ts -index 60838ab88dbfc1aa5889ba3a24729ac7f7a668e3..4137a2752ade8a4a41893635ed94ea0ad572a2e7 100644 ---- a/lib/uni-virtualizer/uni-virtualizer.d.ts -+++ b/lib/uni-virtualizer/uni-virtualizer.d.ts -@@ -1,4 +1,4 @@ - export { VirtualScroller, RangeChangeEvent, scrollerRef } from './lib/VirtualScroller.js'; --export { Layout1d } from './lib/layouts/Layout1d.js'; -+export { Layout1d, layout1d } from './lib/layouts/Layout1d.js'; - export { Layout1dGrid } from './lib/layouts/Layout1dGrid.js'; - //# sourceMappingURL=uni-virtualizer.d.ts.map -\ No newline at end of file -diff --git a/lib/uni-virtualizer/uni-virtualizer.d.ts.map b/lib/uni-virtualizer/uni-virtualizer.d.ts.map -index d045b7d96beda8cc80bc3a3b82e22ff23131a44f..38e1246ce67e788109d6c9310210a601693b6f95 100644 ---- a/lib/uni-virtualizer/uni-virtualizer.d.ts.map -+++ b/lib/uni-virtualizer/uni-virtualizer.d.ts.map -@@ -1 +1 @@ --{"version":3,"file":"uni-virtualizer.d.ts","sourceRoot":"","sources":["../../src/lib/uni-virtualizer/uni-virtualizer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAE1F,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC"} -\ No newline at end of file -+{"version":3,"file":"uni-virtualizer.d.ts","sourceRoot":"","sources":["../../src/lib/uni-virtualizer/uni-virtualizer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAE1F,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC"} -\ No newline at end of file -diff --git a/lib/uni-virtualizer/uni-virtualizer.js b/lib/uni-virtualizer/uni-virtualizer.js -index abbfd723d9d0013a5afa3641ace55bd4b89be275..ed6b82aedaa51d4c38de726c0db6e21d01759f5f 100644 ---- a/lib/uni-virtualizer/uni-virtualizer.js -+++ b/lib/uni-virtualizer/uni-virtualizer.js -@@ -1,3 +1,4 @@ - export { VirtualScroller, scrollerRef } from './lib/VirtualScroller.js'; --export { Layout1d } from './lib/layouts/Layout1d.js'; -+export { Layout1d, layout1d } from './lib/layouts/Layout1d.js'; - export { Layout1dGrid } from './lib/layouts/Layout1dGrid.js'; -+//# sourceMappingURL=uni-virtualizer.js.map -\ No newline at end of file -diff --git a/lib/uni-virtualizer/uni-virtualizer.js.map b/lib/uni-virtualizer/uni-virtualizer.js.map -new file mode 100644 -index 0000000000000000000000000000000000000000..326d70cd4eedd257fb6fbb0078255e2882c49724 ---- /dev/null -+++ b/lib/uni-virtualizer/uni-virtualizer.js.map -@@ -0,0 +1 @@ -+{"version":3,"file":"uni-virtualizer.js","sourceRoot":"","sources":["../../src/lib/uni-virtualizer/uni-virtualizer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAoB,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAE1F,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC","sourcesContent":["export { VirtualScroller, RangeChangeEvent, scrollerRef } from './lib/VirtualScroller.js';\n\nexport { Layout1d, layout1d } from './lib/layouts/Layout1d.js';\nexport { Layout1dGrid } from './lib/layouts/Layout1dGrid.js';\n"]} -\ No newline at end of file -diff --git a/lit-virtualizer.d.ts b/lit-virtualizer.d.ts -index 1c857d4bc1aeb43b56a7923b990ddcd094b09be2..58204903e4356b38a34df0ba29fec6390d435078 100644 ---- a/lit-virtualizer.d.ts -+++ b/lit-virtualizer.d.ts -@@ -1,4 +1,4 @@ - export { scroll } from './lib/scroll.js'; --export { Layout1d, Layout1dGrid, RangeChangeEvent, scrollerRef } from './lib/uni-virtualizer/uni-virtualizer.js'; -+export { Layout1d, layout1d, Layout1dGrid, RangeChangeEvent, scrollerRef } from './lib/uni-virtualizer/uni-virtualizer.js'; - export { LitVirtualizer } from './lib/lit-virtualizer.js'; - //# sourceMappingURL=lit-virtualizer.d.ts.map -\ No newline at end of file -diff --git a/lit-virtualizer.d.ts.map b/lit-virtualizer.d.ts.map -index 3a1ccb65e6cefe6192415cdbb5e6dfa1f273233c..08ad361b84e378b6052924266a57fca5a48177b0 100644 ---- a/lit-virtualizer.d.ts.map -+++ b/lit-virtualizer.d.ts.map -@@ -1 +1 @@ --{"version":3,"file":"lit-virtualizer.d.ts","sourceRoot":"","sources":["src/lit-virtualizer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,0CAA0C,CAAC;AACjH,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC"} -\ No newline at end of file -+{"version":3,"file":"lit-virtualizer.d.ts","sourceRoot":"","sources":["src/lit-virtualizer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,0CAA0C,CAAC;AAC3H,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC"} -\ No newline at end of file -diff --git a/lit-virtualizer.js b/lit-virtualizer.js -index 1774c8a7f63ab390fd6bb96ce2f58dccef32c211..ef3497547e82647ab7282927e83a32ed39456b4c 100644 ---- a/lit-virtualizer.js -+++ b/lit-virtualizer.js -@@ -1,4 +1,5 @@ - // export { repeat } from './lib/repeat.js'; - export { scroll } from './lib/scroll.js'; --export { Layout1d, Layout1dGrid, scrollerRef } from './lib/uni-virtualizer/uni-virtualizer.js'; -+export { Layout1d, layout1d, Layout1dGrid, scrollerRef } from './lib/uni-virtualizer/uni-virtualizer.js'; - export { LitVirtualizer } from './lib/lit-virtualizer.js'; -+//# sourceMappingURL=lit-virtualizer.js.map -\ No newline at end of file -diff --git a/lit-virtualizer.js.map b/lit-virtualizer.js.map -new file mode 100644 -index 0000000000000000000000000000000000000000..c0c3a0f6bb2af1571380e7f79e81a6affa787f18 ---- /dev/null -+++ b/lit-virtualizer.js.map -@@ -0,0 +1 @@ -+{"version":3,"file":"lit-virtualizer.js","sourceRoot":"","sources":["src/lit-virtualizer.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAoB,WAAW,EAAE,MAAM,0CAA0C,CAAC;AAC3H,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC","sourcesContent":["// export { repeat } from './lib/repeat.js';\nexport { scroll } from './lib/scroll.js';\nexport { Layout1d, layout1d, Layout1dGrid, RangeChangeEvent, scrollerRef } from './lib/uni-virtualizer/uni-virtualizer.js';\nexport { LitVirtualizer } from './lib/lit-virtualizer.js';\n"]} -\ No newline at end of file -diff --git a/package.json b/package.json -index b9854ae5453193acdc822d0b06e02674733844a1..f1194c01e74606efba3403df51f2adbadd5b0da6 100644 ---- a/package.json -+++ b/package.json -@@ -45,8 +45,7 @@ - "rollup-plugin-node-resolve": "^4.2.3", - "rollup-plugin-terser": "^5.0.0", - "tachometer": "^0.4.7", -- "tslint": "^5.18.0", -- "typescript": "^4.0.2" -+ "typescript": "^4.1.3" - }, - "dependencies": { - "event-target-shim": "^5.0.1", diff --git a/.yarn/patches/@lit-labs/virtualizer/event-target-shim.patch b/.yarn/patches/@lit-labs/virtualizer/event-target-shim.patch index dfb0a686fe..b5b2421b48 100644 --- a/.yarn/patches/@lit-labs/virtualizer/event-target-shim.patch +++ b/.yarn/patches/@lit-labs/virtualizer/event-target-shim.patch @@ -1,11 +1,10 @@ -diff --git a/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js b/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js -index d92179f7fd5315203f870a6963e871dc8ddf6c0c..362e284121b97e0fba0925225777aebc32e26b8d 100644 ---- a/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js -+++ b/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js -@@ -1,14 +1,15 @@ --let _ET, ET; -+let _ET; -+let ET; +diff --git a/polyfillLoaders/EventTarget.js b/polyfillLoaders/EventTarget.js +index 4e18ade7ba485849f17f28c94c42f0e0e01ac387..8f34f4f646c7f7becc208fb5a546c96034fc74dc 100644 +--- a/polyfillLoaders/EventTarget.js ++++ b/polyfillLoaders/EventTarget.js +@@ -6,16 +6,15 @@ + let _ET; + let ET; export default async function EventTarget() { - return ET || init(); + return ET || init(); @@ -26,4 +25,5 @@ index d92179f7fd5315203f870a6963e871dc8ddf6c0c..362e284121b97e0fba0925225777aebc + _ET = (await import("event-target-shim")).default.EventTarget; + } + return (ET = _ET); - } \ No newline at end of file + } + //# sourceMappingURL=EventTarget.js.map \ No newline at end of file diff --git a/README.md b/README.md index 62b0343f95..685dddba09 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This is the repository for the official [Home Assistant](https://home-assistant.io) frontend. -[![Screenshot of the frontend](https://raw.githubusercontent.com/home-assistant/home-assistant-polymer/master/docs/screenshot.png)](https://demo.home-assistant.io/) +[![Screenshot of the frontend](https://raw.githubusercontent.com/home-assistant/frontend/master/docs/screenshot.png)](https://demo.home-assistant.io/) - [View demo of Home Assistant](https://demo.home-assistant.io/) - [More information about Home Assistant](https://home-assistant.io) diff --git a/build-scripts/bundle.js b/build-scripts/bundle.js index 2325d80bca..2205196266 100644 --- a/build-scripts/bundle.js +++ b/build-scripts/bundle.js @@ -10,7 +10,7 @@ module.exports.ignorePackages = ({ latestBuild }) => [ ]; // Files from NPM packages that we should replace with empty file -module.exports.emptyPackages = ({ latestBuild }) => +module.exports.emptyPackages = ({ latestBuild, isHassioBuild }) => [ // Contains all color definitions for all material color sets. // We don't use it @@ -28,6 +28,15 @@ module.exports.emptyPackages = ({ latestBuild }) => ), // This polyfill is loaded in workers to support ES5, filter it out. latestBuild && require.resolve("proxy-polyfill/src/index.js"), + // Icons in supervisor conflict with icons in HA so we don't load. + isHassioBuild && + require.resolve( + path.resolve(paths.polymer_dir, "src/components/ha-icon.ts") + ), + isHassioBuild && + require.resolve( + path.resolve(paths.polymer_dir, "src/components/ha-icon-picker.ts") + ), ].filter(Boolean); module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({ @@ -196,6 +205,7 @@ module.exports.config = { publicPath: publicPath(latestBuild, paths.hassio_publicPath), isProdBuild, latestBuild, + isHassioBuild: true, defineOverlay: { __SUPERVISOR__: true, }, diff --git a/build-scripts/webpack.js b/build-scripts/webpack.js index bc07a7ec9f..185ef3aa65 100644 --- a/build-scripts/webpack.js +++ b/build-scripts/webpack.js @@ -30,6 +30,7 @@ const createWebpackConfig = ({ isProdBuild, latestBuild, isStatsBuild, + isHassioBuild, dontHash, }) => { if (!dontHash) { @@ -117,7 +118,9 @@ const createWebpackConfig = ({ }, }), new webpack.NormalModuleReplacementPlugin( - new RegExp(bundle.emptyPackages({ latestBuild }).join("|")), + new RegExp( + bundle.emptyPackages({ latestBuild, isHassioBuild }).join("|") + ), path.resolve(paths.polymer_dir, "src/util/empty.js") ), !isProdBuild && new LogStartCompilePlugin(), diff --git a/cast/src/receiver/layout/hc-lovelace.ts b/cast/src/receiver/layout/hc-lovelace.ts index 122ce510fd..3cb24f24cf 100644 --- a/cast/src/receiver/layout/hc-lovelace.ts +++ b/cast/src/receiver/layout/hc-lovelace.ts @@ -7,6 +7,9 @@ import "../../../../src/panels/lovelace/views/hui-view"; import { HomeAssistant } from "../../../../src/types"; import "./hc-launch-screen"; +(window as any).loadCardHelpers = () => + import("../../../../src/panels/lovelace/custom-card-helpers"); + @customElement("hc-lovelace") class HcLovelace extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; diff --git a/demo/src/entrypoint.ts b/demo/src/entrypoint.ts index 54feef73d5..456bba9de4 100644 --- a/demo/src/entrypoint.ts +++ b/demo/src/entrypoint.ts @@ -2,8 +2,3 @@ import "../../src/resources/ha-style"; import "../../src/resources/roboto"; import "../../src/resources/safari-14-attachshadow-patch"; import "./ha-demo"; - -/* polyfill for paper-dropdown */ -setTimeout(() => { - import("web-animations-js/web-animations-next-lite.min"); -}, 1000); diff --git a/gallery/sidebar.js b/gallery/sidebar.js index 84bd8f8eff..02ffeb2aa8 100644 --- a/gallery/sidebar.js +++ b/gallery/sidebar.js @@ -20,7 +20,6 @@ module.exports = [ "editor-trigger", "editor-condition", "editor-action", - "selectors", "trace", "trace-timeline", ], diff --git a/gallery/src/components/demo-black-white-row.ts b/gallery/src/components/demo-black-white-row.ts index 92a4beeffe..549f10554c 100644 --- a/gallery/src/components/demo-black-white-row.ts +++ b/gallery/src/components/demo-black-white-row.ts @@ -3,6 +3,7 @@ import { html, LitElement, css, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element"; import { fireEvent } from "../../../src/common/dom/fire_event"; +import "../../../src/components/ha-card"; @customElement("demo-black-white-row") class DemoBlackWhiteRow extends LitElement { diff --git a/gallery/src/pages/automation/describe-action.ts b/gallery/src/pages/automation/describe-action.ts index 55c3317acc..dd3d6c6e93 100644 --- a/gallery/src/pages/automation/describe-action.ts +++ b/gallery/src/pages/automation/describe-action.ts @@ -3,10 +3,20 @@ import { html, css, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import "../../../../src/components/ha-card"; import { describeAction } from "../../../../src/data/script_i18n"; +import { getEntity } from "../../../../src/fake_data/entity"; import { provideHass } from "../../../../src/fake_data/provide_hass"; import { HomeAssistant } from "../../../../src/types"; -const actions = [ +const ENTITIES = [ + getEntity("scene", "kitchen_morning", "scening", { + friendly_name: "Kitchen Morning", + }), + getEntity("media_player", "kitchen", "playing", { + friendly_name: "Sonos Kitchen", + }), +]; + +const ACTIONS = [ { wait_template: "{{ true }}", alias: "Something with an alias" }, { delay: "0:05" }, { wait_template: "{{ true }}" }, @@ -19,8 +29,20 @@ const actions = [ device_id: "abcdefgh", domain: "plex", entity_id: "media_player.kitchen", + type: "turn_on", }, { scene: "scene.kitchen_morning" }, + { + service: "scene.turn_on", + target: { entity_id: "scene.kitchen_morning" }, + metadata: {}, + }, + { + service: "media_player.play_media", + target: { entity_id: "media_player.kitchen" }, + data: { media_content_id: "", media_content_type: "" }, + metadata: { title: "Happy Song" }, + }, { wait_for_trigger: [ { @@ -52,7 +74,7 @@ export class DemoAutomationDescribeAction extends LitElement { } return html` - ${actions.map( + ${ACTIONS.map( (conf) => html`
${describeAction(this.hass, conf as any)} @@ -68,6 +90,7 @@ export class DemoAutomationDescribeAction extends LitElement { super.firstUpdated(changedProps); const hass = provideHass(this); hass.updateTranslations(null, "en"); + hass.addEntities(ENTITIES); } static get styles() { diff --git a/gallery/src/pages/automation/editor-action.ts b/gallery/src/pages/automation/editor-action.ts index 01769ccaa8..1f7a0d8206 100644 --- a/gallery/src/pages/automation/editor-action.ts +++ b/gallery/src/pages/automation/editor-action.ts @@ -14,7 +14,7 @@ import { HaDelayAction } from "../../../../src/panels/config/automation/action/t import { HaDeviceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-device_id"; import { HaEventAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-event"; import { HaRepeatAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-repeat"; -import { HaSceneAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-scene"; +import { HaSceneAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-activate_scene"; import { HaServiceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-service"; import { HaWaitForTriggerAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger"; import { HaWaitAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_template"; diff --git a/gallery/src/pages/automation/selectors.markdown b/gallery/src/pages/automation/selectors.markdown deleted file mode 100644 index 6342871182..0000000000 --- a/gallery/src/pages/automation/selectors.markdown +++ /dev/null @@ -1,3 +0,0 @@ ---- -title: Selectors ---- diff --git a/gallery/src/pages/automation/selectors.ts b/gallery/src/pages/automation/selectors.ts deleted file mode 100644 index 1e66055887..0000000000 --- a/gallery/src/pages/automation/selectors.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* eslint-disable lit/no-template-arrow */ -import { LitElement, TemplateResult, html } from "lit"; -import { customElement, state } from "lit/decorators"; -import { provideHass } from "../../../../src/fake_data/provide_hass"; -import type { HomeAssistant } from "../../../../src/types"; -import "../../components/demo-black-white-row"; -import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry"; -import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry"; -import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry"; -import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor"; -import "../../../../src/panels/config/automation/trigger/ha-automation-trigger"; -import { Selector } from "../../../../src/data/selector"; -import "../../../../src/components/ha-selector/ha-selector"; - -const SCHEMAS: { name: string; selector: Selector }[] = [ - { name: "Addon", selector: { addon: {} } }, - - { name: "Entity", selector: { entity: {} } }, - { name: "Device", selector: { device: {} } }, - { name: "Area", selector: { area: {} } }, - { name: "Target", selector: { target: {} } }, - { - name: "Number", - selector: { - number: { - min: 0, - max: 10, - }, - }, - }, - { name: "Boolean", selector: { boolean: {} } }, - { name: "Time", selector: { time: {} } }, - { name: "Action", selector: { action: {} } }, - { name: "Text", selector: { text: { multiline: false } } }, - { name: "Text Multiline", selector: { text: { multiline: true } } }, - { name: "Object", selector: { object: {} } }, - { - name: "Select", - selector: { - select: { - options: ["Everyone Home", "Some Home", "All gone"], - }, - }, - }, -]; - -@customElement("demo-automation-selectors") -class DemoHaSelector extends LitElement { - @state() private hass!: HomeAssistant; - - private data: any = SCHEMAS.map(() => undefined); - - constructor() { - super(); - const hass = provideHass(this); - hass.updateTranslations(null, "en"); - hass.updateTranslations("config", "en"); - mockEntityRegistry(hass); - mockDeviceRegistry(hass); - mockAreaRegistry(hass); - mockHassioSupervisor(hass); - } - - protected render(): TemplateResult { - const valueChanged = (ev) => { - const sampleIdx = ev.target.sampleIdx; - this.data[sampleIdx] = ev.detail.value; - this.requestUpdate(); - }; - return html` - ${SCHEMAS.map( - (info, sampleIdx) => html` - - ${["light", "dark"].map( - (slot) => - html` - - ` - )} - - ` - )} - `; - } -} - -declare global { - interface HTMLElementTagNameMap { - "demo-automation-selectors": DemoHaSelector; - } -} diff --git a/gallery/src/pages/components/ha-form.ts b/gallery/src/pages/components/ha-form.ts index 324eb3a58d..b6ff6d1711 100644 --- a/gallery/src/pages/components/ha-form.ts +++ b/gallery/src/pages/components/ha-form.ts @@ -1,11 +1,17 @@ /* eslint-disable lit/no-template-arrow */ import "@material/mwc-button"; import { LitElement, TemplateResult, html } from "lit"; -import { customElement } from "lit/decorators"; +import { customElement, state } from "lit/decorators"; import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data"; import type { HaFormSchema } from "../../../../src/components/ha-form/types"; import "../../../../src/components/ha-form/ha-form"; import "../../components/demo-black-white-row"; +import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry"; +import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry"; +import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry"; +import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor"; +import { provideHass } from "../../../../src/fake_data/provide_hass"; +import { HomeAssistant } from "../../../../src/types"; const SCHEMAS: { title: string; @@ -14,6 +20,63 @@ const SCHEMAS: { schema: HaFormSchema[]; data?: Record; }[] = [ + { + title: "Selectors", + translations: { + addon: "Addon", + entity: "Entity", + device: "Device", + area: "Area", + target: "Target", + number: "Number", + boolean: "Boolean", + time: "Time", + action: "Action", + text: "Text", + text_multiline: "Text Multiline", + object: "Object", + select: "Select", + icon: "Icon", + media: "Media", + }, + schema: [ + { name: "addon", selector: { addon: {} } }, + { name: "entity", selector: { entity: {} } }, + { + name: "Attribute", + selector: { attribute: { entity_id: "" } }, + }, + { name: "Device", selector: { device: {} } }, + { name: "Duration", selector: { duration: {} } }, + { name: "area", selector: { area: {} } }, + { name: "target", selector: { target: {} } }, + { name: "number", selector: { number: { min: 0, max: 10 } } }, + { name: "boolean", selector: { boolean: {} } }, + { name: "time", selector: { time: {} } }, + { name: "action", selector: { action: {} } }, + { name: "text", selector: { text: { multiline: false } } }, + { name: "text_multiline", selector: { text: { multiline: true } } }, + { name: "object", selector: { object: {} } }, + { + name: "select", + selector: { + select: { options: ["Everyone Home", "Some Home", "All gone"] }, + }, + }, + { + name: "icon", + selector: { + icon: {}, + }, + }, + { + name: "media", + selector: { + media: {}, + }, + }, + ], + }, { title: "Authentication", translations: { @@ -50,13 +113,11 @@ const SCHEMAS: { { type: "boolean", name: "bool", - optional: true, default: false, }, { type: "integer", name: "int", - optional: true, default: 10, }, { @@ -67,7 +128,6 @@ const SCHEMAS: { { type: "string", name: "string", - optional: true, default: "Default", }, { @@ -77,7 +137,6 @@ const SCHEMAS: { ["other", "other"], ], name: "select", - optional: true, default: "default", }, { @@ -87,7 +146,6 @@ const SCHEMAS: { other: "Other", }, name: "multi", - optional: true, default: ["default"], }, { @@ -108,7 +166,6 @@ const SCHEMAS: { { type: "integer", name: "int with default", - optional: true, default: 10, }, { @@ -122,7 +179,6 @@ const SCHEMAS: { { type: "integer", name: "int range optional", - optional: true, valueMin: 0, valueMax: 10, }, @@ -148,7 +204,6 @@ const SCHEMAS: { ["other", "Other"], ], name: "select optional", - optional: true, }, { type: "select", @@ -161,7 +216,6 @@ const SCHEMAS: { ["option", "1000"], ], name: "select many otions", - optional: true, default: "default", }, ], @@ -190,7 +244,6 @@ const SCHEMAS: { option: "1000", }, name: "multi many otions", - optional: true, default: ["default"], }, ], @@ -239,23 +292,35 @@ const SCHEMAS: { valueMin: 1, valueMax: 65535, name: "port", - optional: true, default: 80, }, - { type: "string", name: "path", optional: true, default: "/" }, - { type: "boolean", name: "ssl", optional: true, default: false }, + { type: "string", name: "path", default: "/" }, + { type: "boolean", name: "ssl", default: false }, ], }, ]; @customElement("demo-components-ha-form") class DemoHaForm extends LitElement { + @state() private hass!: HomeAssistant; + private data = SCHEMAS.map( ({ schema, data }) => data || computeInitialHaFormData(schema) ); private disabled = SCHEMAS.map(() => false); + constructor() { + super(); + const hass = provideHass(this); + hass.updateTranslations(null, "en"); + hass.updateTranslations("config", "en"); + mockEntityRegistry(hass); + mockDeviceRegistry(hass); + mockAreaRegistry(hass); + mockHassioSupervisor(hass); + } + protected render(): TemplateResult { return html` ${SCHEMAS.map((info, idx) => { @@ -278,6 +343,7 @@ class DemoHaForm extends LitElement { (slot) => html` ({})); @@ -73,12 +183,130 @@ class DemoHaSelector extends LitElement { const hass = provideHass(this); hass.updateTranslations(null, "en"); hass.updateTranslations("config", "en"); + hass.addEntities(ENTITIES); mockEntityRegistry(hass); - mockDeviceRegistry(hass); - mockAreaRegistry(hass); + mockDeviceRegistry(hass, DEVICES); + mockAreaRegistry(hass, AREAS); mockHassioSupervisor(hass); + hass.mockWS("auth/sign_path", (params) => params); + hass.mockWS("media_player/browse_media", this._browseMedia); } + public provideHass(el) { + el.hass = this.hass; + } + + public connectedCallback() { + super.connectedCallback(); + this.addEventListener("show-dialog", this._dialogManager); + } + + public disconnectedCallback() { + super.disconnectedCallback(); + this.removeEventListener("show-dialog", this._dialogManager); + } + + private _browseMedia = ({ media_content_id }) => { + if (media_content_id === undefined) { + return { + title: "Media", + media_class: "directory", + media_content_type: "", + media_content_id: "media-source://media_source/local/.", + can_play: false, + can_expand: true, + children_media_class: "directory", + thumbnail: null, + children: [ + { + title: "Misc", + media_class: "directory", + media_content_type: "", + media_content_id: "media-source://media_source/local/misc", + can_play: false, + can_expand: true, + children_media_class: null, + thumbnail: null, + }, + { + title: "Movies", + media_class: "directory", + media_content_type: "", + media_content_id: "media-source://media_source/local/movies", + can_play: true, + can_expand: true, + children_media_class: "movie", + thumbnail: null, + }, + { + title: "Music", + media_class: "album", + media_content_type: "", + media_content_id: "media-source://media_source/local/music", + can_play: false, + can_expand: true, + children_media_class: "music", + thumbnail: "/images/album_cover_2.jpg", + }, + ], + }; + } + return { + title: "Subfolder", + media_class: "directory", + media_content_type: "", + media_content_id: "media-source://media_source/local/sub", + can_play: false, + can_expand: true, + children_media_class: "directory", + thumbnail: null, + children: [ + { + title: "audio.mp3", + media_class: "music", + media_content_type: "audio/mpeg", + media_content_id: "media-source://media_source/local/audio.mp3", + can_play: true, + can_expand: false, + children_media_class: null, + thumbnail: "/images/album_cover.jpg", + }, + { + title: "image.jpg", + media_class: "image", + media_content_type: "image/jpeg", + media_content_id: "media-source://media_source/local/image.jpg", + can_play: true, + can_expand: false, + children_media_class: null, + thumbnail: null, + }, + { + title: "movie.mp4", + media_class: "movie", + media_content_type: "image/jpeg", + media_content_id: "media-source://media_source/local/movie.mp4", + can_play: true, + can_expand: false, + children_media_class: null, + thumbnail: null, + }, + ], + }; + }; + + private _dialogManager = (e) => { + const { dialogTag, dialogImport, dialogParams, addHistory } = e.detail; + showDialog( + this, + this.shadowRoot!, + dialogTag, + dialogParams, + dialogImport, + addHistory + ); + }; + protected render(): TemplateResult { return html` ${SCHEMAS.map((info, idx) => { @@ -117,7 +345,6 @@ class DemoHaSelector extends LitElement { } static styles = css` - paper-input, ha-selector { width: 60; } diff --git a/gallery/src/pages/misc/integration-card.ts b/gallery/src/pages/misc/integration-card.ts index e9d74f2f1b..53f16a16b2 100644 --- a/gallery/src/pages/misc/integration-card.ts +++ b/gallery/src/pages/misc/integration-card.ts @@ -29,6 +29,7 @@ const createConfigEntry = ( source: "zeroconf", state: "loaded", supports_options: false, + supports_remove_device: false, supports_unload: true, disabled_by: null, pref_disable_new_entities: false, diff --git a/hassio/src/addon-store/hassio-addon-repository.ts b/hassio/src/addon-store/hassio-addon-repository.ts index 49da9c3c21..43e9822df2 100644 --- a/hassio/src/addon-store/hassio-addon-repository.ts +++ b/hassio/src/addon-store/hassio-addon-repository.ts @@ -42,7 +42,9 @@ class HassioAddonRepositoryEl extends LitElement { const repo = this.repo; let _addons = this.addons; if (!this.hass.userData?.showAdvanced) { - _addons = _addons.filter((addon) => !addon.advanced); + _addons = _addons.filter( + (addon) => !addon.advanced && addon.stage === "stable" + ); } const addons = this._getAddons(_addons, this.filter); diff --git a/hassio/src/addon-store/hassio-addon-store.ts b/hassio/src/addon-store/hassio-addon-store.ts index b83d282787..2e071dc145 100644 --- a/hassio/src/addon-store/hassio-addon-store.ts +++ b/hassio/src/addon-store/hassio-addon-store.ts @@ -221,13 +221,14 @@ class HassioAddonStore extends LitElement { margin-top: 24px; } .search { - padding: 0 16px; - background: var(--sidebar-background-color); - border-bottom: 1px solid var(--divider-color); + position: sticky; + top: 0; + z-index: 2; } - .search search-input { - position: relative; - top: 2px; + search-input { + display: block; + --mdc-text-field-fill-color: var(--sidebar-background-color); + --mdc-text-field-idle-line-color: var(--divider-color); } .advanced { padding: 12px; diff --git a/hassio/src/addon-view/config/hassio-addon-audio.ts b/hassio/src/addon-view/config/hassio-addon-audio.ts index 0851a1269b..acf59950fa 100644 --- a/hassio/src/addon-view/config/hassio-addon-audio.ts +++ b/hassio/src/addon-view/config/hassio-addon-audio.ts @@ -1,7 +1,5 @@ import "@material/mwc-button"; -import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; +import "@material/mwc-list/mwc-list-item"; import { css, CSSResultGroup, @@ -11,10 +9,11 @@ import { TemplateResult, } from "lit"; import { customElement, property, state } from "lit/decorators"; -import "web-animations-js/web-animations-next-lite.min"; +import { stopPropagation } from "../../../../src/common/dom/stop_propagation"; import "../../../../src/components/buttons/ha-progress-button"; import "../../../../src/components/ha-alert"; import "../../../../src/components/ha-card"; +import "../../../../src/components/ha-select"; import { HassioAddonDetails, HassioAddonSetOptionParams, @@ -57,49 +56,44 @@ class HassioAddonAudio extends LitElement { ${this._error ? html`${this._error}` : ""} - - - - ${this._inputDevices && - this._inputDevices.map( - (item) => html` - - ${item.name} - - ` - )} - - - html` + + ${item.name} + + ` + )} + `} + ${this._outputDevices && + html` - - ${this._outputDevices && - this._outputDevices.map( - (item) => html` - ${item.name} - ` - )} - - + ${this._outputDevices.map( + (item) => html` + ${item.name} + ` + )} + `}
@@ -116,8 +110,7 @@ class HassioAddonAudio extends LitElement { hassioStyle, css` :host, - ha-card, - paper-dropdown-menu { + ha-card { display: block; } paper-item { @@ -126,24 +119,30 @@ class HassioAddonAudio extends LitElement { .card-actions { text-align: right; } + ha-select { + width: 100%; + } + ha-select:last-child { + margin-top: 8px; + } `, ]; } - protected update(changedProperties: PropertyValues): void { - super.update(changedProperties); + protected willUpdate(changedProperties: PropertyValues): void { + super.willUpdate(changedProperties); if (changedProperties.has("addon")) { this._addonChanged(); } } private _setInputDevice(ev): void { - const device = ev.detail.item.getAttribute("device"); + const device = ev.target.value; this._selectedInput = device; } private _setOutputDevice(ev): void { - const device = ev.detail.item.getAttribute("device"); + const device = ev.target.value; this._selectedOutput = device; } diff --git a/hassio/src/addon-view/info/hassio-addon-info.ts b/hassio/src/addon-view/info/hassio-addon-info.ts index 91e072ba72..47e6087e36 100644 --- a/hassio/src/addon-view/info/hassio-addon-info.ts +++ b/hassio/src/addon-view/info/hassio-addon-info.ts @@ -9,6 +9,7 @@ import { mdiFlask, mdiHomeAssistant, mdiKey, + mdiLinkLock, mdiNetwork, mdiNumeric1, mdiNumeric2, @@ -16,6 +17,8 @@ import { mdiNumeric4, mdiNumeric5, mdiNumeric6, + mdiNumeric7, + mdiNumeric8, mdiPound, mdiShield, } from "@mdi/js"; @@ -31,6 +34,7 @@ import "../../../../src/components/buttons/ha-progress-button"; import "../../../../src/components/ha-alert"; import "../../../../src/components/ha-card"; import "../../../../src/components/ha-chip"; +import "../../../../src/components/ha-chip-set"; import "../../../../src/components/ha-markdown"; import "../../../../src/components/ha-settings-row"; import "../../../../src/components/ha-svg-icon"; @@ -84,6 +88,8 @@ const RATING_ICON = { 4: mdiNumeric4, 5: mdiNumeric5, 6: mdiNumeric6, + 7: mdiNumeric7, + 8: mdiNumeric8, }; @customElement("hassio-addon-info") @@ -209,7 +215,7 @@ class HassioAddonInfo extends LitElement { >`}
-
+ ${this.addon.stage !== "stable" ? html` = 6, + yellow: [3, 4, 5].includes(Number(this.addon.rating)), + red: Number(this.addon.rating) >= 2, })} @click=${this._showMoreInfo} id="rating" @@ -364,7 +370,17 @@ class HassioAddonInfo extends LitElement { ` : ""} -
+ ${this.addon.signed + ? html` + + + ${this.supervisor.localize( + "addon.dashboard.capability.label.signed" + )} + + ` + : ""} +
${this.addon.description}.
diff --git a/hassio/src/components/supervisor-backup-content.ts b/hassio/src/components/supervisor-backup-content.ts index d1422521b8..5d0b71741f 100644 --- a/hassio/src/components/supervisor-backup-content.ts +++ b/hassio/src/components/supervisor-backup-content.ts @@ -1,7 +1,7 @@ import { mdiFolder, mdiHomeAssistant, mdiPuzzle } from "@mdi/js"; import { PaperInputElement } from "@polymer/paper-input/paper-input"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property } from "lit/decorators"; +import { customElement, property, query } from "lit/decorators"; import { atLeastVersion } from "../../../src/common/config/version"; import { formatDate } from "../../../src/common/datetime/format_date"; import { formatDateTime } from "../../../src/common/datetime/format_date_time"; @@ -92,6 +92,8 @@ export class SupervisorBackupContent extends LitElement { @property() public confirmBackupPassword = ""; + @query("paper-input, ha-radio, ha-checkbox", true) private _focusTarget; + public willUpdate(changedProps) { super.willUpdate(changedProps); if (!this.hasUpdated) { @@ -109,6 +111,10 @@ export class SupervisorBackupContent extends LitElement { } } + public override focus() { + this._focusTarget?.focus(); + } + private _localize = (string: string) => this.supervisor?.localize(`backup.${string}`) || this.localize!(`ui.panel.page-onboarding.restore.${string}`); @@ -169,24 +175,23 @@ export class SupervisorBackupContent extends LitElement { : ""} ${this.backupType === "partial" ? html`
- ${this.backup && this.backup.homeassistant - ? html` - - `} - > - - - - ` - : ""} + + `} + > + + + + ${foldersSection?.templates.length ? html`
diff --git a/hassio/src/dialogs/backup/dialog-hassio-backup.ts b/hassio/src/dialogs/backup/dialog-hassio-backup.ts index 5a5fc61810..a993113ee5 100644 --- a/hassio/src/dialogs/backup/dialog-hassio-backup.ts +++ b/hassio/src/dialogs/backup/dialog-hassio-backup.ts @@ -92,6 +92,7 @@ class HassioBackupDialog .backup=${this._backup} .onboarding=${this._dialogParams.onboarding || false} .localize=${this._dialogParams.localize} + dialogInitialFocus > `} ${this._error diff --git a/hassio/src/dialogs/backup/dialog-hassio-create-backup.ts b/hassio/src/dialogs/backup/dialog-hassio-create-backup.ts index 0c73b81e88..f5fe92e28d 100644 --- a/hassio/src/dialogs/backup/dialog-hassio-create-backup.ts +++ b/hassio/src/dialogs/backup/dialog-hassio-create-backup.ts @@ -61,6 +61,7 @@ class HassioCreateBackupDialog extends LitElement { : html` `} ${this._error diff --git a/hassio/src/dialogs/datadisk/dialog-hassio-datadisk.ts b/hassio/src/dialogs/datadisk/dialog-hassio-datadisk.ts index f91c6362f7..92eda0e5b5 100644 --- a/hassio/src/dialogs/datadisk/dialog-hassio-datadisk.ts +++ b/hassio/src/dialogs/datadisk/dialog-hassio-datadisk.ts @@ -1,12 +1,11 @@ -import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; +import "@material/mwc-list/mwc-list-item"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../src/common/dom/fire_event"; import "../../../../src/components/ha-circular-progress"; import "../../../../src/components/ha-markdown"; +import "../../../../src/components/ha-select"; import { extractApiErrorMessage, ignoreSupervisorError, @@ -90,18 +89,20 @@ class HassioDatadiskDialog extends LitElement { )}

- - - ${this.devices.map( - (device) => html`${device}` - )} - - + ${this.devices.map( + (device) => + html`${device}` + )} + ` : this.devices === undefined ? this.dialogParams.supervisor.localize( @@ -111,7 +112,11 @@ class HassioDatadiskDialog extends LitElement { "dialog.datadisk_move.no_devices" )} - + ${this.dialogParams.supervisor.localize( "dialog.datadisk_move.cancel" )} @@ -130,8 +135,8 @@ class HassioDatadiskDialog extends LitElement { `; } - private _select_device(event) { - this.selectedDevice = event.detail.value; + private _select_device(ev) { + this.selectedDevice = ev.target.value; } private async _moveDatadisk() { @@ -156,7 +161,7 @@ class HassioDatadiskDialog extends LitElement { haStyle, haStyleDialog, css` - paper-dropdown-menu { + ha-select { width: 100%; } ha-circular-progress { diff --git a/hassio/src/dialogs/hardware/dialog-hassio-hardware.ts b/hassio/src/dialogs/hardware/dialog-hassio-hardware.ts index ca94af7c8d..4022305946 100755 --- a/hassio/src/dialogs/hardware/dialog-hassio-hardware.ts +++ b/hassio/src/dialogs/hardware/dialog-hassio-hardware.ts @@ -80,7 +80,7 @@ class HassioHardwareDialog extends LitElement { > - + `; } diff --git a/hassio/src/dialogs/network/dialog-hassio-network.ts b/hassio/src/dialogs/network/dialog-hassio-network.ts index 3e3c8720b4..898806009c 100644 --- a/hassio/src/dialogs/network/dialog-hassio-network.ts +++ b/hassio/src/dialogs/network/dialog-hassio-network.ts @@ -119,6 +119,7 @@ export class DialogHassioNetwork html` ` )} @@ -315,6 +316,7 @@ export class DialogHassioNetwork value="auto" name="${version}method" .checked=${this._interface![version]?.method === "auto"} + dialogInitialFocus > diff --git a/hassio/src/dialogs/registries/dialog-hassio-registries.ts b/hassio/src/dialogs/registries/dialog-hassio-registries.ts index 1eb0072a3f..7ca0b3de1c 100644 --- a/hassio/src/dialogs/registries/dialog-hassio-registries.ts +++ b/hassio/src/dialogs/registries/dialog-hassio-registries.ts @@ -19,22 +19,21 @@ import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; import type { HomeAssistant } from "../../../../src/types"; import { RegistriesDialogParams } from "./show-dialog-registries"; -const SCHEMA = [ +const SCHEMA: HaFormSchema[] = [ { - type: "string", name: "registry", required: true, + selector: { text: {} }, }, { - type: "string", name: "username", required: true, + selector: { text: {} }, }, { - type: "string", name: "password", required: true, - format: "password", + selector: { text: { type: "password" } }, }, ]; @@ -81,6 +80,7 @@ class HassioRegistriesDialog extends LitElement { .schema=${SCHEMA} @value-changed=${this._valueChanged} .computeLabel=${this._computeLabel} + dialogInitialFocus >
`}
- + ${this.supervisor.localize( "dialog.registries.add_new_registry" )} diff --git a/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts b/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts index b9707f5986..b208093465 100644 --- a/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts +++ b/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts @@ -139,6 +139,7 @@ class HassioRepositoriesDialog extends LitElement { "dialog.repositories.add" )} @keydown=${this._handleKeyAdd} + dialogInitialFocus > ${this._processing diff --git a/hassio/src/system/hassio-core-info.ts b/hassio/src/system/hassio-core-info.ts index dc3589246e..df078742d7 100644 --- a/hassio/src/system/hassio-core-info.ts +++ b/hassio/src/system/hassio-core-info.ts @@ -205,16 +205,6 @@ class HassioCoreInfo extends LitElement { color: var(--secondary-text-color); --mdc-menu-min-width: 200px; } - @media (min-width: 563px) { - paper-listbox { - max-height: 150px; - overflow: auto; - } - } - paper-item { - cursor: pointer; - min-height: 35px; - } mwc-list-item ha-svg-icon { color: var(--secondary-text-color); } diff --git a/hassio/src/system/hassio-host-info.ts b/hassio/src/system/hassio-host-info.ts index ff51c8903b..bd743fe8b2 100644 --- a/hassio/src/system/hassio-host-info.ts +++ b/hassio/src/system/hassio-host-info.ts @@ -440,16 +440,6 @@ class HassioHostInfo extends LitElement { color: var(--secondary-text-color); --mdc-menu-min-width: 200px; } - @media (min-width: 563px) { - paper-listbox { - max-height: 150px; - overflow: auto; - } - } - paper-item { - cursor: pointer; - min-height: 35px; - } mwc-list-item ha-svg-icon { color: var(--secondary-text-color); } diff --git a/hassio/src/system/hassio-supervisor-log.ts b/hassio/src/system/hassio-supervisor-log.ts index 09f518703c..a18f3c1d3b 100644 --- a/hassio/src/system/hassio-supervisor-log.ts +++ b/hassio/src/system/hassio-supervisor-log.ts @@ -1,12 +1,10 @@ import "@material/mwc-button"; -import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/ha-alert"; import "../../../src/components/ha-card"; +import "../../../src/components/ha-select"; import { extractApiErrorMessage } from "../../../src/data/hassio/common"; import { fetchHassioLogs } from "../../../src/data/hassio/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor"; @@ -73,24 +71,19 @@ class HassioSupervisorLog extends LitElement { : ""} ${this.hass.userData?.showAdvanced ? html` - - - ${logProviders.map( - (provider) => html` - - ${provider.name} - - ` - )} - - + ${logProviders.map( + (provider) => html` + + ${provider.name} + + ` + )} + ` : ""} @@ -110,7 +103,7 @@ class HassioSupervisorLog extends LitElement { } private async _setLogProvider(ev): Promise { - const provider = ev.detail.item.getAttribute("provider"); + const provider = ev.target.value; this._selectedLogProvider = provider; this._loadData(); } @@ -153,9 +146,9 @@ class HassioSupervisorLog extends LitElement { pre { white-space: pre-wrap; } - paper-dropdown-menu { - padding: 0 2%; - width: 96%; + ha-select { + width: 100%; + margin-bottom: 4px; } `, ]; diff --git a/hassio/src/update-available/update-available-card.ts b/hassio/src/update-available/update-available-card.ts index 5012d61567..a5a2bc81a2 100644 --- a/hassio/src/update-available/update-available-card.ts +++ b/hassio/src/update-available/update-available-card.ts @@ -10,7 +10,6 @@ import { import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../src/common/dom/fire_event"; -import "../../../src/common/search/search-input"; import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/ha-alert"; import "../../../src/components/ha-button-menu"; @@ -192,13 +191,7 @@ class UpdateAvailableCard extends LitElement { ` : ""} - + ${this.supervisor.localize("common.update")}
@@ -360,8 +353,14 @@ class UpdateAvailableCard extends LitElement { } private async _update() { + if (this._shouldCreateBackup && this.supervisor.info.state === "freeze") { + this._error = this.supervisor.localize("backup.backup_already_running"); + return; + } + this._error = undefined; this._updating = true; + try { if (this._updateType === "addon") { await updateHassioAddon( diff --git a/package.json b/package.json index 2e31be62fd..6562576055 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { - "description": "A frontend for Home Assistant using the Polymer framework", + "description": "A frontend for Home Assistant", "repository": { "type": "git", - "url": "https://github.com/home-assistant/home-assistant-polymer" + "url": "https://github.com/home-assistant/frontend" }, "name": "home-assistant-frontend", "version": "1.0.0", @@ -22,17 +22,18 @@ "license": "Apache-2.0", "dependencies": { "@braintree/sanitize-url": "^5.0.2", - "@codemirror/commands": "^0.19.5", - "@codemirror/gutter": "^0.19.4", - "@codemirror/highlight": "^0.19.6", - "@codemirror/history": "^0.19.0", + "@codemirror/autocomplete": "^0.19.12", + "@codemirror/commands": "^0.19.8", + "@codemirror/gutter": "^0.19.9", + "@codemirror/highlight": "^0.19.7", + "@codemirror/history": "^0.19.2", "@codemirror/legacy-modes": "^0.19.0", "@codemirror/rectangular-selection": "^0.19.1", - "@codemirror/search": "^0.19.2", - "@codemirror/state": "^0.19.4", - "@codemirror/stream-parser": "^0.19.2", - "@codemirror/text": "^0.19.5", - "@codemirror/view": "^0.19.15", + "@codemirror/search": "^0.19.6", + "@codemirror/state": "^0.19.6", + "@codemirror/stream-parser": "^0.19.5", + "@codemirror/text": "^0.19.6", + "@codemirror/view": "^0.19.40", "@formatjs/intl-datetimeformat": "^4.2.5", "@formatjs/intl-getcanonicallocales": "^1.8.0", "@formatjs/intl-locale": "^2.4.40", @@ -45,7 +46,8 @@ "@fullcalendar/daygrid": "5.9.0", "@fullcalendar/interaction": "5.9.0", "@fullcalendar/list": "5.9.0", - "@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.6.0#./.yarn/patches/@lit-labs/virtualizer/0.7.0.patch", + "@lit-labs/motion": "^1.0.2", + "@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.7.0-pre.2#./.yarn/patches/@lit-labs/virtualizer/event-target-shim.patch", "@material/chips": "14.0.0-canary.261f2db59.0", "@material/data-table": "14.0.0-canary.261f2db59.0", "@material/mwc-button": "0.25.3", @@ -57,7 +59,7 @@ "@material/mwc-formfield": "0.25.3", "@material/mwc-icon-button": "patch:@material/mwc-icon-button@0.25.3#./.yarn/patches/@material/mwc-icon-button/remove-icon.patch", "@material/mwc-linear-progress": "0.25.3", - "@material/mwc-list": "0.25.3", + "@material/mwc-list": "^0.25.3", "@material/mwc-menu": "0.25.3", "@material/mwc-radio": "0.25.3", "@material/mwc-ripple": "0.25.3", @@ -66,6 +68,7 @@ "@material/mwc-switch": "0.25.3", "@material/mwc-tab": "0.25.3", "@material/mwc-tab-bar": "0.25.3", + "@material/mwc-textarea": "^0.25.3", "@material/mwc-textfield": "0.25.3", "@material/mwc-top-app-bar-fixed": "^0.25.3", "@material/top-app-bar": "14.0.0-canary.261f2db59.0", @@ -87,13 +90,15 @@ "@polymer/paper-tooltip": "^3.0.1", "@polymer/polymer": "3.4.1", "@thomasloven/round-slider": "0.5.4", - "@vaadin/vaadin-combo-box": "^21.0.2", - "@vaadin/vaadin-date-picker": "^21.0.2", + "@vaadin/combo-box": "^22.0.4", + "@vaadin/vaadin-themable-mixin": "^22.0.4", "@vibrant/color": "^3.2.1-alpha.1", "@vibrant/core": "^3.2.1-alpha.1", "@vibrant/quantizer-mmcq": "^3.2.1-alpha.1", "@vue/web-component-wrapper": "^1.2.0", + "@webcomponents/scoped-custom-element-registry": "^0.0.5", "@webcomponents/webcomponentsjs": "^2.2.10", + "app-datepicker": "^5.0.1", "chart.js": "^3.3.2", "comlink": "^4.3.1", "core-js": "^3.15.2", @@ -103,7 +108,7 @@ "deep-freeze": "^0.0.1", "fuse.js": "^6.0.0", "google-timezones-json": "^1.0.2", - "hls.js": "^1.0.11", + "hls.js": "^1.1.5", "home-assistant-js-websocket": "^6.0.1", "idb-keyval": "^5.1.3", "intl-messageformat": "^9.9.1", @@ -111,7 +116,7 @@ "leaflet": "^1.7.1", "leaflet-draw": "^1.0.4", "lit": "^2.1.2", - "lit-vaadin-helpers": "^0.2.1", + "lit-vaadin-helpers": "^0.3.0", "marked": "^3.0.2", "memoize-one": "^5.2.1", "node-vibrant": "3.2.1-alpha.1", diff --git a/script/core b/script/core index 00d663cab2..79ba38a348 100755 --- a/script/core +++ b/script/core @@ -4,6 +4,8 @@ # Stop on errors set -e +WD="${WORKSPACE_DIRECTORY:=/workspaces/frontend}" + if [ -z "${DEVCONTAINER}" ]; then echo "This task should only run inside a devcontainer, for local install HA Core in a venv." exit 1 @@ -16,9 +18,9 @@ if [ -z $(which hass) ]; then git+git://github.com/home-assistant/home-assistant.git@dev fi -if [ ! -d "/workspaces/frontend/config" ]; then +if [ ! -d "${WD}/config" ]; then echo "Creating default configuration." - mkdir -p "/workspaces/frontend/config"; + mkdir -p "${WD}/config"; hass --script ensure_config -c config echo "demo: @@ -26,24 +28,24 @@ logger: default: info logs: homeassistant.components.frontend: debug -" >> /workspaces/frontend/config/configuration.yaml +" >> "${WD}/config/configuration.yaml" if [ ! -z "${HASSIO}" ]; then echo " # frontend: -# development_repo: /workspaces/frontend +# development_repo: ${WD} hassio: - development_repo: /workspaces/frontend" >> /workspaces/frontend/config/configuration.yaml + development_repo: ${WD}" >> "${WD}/config/configuration.yaml" else echo " frontend: - development_repo: /workspaces/frontend + development_repo: ${WD} # hassio: -# development_repo: /workspaces/frontend" >> /workspaces/frontend/config/configuration.yaml +# development_repo: ${WD}" >> "${WD}/config/configuration.yaml" fi fi -hass -c /workspaces/frontend/config +hass -c "${WD}/config" diff --git a/setup.cfg b/setup.cfg index 96130d2df7..6205a6a259 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = home-assistant-frontend -version = 20220203.1 +version = 20220223.0 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 diff --git a/src/common/datetime/create_duration_data.ts b/src/common/datetime/create_duration_data.ts index 92b3d01021..3b743471ca 100644 --- a/src/common/datetime/create_duration_data.ts +++ b/src/common/datetime/create_duration_data.ts @@ -1,11 +1,11 @@ -import { HaDurationData } from "../../components/ha-duration-input"; -import { ForDict } from "../../data/automation"; +import type { HaDurationData } from "../../components/ha-duration-input"; +import type { ForDict } from "../../data/automation"; export const createDurationData = ( duration: string | number | ForDict | undefined -): HaDurationData => { +): HaDurationData | undefined => { if (duration === undefined) { - return {}; + return undefined; } if (typeof duration !== "object") { if (typeof duration === "string" || isNaN(duration)) { @@ -19,6 +19,9 @@ export const createDurationData = ( } return { seconds: duration }; } + if (!("days" in duration)) { + return duration; + } const { days, minutes, seconds, milliseconds } = duration; let hours = duration.hours || 0; hours = (hours || 0) + (days || 0) * 24; diff --git a/src/common/entity/can_toggle_domain.ts b/src/common/entity/can_toggle_domain.ts index d1c4a08b7c..df87bfcf7a 100644 --- a/src/common/entity/can_toggle_domain.ts +++ b/src/common/entity/can_toggle_domain.ts @@ -1,4 +1,4 @@ -import { HomeAssistant } from "../../types"; +import type { HomeAssistant } from "../../types"; export const canToggleDomain = (hass: HomeAssistant, domain: string) => { const services = hass.services[domain]; diff --git a/src/common/entity/can_toggle_state.ts b/src/common/entity/can_toggle_state.ts index f0480b60ff..afc8dd9f9c 100644 --- a/src/common/entity/can_toggle_state.ts +++ b/src/common/entity/can_toggle_state.ts @@ -1,14 +1,30 @@ -import { HassEntity } from "home-assistant-js-websocket"; -import { HomeAssistant } from "../../types"; +import type { HassEntity } from "home-assistant-js-websocket"; +import type { HomeAssistant } from "../../types"; import { canToggleDomain } from "./can_toggle_domain"; import { computeStateDomain } from "./compute_state_domain"; import { supportsFeature } from "./supports-feature"; export const canToggleState = (hass: HomeAssistant, stateObj: HassEntity) => { const domain = computeStateDomain(stateObj); + if (domain === "group") { - return stateObj.state === "on" || stateObj.state === "off"; + if ( + stateObj.attributes?.entity_id?.some((entity) => { + const entityStateObj = hass.states[entity]; + if (!entityStateObj) { + return false; + } + + const entityDomain = computeStateDomain(entityStateObj); + return canToggleDomain(hass, entityDomain); + }) + ) { + return stateObj.state === "on" || stateObj.state === "off"; + } + + return false; } + if (domain === "climate") { return supportsFeature(stateObj, 4096); } diff --git a/src/common/entity/cover_icon.ts b/src/common/entity/cover_icon.ts index 4308306890..c7fa277ecb 100644 --- a/src/common/entity/cover_icon.ts +++ b/src/common/entity/cover_icon.ts @@ -120,6 +120,7 @@ export const computeOpenIcon = (stateObj: HassEntity): string => { case "awning": case "door": case "gate": + case "curtain": return mdiArrowExpandHorizontal; default: return mdiArrowUp; @@ -131,6 +132,7 @@ export const computeCloseIcon = (stateObj: HassEntity): string => { case "awning": case "door": case "gate": + case "curtain": return mdiArrowCollapseHorizontal; default: return mdiArrowDown; diff --git a/src/common/entity/strip_prefix_from_entity_name.ts b/src/common/entity/strip_prefix_from_entity_name.ts index 1efa3704d5..e976b7b18f 100644 --- a/src/common/entity/strip_prefix_from_entity_name.ts +++ b/src/common/entity/strip_prefix_from_entity_name.ts @@ -1,24 +1,32 @@ +const SUFFIXES = [" ", ": "]; + /** * Strips a device name from an entity name. * @param entityName the entity name - * @param lowerCasedPrefixWithSpaceSuffix the prefix to strip, lower cased with a space suffix + * @param lowerCasedPrefix the prefix to strip, lower cased * @returns */ export const stripPrefixFromEntityName = ( entityName: string, - lowerCasedPrefixWithSpaceSuffix: string + lowerCasedPrefix: string ) => { - if (!entityName.toLowerCase().startsWith(lowerCasedPrefixWithSpaceSuffix)) { - return undefined; + const lowerCasedEntityName = entityName.toLowerCase(); + + for (const suffix of SUFFIXES) { + const lowerCasedPrefixWithSuffix = `${lowerCasedPrefix}${suffix}`; + + if (lowerCasedEntityName.startsWith(lowerCasedPrefixWithSuffix)) { + const newName = entityName.substring(lowerCasedPrefixWithSuffix.length); + + // If first word already has an upper case letter (e.g. from brand name) + // leave as-is, otherwise capitalize the first word. + return hasUpperCase(newName.substr(0, newName.indexOf(" "))) + ? newName + : newName[0].toUpperCase() + newName.slice(1); + } } - const newName = entityName.substring(lowerCasedPrefixWithSpaceSuffix.length); - - // If first word already has an upper case letter (e.g. from brand name) - // leave as-is, otherwise capitalize the first word. - return hasUpperCase(newName.substr(0, newName.indexOf(" "))) - ? newName - : newName[0].toUpperCase() + newName.slice(1); + return undefined; }; const hasUpperCase = (str: string): boolean => str.toLowerCase() !== str; diff --git a/src/common/search/search-input.ts b/src/common/search/search-input.ts index 08084af98a..ae1d48811a 100644 --- a/src/common/search/search-input.ts +++ b/src/common/search/search-input.ts @@ -1,17 +1,10 @@ import { mdiClose, mdiMagnify } from "@mdi/js"; -import "@polymer/paper-input/paper-input"; -import type { PaperInputElement } from "@polymer/paper-input/paper-input"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, query } from "lit/decorators"; import "../../components/ha-icon-button"; import "../../components/ha-svg-icon"; +import "../../components/ha-textfield"; +import type { HaTextField } from "../../components/ha-textfield"; import { HomeAssistant } from "../../types"; import { fireEvent } from "../dom/fire_event"; @@ -21,11 +14,8 @@ class SearchInput extends LitElement { @property() public filter?: string; - @property({ type: Boolean, attribute: "no-label-float" }) - public noLabelFloat? = false; - - @property({ type: Boolean, attribute: "no-underline" }) - public noUnderline = false; + @property({ type: Boolean }) + public suffix = false; @property({ type: Boolean }) public autofocus = false; @@ -34,49 +24,44 @@ class SearchInput extends LitElement { public label?: string; public focus() { - this.shadowRoot!.querySelector("paper-input")!.focus(); + this._input?.focus(); } - @query("paper-input", true) private _input!: PaperInputElement; + @query("ha-textfield", true) private _input!: HaTextField; protected render(): TemplateResult { return html` - - - + + - ${this.filter && - html` - - `} - +
+ ${this.filter && + html` + + `} + +
+ `; } - protected updated(changedProps: PropertyValues) { - if ( - changedProps.has("noUnderline") && - (this.noUnderline || changedProps.get("noUnderline") !== undefined) - ) { - ( - this._input.inputElement!.parentElement!.shadowRoot!.querySelector( - "div.unfocused-line" - ) as HTMLElement - ).style.display = this.noUnderline ? "none" : "block"; - } - } - private async _filterChanged(value: string) { fireEvent(this, "value-changed", { value: String(value) }); } @@ -91,15 +76,25 @@ class SearchInput extends LitElement { static get styles(): CSSResultGroup { return css` + :host { + display: inline-flex; + } ha-svg-icon, ha-icon-button { color: var(--primary-text-color); } - ha-icon-button { - --mdc-icon-button-size: 24px; + ha-svg-icon { + outline: none; } - ha-svg-icon.prefix { - margin: 8px; + .clear-button { + --mdc-icon-size: 20px; + } + ha-textfield { + display: inherit; + } + .trailing { + display: flex; + align-items: center; } `; } diff --git a/src/common/style/icon_color_css.ts b/src/common/style/icon_color_css.ts index a0905c0ea9..7e00bf1c77 100644 --- a/src/common/style/icon_color_css.ts +++ b/src/common/style/icon_color_css.ts @@ -15,6 +15,7 @@ export const iconColorCSS = css` ha-state-icon[data-domain="media_player"][data-state="on"], ha-state-icon[data-domain="media_player"][data-state="paused"], ha-state-icon[data-domain="media_player"][data-state="playing"], + ha-state-icon[data-domain="remote"][data-state="on"], ha-state-icon[data-domain="script"][data-state="on"], ha-state-icon[data-domain="sun"][data-state="above_horizon"], ha-state-icon[data-domain="switch"][data-state="on"], diff --git a/src/common/util/debounce.ts b/src/common/util/debounce.ts index 70aaddf2e0..cd61f072b9 100644 --- a/src/common/util/debounce.ts +++ b/src/common/util/debounce.ts @@ -11,7 +11,7 @@ export const debounce = ( immediate = false ) => { let timeout: number | undefined; - return (...args: T): void => { + const debouncedFunc = (...args: T): void => { const later = () => { timeout = undefined; if (!immediate) { @@ -25,4 +25,8 @@ export const debounce = ( func(...args); } }; + debouncedFunc.cancel = () => { + clearTimeout(timeout); + }; + return debouncedFunc; }; diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index b2d669105b..d199040c5c 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -1,4 +1,3 @@ -import { Layout1d, scroll } from "@lit-labs/virtualizer"; import { mdiArrowDown, mdiArrowUp } from "@mdi/js"; import deepClone from "deep-clone-simple"; import { @@ -31,6 +30,7 @@ import type { HaCheckbox } from "../ha-checkbox"; import "../ha-svg-icon"; import { filterData, sortData } from "./sort-filter"; import { HomeAssistant } from "../../types"; +import "@lit-labs/virtualizer"; declare global { // for fire event @@ -70,6 +70,7 @@ export interface DataTableSortColumnData { export interface DataTableColumnData extends DataTableSortColumnData { title: TemplateResult | string; + label?: TemplateResult | string; type?: "numeric" | "icon" | "icon-button" | "overflow-menu"; template?: (data: any, row: T) => TemplateResult | string; width?: string; @@ -294,6 +295,7 @@ export class HaDataTable extends LitElement { }; return html`
` : html` -
- ${scroll({ - items: this._items, - layout: Layout1d, - renderItem: (row: DataTableRowData, index) => { - // not sure how this happens... - if (!row) { - return html``; - } - if (row.append) { - return html` -
${row.content}
- `; - } - if (row.empty) { - return html`
`; - } - return html` -
- ${this.selectable - ? html` -
- - -
- ` - : ""} - ${Object.entries(this.columns).map( - ([key, column]) => { - if (column.hidden) { - return ""; - } - return html` -
- ${column.template - ? column.template(row[key], row) - : row[key]} -
- `; - } - )} -
- `; - }, - })} -
+ .items=${this._items} + .renderItem=${this._renderRow} + > `}
`; } + private _renderRow = ( + row: DataTableRowData, + index: number + ): TemplateResult => { + // not sure how this happens... + if (!row) { + return html``; + } + if (row.append) { + return html`
${row.content}
`; + } + if (row.empty) { + return html`
`; + } + return html` +
+ ${this.selectable + ? html` +
+ + +
+ ` + : ""} + ${Object.entries(this.columns).map(([key, column]) => { + if (column.hidden) { + return ""; + } + return html` +
+ ${column.template ? column.template(row[key], row) : row[key]} +
+ `; + })} +
+ `; + }; + private async _sortFilterData() { const startTime = new Date().getTime(); this.curRequest++; @@ -536,7 +526,7 @@ export class HaDataTable extends LitElement { } } - private _handleRowCheckboxClick(ev: Event) { + private _handleRowCheckboxClick = (ev: Event) => { const checkbox = ev.currentTarget as HaCheckbox; const rowId = (checkbox as any).rowId; @@ -549,16 +539,16 @@ export class HaDataTable extends LitElement { this._checkedRows = this._checkedRows.filter((row) => row !== rowId); } this._checkedRowsChanged(); - } + }; - private _handleRowClick(ev: Event) { + private _handleRowClick = (ev: Event) => { const target = ev.target as HTMLElement; if (["HA-CHECKBOX", "MWC-BUTTON"].includes(target.tagName)) { return; } const rowId = (ev.currentTarget as any).rowId; fireEvent(this, "row-click", { id: rowId }, { bubbles: false }); - } + }; private _checkedRowsChanged() { // force scroller to update, change it's items @@ -571,6 +561,9 @@ export class HaDataTable extends LitElement { } private _handleSearchChange(ev: CustomEvent): void { + if (this.filter) { + return; + } this._debounceSearch(ev.detail.value); } @@ -935,11 +928,10 @@ export class HaDataTable extends LitElement { } .table-header { border-bottom: 1px solid var(--divider-color); - padding: 0 16px; } search-input { - position: relative; - top: 2px; + display: block; + flex: 1; } slot[name="header"] { display: block; @@ -952,6 +944,7 @@ export class HaDataTable extends LitElement { } .scroller { height: calc(100% - 57px); + overflow: overlay !important; } .mdc-data-table__table.auto-height .scroller { @@ -967,6 +960,9 @@ export class HaDataTable extends LitElement { .clickable { cursor: pointer; } + lit-virtualizer { + contain: size layout !important; + } `, ]; } diff --git a/src/components/device/ha-area-devices-picker.ts b/src/components/device/ha-area-devices-picker.ts index f863bacd45..6ce007e441 100644 --- a/src/components/device/ha-area-devices-picker.ts +++ b/src/components/device/ha-area-devices-picker.ts @@ -1,20 +1,7 @@ import "@material/mwc-button/mwc-button"; -import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-item/paper-item-body"; -import "@polymer/paper-listbox/paper-listbox"; -import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers"; +import { html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { ComboBoxLitRenderer } from "lit-vaadin-helpers"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../common/dom/fire_event"; @@ -50,36 +37,12 @@ interface AreaDevices { devices: string[]; } -// eslint-disable-next-line lit/prefer-static-styles -const rowRenderer: ComboBoxLitRenderer = (item) => html` - - - -
${item.name}
-
${item.devices.length} devices
-
-
`; +const rowRenderer: ComboBoxLitRenderer = ( + item +) => html` + ${item.name} + ${item.devices.length} devices +`; @customElement("ha-area-devices-picker") export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) { @@ -117,9 +80,6 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) { @property({ type: Array, attribute: "include-device-classes" }) public includeDeviceClasses?: string[]; - @property({ type: Boolean }) - private _opened?: boolean; - @state() private _areaPicker = true; @state() private _devices?: DeviceRegistryEntry[]; @@ -302,71 +262,30 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) { `; } return html` - - -
- ${this.value - ? html` ` - : ""} - ${areas.length > 0 - ? html` - - ` - : ""} -
-
-
- Choose individual devices + + + Choose individual devices + `; } - private _clearValue(ev: Event) { - ev.stopPropagation(); - this._setValue([]); - } - private get _value() { return this.value || []; } - private _openedChanged(ev: PolymerChangedEvent) { - this._opened = ev.detail.value; - } - private async _switchPicker() { this._areaPicker = !this._areaPicker; } @@ -398,22 +317,6 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) { fireEvent(this, "change"); }, 0); } - - static get styles(): CSSResultGroup { - return css` - .suffix { - display: flex; - } - ha-icon-button { - --mdc-icon-button-size: 24px; - padding: 0px 2px; - color: var(--secondary-text-color); - } - [hidden] { - display: none; - } - `; - } } declare global { diff --git a/src/components/device/ha-device-automation-picker.ts b/src/components/device/ha-device-automation-picker.ts index e4cc705dce..57b46e5b5b 100644 --- a/src/components/device/ha-device-automation-picker.ts +++ b/src/components/device/ha-device-automation-picker.ts @@ -1,7 +1,4 @@ -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-item/paper-item-body"; -import "@polymer/paper-listbox/paper-listbox"; +import "@material/mwc-list/mwc-list-item"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { property, state } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; @@ -10,7 +7,7 @@ import { deviceAutomationsEqual, } from "../../data/device_automation"; import { HomeAssistant } from "../../types"; -import "../ha-paper-dropdown-menu"; +import "../ha-select"; const NO_AUTOMATION_KEY = "NO_AUTOMATION"; const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION"; @@ -67,14 +64,12 @@ export abstract class HaDeviceAutomationPicker< this._createNoAutomation = createNoAutomation; } - private get _key() { - if ( - !this.value || - deviceAutomationsEqual( - this._createNoAutomation(this.deviceId), - this.value - ) - ) { + private get _value() { + if (!this.value) { + return ""; + } + + if (!this._automations.length) { return NO_AUTOMATION_KEY; } @@ -93,42 +88,32 @@ export abstract class HaDeviceAutomationPicker< if (this._renderEmpty) { return html``; } + const value = this._value; return html` - - - - - ${this._automations.map( - (automation, idx) => html` - - ${this._localizeDeviceAutomation(this.hass, automation)} - - ` - )} - - + ${value === NO_AUTOMATION_KEY + ? html` + ${this.NO_AUTOMATION_TEXT} + ` + : ""} + ${value === UNKNOWN_AUTOMATION_KEY + ? html` + ${this.UNKNOWN_AUTOMATION_TEXT} + ` + : ""} + ${this._automations.map( + (automation, idx) => html` + + ${this._localizeDeviceAutomation(this.hass, automation)} + + ` + )} + `; } @@ -138,14 +123,6 @@ export abstract class HaDeviceAutomationPicker< if (changedProps.has("deviceId")) { this._updateDeviceInfo(); } - - // The value has changed, force the listbox to update - if (changedProps.has("value") || changedProps.has("_renderEmpty")) { - const listbox = this.shadowRoot!.querySelector("paper-listbox")!; - if (listbox) { - listbox._selectSelected(this._key); - } - } } private async _updateDeviceInfo() { @@ -168,9 +145,16 @@ export abstract class HaDeviceAutomationPicker< } private _automationChanged(ev) { - if (ev.detail.item.automation) { - this._setValue(ev.detail.item.automation); + const value = ev.target.value; + if (!value || [UNKNOWN_AUTOMATION_KEY, NO_AUTOMATION_KEY].includes(value)) { + return; } + const [deviceId, idx] = value.split("_"); + const automation = this._automations[idx]; + if (automation.device_id !== deviceId) { + return; + } + this._setValue(automation); } private _setValue(automation: T) { @@ -183,14 +167,9 @@ export abstract class HaDeviceAutomationPicker< static get styles(): CSSResultGroup { return css` - ha-paper-dropdown-menu { + ha-select { width: 100%; - } - paper-listbox { - min-width: 200px; - } - paper-item { - cursor: pointer; + margin-top: 4px; } `; } diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts index 3b13c963e1..d1a7d2b55a 100644 --- a/src/components/device/ha-device-picker.ts +++ b/src/components/device/ha-device-picker.ts @@ -1,18 +1,9 @@ -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-item/paper-item-body"; +import "@material/mwc-list/mwc-list-item"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property, state, query } from "lit/decorators"; -import memoizeOne from "memoize-one"; +import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { ComboBoxLitRenderer } from "lit-vaadin-helpers"; -import { mdiCheck } from "@mdi/js"; +import { customElement, property, query, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { fireEvent } from "../../common/dom/fire_event"; import { computeDomain } from "../../common/entity/compute_domain"; import { stringCompare } from "../../common/string/compare"; @@ -46,36 +37,12 @@ export type HaDevicePickerDeviceFilterFunc = ( device: DeviceRegistryEntry ) => boolean; -// eslint-disable-next-line lit/prefer-static-styles -const rowRenderer: ComboBoxLitRenderer = (item) => html` - - - - ${item.name} - ${item.area} - - `; +const rowRenderer: ComboBoxLitRenderer = (item) => html` + ${item.name} + ${item.area} +`; @customElement("ha-device-picker") export class HaDevicePicker extends SubscribeMixin(LitElement) { @@ -138,7 +105,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { if (!devices.length) { return [ { - id: "", + id: "no_devices", area: "", name: this.hass.localize("ui.components.device-picker.no_devices"), }, @@ -234,7 +201,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { if (!outputDevices.length) { return [ { - id: "", + id: "no_devices", area: "", name: this.hass.localize("ui.components.device-picker.no_match"), }, @@ -303,7 +270,6 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { .renderer=${rowRenderer} .disabled=${this.disabled} item-value-path="id" - item-id-path="id" item-label-path="name" @opened-changed=${this._openedChanged} @value-changed=${this._deviceChanged} @@ -317,7 +283,11 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { private _deviceChanged(ev: PolymerChangedEvent) { ev.stopPropagation(); - const newValue = ev.detail.value; + let newValue = ev.detail.value; + + if (newValue === "no_devices") { + newValue = ""; + } if (newValue !== this._value) { this._setValue(newValue); @@ -335,19 +305,6 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { fireEvent(this, "change"); }, 0); } - - static get styles(): CSSResultGroup { - return css` - paper-input > ha-icon-button { - --mdc-icon-button-size: 24px; - padding: 2px; - color: var(--secondary-text-color); - } - [hidden] { - display: none; - } - `; - } } declare global { diff --git a/src/components/entity/ha-entities-picker.ts b/src/components/entity/ha-entities-picker.ts index 19c20886fa..35e21dc950 100644 --- a/src/components/entity/ha-entities-picker.ts +++ b/src/components/entity/ha-entities-picker.ts @@ -1,5 +1,5 @@ import type { HassEntity } from "home-assistant-js-websocket"; -import { html, LitElement, TemplateResult } from "lit"; +import { css, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; import { isValidEntityId } from "../../common/entity/valid_entity_id"; @@ -114,7 +114,7 @@ class HaEntitiesPickerLight extends LitElement { const newValue = event.detail.value; if ( newValue === curValue || - (newValue !== "" && !isValidEntityId(newValue)) + (newValue !== undefined && !isValidEntityId(newValue)) ) { return; } @@ -145,6 +145,12 @@ class HaEntitiesPickerLight extends LitElement { this._updateEntities([...currentEntities, toAdd]); } + + static override styles = css` + div { + margin-top: 8px; + } + `; } declare global { diff --git a/src/components/entity/ha-entity-attribute-picker.ts b/src/components/entity/ha-entity-attribute-picker.ts index d199c4f7b6..291d30da5e 100644 --- a/src/components/entity/ha-entity-attribute-picker.ts +++ b/src/components/entity/ha-entity-attribute-picker.ts @@ -1,54 +1,14 @@ -import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-item/paper-item"; -import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light"; import { HassEntity } from "home-assistant-js-websocket"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers"; +import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { customElement, property, query } from "lit/decorators"; -import { fireEvent } from "../../common/dom/fire_event"; import { formatAttributeName } from "../../data/entity_attributes"; import { PolymerChangedEvent } from "../../polymer-types"; import { HomeAssistant } from "../../types"; -import "../ha-icon-button"; -import "../ha-svg-icon"; -import "./state-badge"; +import "../ha-combo-box"; +import type { HaComboBox } from "../ha-combo-box"; export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; -// eslint-disable-next-line lit/prefer-static-styles -const rowRenderer: ComboBoxLitRenderer = (item) => html` - - ${formatAttributeName(item)}`; - @customElement("ha-entity-attribute-picker") class HaEntityAttributePicker extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -68,7 +28,7 @@ class HaEntityAttributePicker extends LitElement { @property({ type: Boolean }) private _opened = false; - @query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement; + @query("ha-combo-box", true) private _comboBox!: HaComboBox; protected shouldUpdate(changedProps: PropertyValues) { return !(!changedProps.has("_opened") && this._opened); @@ -78,7 +38,10 @@ class HaEntityAttributePicker extends LitElement { if (changedProps.has("_opened") && this._opened) { const state = this.entityId ? this.hass.states[this.entityId] : undefined; (this._comboBox as any).items = state - ? Object.keys(state.attributes) + ? Object.keys(state.attributes).map((key) => ({ + value: key, + label: formatAttributeName(key), + })) : []; } } @@ -89,100 +52,31 @@ class HaEntityAttributePicker extends LitElement { } return html` - - -
- ${this.value - ? html` - - ` - : ""} - - -
-
-
+ `; } - private _clearValue(ev: Event) { - ev.stopPropagation(); - this._setValue(""); - } - - private get _value() { - return this.value; - } - private _openedChanged(ev: PolymerChangedEvent) { this._opened = ev.detail.value; } private _valueChanged(ev: PolymerChangedEvent) { - const newValue = ev.detail.value; - if (newValue !== this._value) { - this._setValue(newValue); - } - } - - private _setValue(value: string) { - this.value = value; - setTimeout(() => { - fireEvent(this, "value-changed", { value }); - fireEvent(this, "change"); - }, 0); - } - - static get styles(): CSSResultGroup { - return css` - .suffix { - display: flex; - } - ha-icon-button { - --mdc-icon-button-size: 24px; - padding: 0px 2px; - color: var(--secondary-text-color); - } - [hidden] { - display: none; - } - `; + this.value = ev.detail.value; } } diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts index 016684e6c2..d24f6fbac4 100644 --- a/src/components/entity/ha-entity-picker.ts +++ b/src/components/entity/ha-entity-picker.ts @@ -1,25 +1,16 @@ -import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-item/paper-icon-item"; -import "@polymer/paper-item/paper-item-body"; -import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light"; +import "@material/mwc-list/mwc-list-item"; import { HassEntity } from "home-assistant-js-websocket"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers"; -import { customElement, property, query } from "lit/decorators"; +import { html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { ComboBoxLitRenderer } from "lit-vaadin-helpers"; +import { customElement, property, query, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../common/dom/fire_event"; import { computeDomain } from "../../common/entity/compute_domain"; import { computeStateName } from "../../common/entity/compute_state_name"; import { PolymerChangedEvent } from "../../polymer-types"; import { HomeAssistant } from "../../types"; +import "../ha-combo-box"; +import type { HaComboBox } from "../ha-combo-box"; import "../ha-icon-button"; import "../ha-svg-icon"; import "./state-badge"; @@ -27,35 +18,15 @@ import "./state-badge"; export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; // eslint-disable-next-line lit/prefer-static-styles -const rowRenderer: ComboBoxLitRenderer = (item) => html` - - - - - ${computeStateName(item)} - ${item.entity_id} - - `; - +const rowRenderer: ComboBoxLitRenderer = + (item) => + html` + ${item.state + ? html`` + : ""} + ${item.friendly_name} + ${item.entity_id} + `; @customElement("ha-entity-picker") export class HaEntityPicker extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -107,19 +78,19 @@ export class HaEntityPicker extends LitElement { @property({ type: Boolean }) public hideClearIcon = false; - @property({ type: Boolean }) private _opened = false; + @state() private _opened = false; - @query("vaadin-combo-box-light", true) private comboBox!: HTMLElement; + @query("ha-combo-box", true) public comboBox!: HaComboBox; public open() { this.updateComplete.then(() => { - (this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open(); + this.comboBox?.open(); }); } public focus() { this.updateComplete.then(() => { - this.shadowRoot?.querySelector("paper-input")?.focus(); + this.comboBox?.focus(); }); } @@ -144,6 +115,27 @@ export class HaEntityPicker extends LitElement { } let entityIds = Object.keys(hass.states); + if (!entityIds.length) { + return [ + { + entity_id: "", + state: "", + last_changed: "", + last_updated: "", + context: { id: "", user_id: null }, + friendly_name: this.hass!.localize( + "ui.components.entity.entity-picker.no_entities" + ), + attributes: { + friendly_name: this.hass!.localize( + "ui.components.entity.entity-picker.no_entities" + ), + icon: "mdi:magnify", + }, + }, + ]; + } + if (includeDomains) { entityIds = entityIds.filter((eid) => includeDomains.includes(computeDomain(eid)) @@ -156,7 +148,10 @@ export class HaEntityPicker extends LitElement { ); } - states = entityIds.sort().map((key) => hass!.states[key]); + states = entityIds.sort().map((key) => ({ + ...hass!.states[key], + friendly_name: computeStateName(hass!.states[key]) || key, + })); if (includeDeviceClasses) { states = states.filter( @@ -196,6 +191,9 @@ export class HaEntityPicker extends LitElement { last_changed: "", last_updated: "", context: { id: "", user_id: null }, + friendly_name: this.hass!.localize( + "ui.components.entity.entity-picker.no_match" + ), attributes: { friendly_name: this.hass!.localize( "ui.components.entity.entity-picker.no_match" @@ -241,64 +239,25 @@ export class HaEntityPicker extends LitElement { protected render(): TemplateResult { return html` - - -
- ${this.value && !this.hideClearIcon - ? html` - - ` - : ""} - - -
-
-
+ `; } - private _clearValue(ev: Event) { - ev.stopPropagation(); - this._setValue(""); - } - private get _value() { return this.value || ""; } @@ -308,6 +267,7 @@ export class HaEntityPicker extends LitElement { } private _valueChanged(ev: PolymerChangedEvent) { + ev.stopPropagation(); const newValue = ev.detail.value; if (newValue !== this._value) { this._setValue(newValue); @@ -317,9 +277,9 @@ export class HaEntityPicker extends LitElement { private _filterChanged(ev: CustomEvent): void { const filterString = ev.detail.value.toLowerCase(); (this.comboBox as any).filteredItems = this._states.filter( - (state) => - state.entity_id.toLowerCase().includes(filterString) || - computeStateName(state).toLowerCase().includes(filterString) + (entityState) => + entityState.entity_id.toLowerCase().includes(filterString) || + computeStateName(entityState).toLowerCase().includes(filterString) ); } @@ -330,22 +290,6 @@ export class HaEntityPicker extends LitElement { fireEvent(this, "change"); }, 0); } - - static get styles(): CSSResultGroup { - return css` - .suffix { - display: flex; - } - ha-icon-button { - --mdc-icon-button-size: 24px; - padding: 0px 2px; - color: var(--secondary-text-color); - } - [hidden] { - display: none; - } - `; - } } declare global { diff --git a/src/components/entity/ha-statistic-picker.ts b/src/components/entity/ha-statistic-picker.ts index 080a78a252..76965ecee5 100644 --- a/src/components/entity/ha-statistic-picker.ts +++ b/src/components/entity/ha-statistic-picker.ts @@ -1,17 +1,5 @@ -import { mdiCheck } from "@mdi/js"; -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-item/paper-icon-item"; -import "@polymer/paper-item/paper-item-body"; -import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light"; import { HassEntity } from "home-assistant-js-websocket"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; +import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { ComboBoxLitRenderer } from "lit-vaadin-helpers"; import { customElement, property, query, state } from "lit/decorators"; import memoizeOne from "memoize-one"; @@ -76,54 +64,24 @@ export class HaStatisticPicker extends LitElement { id: string; name: string; state?: HassEntity; - // eslint-disable-next-line lit/prefer-static-styles - }> = (item) => html` - - - ${item.state - ? html`` - : ""} - - ${item.name} - ${item.id === "" || item.id === "__missing" - ? html`${this.hass.localize( - "ui.components.statistic-picker.learn_more" - )}` - : item.id} - - `; + }> = (item) => html` + ${item.state + ? html`` + : ""} + ${item.name} + ${item.id === "" || item.id === "__missing" + ? html`${this.hass.localize( + "ui.components.statistic-picker.learn_more" + )}` + : item.id} + `; private _getStatistics = memoizeOne( ( @@ -293,19 +251,6 @@ export class HaStatisticPicker extends LitElement { fireEvent(this, "change"); }, 0); } - - static get styles(): CSSResultGroup { - return css` - paper-input > ha-icon-button { - --mdc-icon-button-size: 24px; - padding: 2px; - color: var(--secondary-text-color); - } - [hidden] { - display: none; - } - `; - } } declare global { diff --git a/src/components/entity/ha-statistics-picker.ts b/src/components/entity/ha-statistics-picker.ts index 46e10060d0..aa25e45803 100644 --- a/src/components/entity/ha-statistics-picker.ts +++ b/src/components/entity/ha-statistics-picker.ts @@ -1,4 +1,4 @@ -import { html, LitElement, TemplateResult } from "lit"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; import type { PolymerChangedEvent } from "../../polymer-types"; @@ -103,6 +103,20 @@ class HaStatisticsPicker extends LitElement { this._updateStatistics([...currentEntities, toAdd]); } + + static get styles(): CSSResultGroup { + return css` + :host { + width: 200px; + display: block; + } + ha-statistic-picker { + display: block; + width: 100%; + margin-top: 8px; + } + `; + } } declare global { diff --git a/src/components/ha-addon-picker.ts b/src/components/ha-addon-picker.ts index c40d874c13..a28f188de9 100644 --- a/src/components/ha-addon-picker.ts +++ b/src/components/ha-addon-picker.ts @@ -1,4 +1,3 @@ -import { mdiCheck } from "@mdi/js"; import { html, LitElement, TemplateResult } from "lit"; import { ComboBoxLitRenderer } from "lit-vaadin-helpers"; import { customElement, property, query, state } from "lit/decorators"; @@ -12,39 +11,12 @@ import { PolymerChangedEvent } from "../polymer-types"; import { HomeAssistant } from "../types"; import { HaComboBox } from "./ha-combo-box"; -// eslint-disable-next-line lit/prefer-static-styles -const rowRenderer: ComboBoxLitRenderer = (item) => html` - - - - ${item.name} - ${item.slug} - - `; +const rowRenderer: ComboBoxLitRenderer = ( + item +) => html` + ${item.name} + ${item.slug} +`; @customElement("ha-addon-picker") class HaAddonPicker extends LitElement { diff --git a/src/components/ha-area-picker.ts b/src/components/ha-area-picker.ts index ed658188df..a671893737 100644 --- a/src/components/ha-area-picker.ts +++ b/src/components/ha-area-picker.ts @@ -1,19 +1,6 @@ -import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-item/paper-item-body"; -import "@polymer/paper-listbox/paper-listbox"; -import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers"; +import { html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { ComboBoxLitRenderer } from "lit-vaadin-helpers"; import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; @@ -41,38 +28,18 @@ import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { PolymerChangedEvent } from "../polymer-types"; import { HomeAssistant } from "../types"; import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; +import type { HaComboBox } from "./ha-combo-box"; +import "./ha-combo-box"; import "./ha-icon-button"; import "./ha-svg-icon"; const rowRenderer: ComboBoxLitRenderer = ( item - // eslint-disable-next-line lit/prefer-static-styles -) => html` - - - ${item.name} - `; +) => html` + ${item.name} +`; @customElement("ha-area-picker") export class HaAreaPicker extends SubscribeMixin(LitElement) { @@ -125,7 +92,9 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { @state() private _opened?: boolean; - @query("vaadin-combo-box-light", true) public comboBox!: HTMLElement; + @query("ha-combo-box", true) public comboBox!: HaComboBox; + + private _filter?: string; private _init = false; @@ -145,13 +114,13 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { public open() { this.updateComplete.then(() => { - (this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open(); + this.comboBox?.open(); }); } public focus() { this.updateComplete.then(() => { - this.shadowRoot?.querySelector("paper-input")?.focus(); + this.comboBox?.focus(); }); } @@ -170,7 +139,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { if (!areas.length) { return [ { - area_id: "", + area_id: "no_areas", name: this.hass.localize("ui.components.area-picker.no_areas"), picture: null, }, @@ -294,7 +263,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { if (!outputAreas.length) { outputAreas = [ { - area_id: "", + area_id: "no_areas", name: this.hass.localize("ui.components.area-picker.no_match"), picture: null, }, @@ -339,52 +308,25 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { return html``; } return html` - - - ${this.value - ? html` - - ` - : ""} - - - - + `; } @@ -392,9 +334,29 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { this._areas?.find((area) => area.area_id === areaId) ); - private _clearValue(ev: Event) { - ev.stopPropagation(); - this._setValue(""); + private _filterChanged(ev: CustomEvent): void { + this._filter = ev.detail.value; + if (!this._filter) { + this.comboBox.filteredItems = this.comboBox.items; + return; + } + // @ts-ignore + if (!this.noAdd && this.comboBox._comboBox.filteredItems?.length === 0) { + this.comboBox.filteredItems = [ + { + area_id: "add_new_suggestion", + name: this.hass.localize( + "ui.components.area-picker.add_new_sugestion", + { name: this._filter } + ), + picture: null, + }, + ]; + } else { + this.comboBox.filteredItems = this.comboBox.items?.filter((item) => + item.name.toLowerCase().includes(this._filter!.toLowerCase()) + ); + } } private get _value() { @@ -406,9 +368,14 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { } private _areaChanged(ev: PolymerChangedEvent) { - const newValue = ev.detail.value; + ev.stopPropagation(); + let newValue = ev.detail.value; - if (newValue !== "add_new") { + if (newValue === "no_areas") { + newValue = ""; + } + + if (!["add_new_suggestion", "add_new"].includes(newValue)) { if (newValue !== this._value) { this._setValue(newValue); } @@ -425,6 +392,8 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { inputLabel: this.hass.localize( "ui.components.area-picker.add_dialog.name" ), + defaultValue: + newValue === "add_new_suggestion" ? this._filter : undefined, confirm: async (name) => { if (!name) { return; @@ -445,6 +414,8 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { this.entityFilter, this.noAdd ); + await this.updateComplete; + await this.comboBox.updateComplete; this._setValue(area.area_id); } catch (err: any) { showAlertDialog(this, { @@ -465,19 +436,6 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { fireEvent(this, "change"); }, 0); } - - static get styles(): CSSResultGroup { - return css` - paper-input > ha-icon-button { - --mdc-icon-button-size: 24px; - padding: 2px; - color: var(--secondary-text-color); - } - [hidden] { - display: none; - } - `; - } } declare global { diff --git a/src/components/ha-base-time-input.ts b/src/components/ha-base-time-input.ts new file mode 100644 index 0000000000..64b12f18e2 --- /dev/null +++ b/src/components/ha-base-time-input.ts @@ -0,0 +1,313 @@ +import { LitElement, html, TemplateResult, css } from "lit"; +import { customElement, property } from "lit/decorators"; +import "./ha-select"; +import "@material/mwc-list/mwc-list-item"; +import "./ha-textfield"; +import { fireEvent } from "../common/dom/fire_event"; +import { stopPropagation } from "../common/dom/stop_propagation"; + +export interface TimeChangedEvent { + hours: number; + minutes: number; + seconds: number; + milliseconds: number; + amPm?: "AM" | "PM"; +} + +@customElement("ha-base-time-input") +export class HaBaseTimeInput extends LitElement { + /** + * Label for the input + */ + @property() label?: string; + + /** + * auto validate time inputs + */ + @property({ type: Boolean }) autoValidate = false; + + /** + * determines if inputs are required + */ + @property({ type: Boolean }) public required?: boolean; + + /** + * 12 or 24 hr format + */ + @property({ type: Number }) format: 12 | 24 = 12; + + /** + * disables the inputs + */ + @property({ type: Boolean }) disabled = false; + + /** + * hour + */ + @property({ type: Number }) hours = 0; + + /** + * minute + */ + @property({ type: Number }) minutes = 0; + + /** + * second + */ + @property({ type: Number }) seconds = 0; + + /** + * milli second + */ + @property({ type: Number }) milliseconds = 0; + + /** + * Label for the hour input + */ + @property() hourLabel = ""; + + /** + * Label for the min input + */ + @property() minLabel = ""; + + /** + * Label for the sec input + */ + @property() secLabel = ""; + + /** + * Label for the milli sec input + */ + @property() millisecLabel = ""; + + /** + * show the sec field + */ + @property({ type: Boolean }) enableSecond = false; + + /** + * show the milli sec field + */ + @property({ type: Boolean }) enableMillisecond = false; + + /** + * limit hours input + */ + @property({ type: Boolean }) noHoursLimit = false; + + /** + * AM or PM + */ + @property() amPm: "AM" | "PM" = "AM"; + + /** + * Formatted time string + */ + @property() value?: string; + + protected render(): TemplateResult { + return html` + ${this.label ? html`` : ""} +
+ + + + + ${this.enableSecond + ? html` + ` + : ""} + ${this.enableMillisecond + ? html` + ` + : ""} + ${this.format === 24 + ? "" + : html` + AM + PM + `} +
+ `; + } + + private _valueChanged(ev) { + this[ev.target.name] = + ev.target.name === "amPm" ? ev.target.value : Number(ev.target.value); + const value: TimeChangedEvent = { + hours: this.hours, + minutes: this.minutes, + seconds: this.seconds, + milliseconds: this.milliseconds, + }; + if (this.format === 12) { + value.amPm = this.amPm; + } + fireEvent(this, "value-changed", { + value, + }); + } + + private _onFocus(ev) { + ev.target.select(); + } + + /** + * Format time fragments + */ + private _formatValue(value: number, padding = 2) { + return value.toString().padStart(padding, "0"); + } + + /** + * 24 hour format has a max hr of 23 + */ + private get _hourMax() { + if (this.noHoursLimit) { + return null; + } + if (this.format === 12) { + return 12; + } + return 23; + } + + static styles = css` + :host { + display: block; + } + .time-input-wrap { + display: flex; + border-radius: var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0 0; + overflow: hidden; + position: relative; + } + ha-textfield { + width: 40px; + text-align: center; + --mdc-shape-small: 0; + --text-field-appearance: none; + --text-field-padding: 0 4px; + --text-field-suffix-padding-left: 2px; + --text-field-suffix-padding-right: 0; + --text-field-text-align: center; + } + ha-textfield.hasSuffix { + --text-field-padding: 0 0 0 4px; + } + ha-textfield:first-child { + --text-field-border-top-left-radius: var(--mdc-shape-medium); + } + ha-textfield:last-child { + --text-field-border-top-right-radius: var(--mdc-shape-medium); + } + ha-select { + --mdc-shape-small: 0; + width: 85px; + } + label { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + font-family: var( + --mdc-typography-body2-font-family, + var(--mdc-typography-font-family, Roboto, sans-serif) + ); + font-size: var(--mdc-typography-body2-font-size, 0.875rem); + line-height: var(--mdc-typography-body2-line-height, 1.25rem); + font-weight: var(--mdc-typography-body2-font-weight, 400); + letter-spacing: var( + --mdc-typography-body2-letter-spacing, + 0.0178571429em + ); + text-decoration: var(--mdc-typography-body2-text-decoration, inherit); + text-transform: var(--mdc-typography-body2-text-transform, inherit); + color: var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, 0.87)); + padding-left: 4px; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-base-time-input": HaBaseTimeInput; + } +} diff --git a/src/components/ha-blueprint-picker.ts b/src/components/ha-blueprint-picker.ts index ba17709a76..e45e31b6da 100644 --- a/src/components/ha-blueprint-picker.ts +++ b/src/components/ha-blueprint-picker.ts @@ -1,10 +1,10 @@ -import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; +import "@material/mwc-list/mwc-list-item"; +import "./ha-select"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../common/dom/fire_event"; +import { stopPropagation } from "../common/dom/stop_propagation"; import { stringCompare } from "../common/string/compare"; import { Blueprint, Blueprints, fetchBlueprints } from "../data/blueprint"; import { HomeAssistant } from "../types"; @@ -24,7 +24,11 @@ class HaBluePrintPicker extends LitElement { @property({ type: Boolean }) public disabled = false; public open() { - this.shadowRoot!.querySelector("paper-dropdown-menu-light")!.open(); + const select = this.shadowRoot?.querySelector("ha-select"); + if (select) { + // @ts-expect-error + select.menuOpen = true; + } } private _processedBlueprints = memoizeOne((blueprints?: Blueprints) => { @@ -45,32 +49,29 @@ class HaBluePrintPicker extends LitElement { return html``; } return html` - - - - ${this.hass.localize( - "ui.components.blueprint-picker.select_blueprint" - )} - - ${this._processedBlueprints(this.blueprints).map( - (blueprint) => html` - - ${blueprint.name} - - ` + + ${this.hass.localize( + "ui.components.blueprint-picker.select_blueprint" )} - - + + ${this._processedBlueprints(this.blueprints).map( + (blueprint) => html` + + ${blueprint.name} + + ` + )} + `; } @@ -84,10 +85,10 @@ class HaBluePrintPicker extends LitElement { } private _blueprintChanged(ev) { - const newValue = ev.detail.item.dataset.blueprintPath; + const newValue = ev.target.value; if (newValue !== this.value) { - this.value = ev.detail.value; + this.value = newValue; setTimeout(() => { fireEvent(this, "value-changed", { value: newValue }); fireEvent(this, "change"); @@ -100,15 +101,11 @@ class HaBluePrintPicker extends LitElement { :host { display: inline-block; } - paper-dropdown-menu-light { + ha-select { width: 100%; min-width: 200px; display: block; } - paper-item { - cursor: pointer; - min-width: 200px; - } `; } } diff --git a/src/components/ha-button-menu.ts b/src/components/ha-button-menu.ts index 4154480615..bf0d1d3487 100644 --- a/src/components/ha-button-menu.ts +++ b/src/components/ha-button-menu.ts @@ -1,5 +1,5 @@ import "@material/mwc-menu"; -import type { Corner, Menu } from "@material/mwc-menu"; +import type { Corner, Menu, MenuCorner } from "@material/mwc-menu"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, query } from "lit/decorators"; @@ -7,6 +7,12 @@ import { customElement, property, query } from "lit/decorators"; export class HaButtonMenu extends LitElement { @property() public corner: Corner = "TOP_START"; + @property() public menuCorner: MenuCorner = "START"; + + @property({ type: Number }) public x?: number; + + @property({ type: Number }) public y?: number; + @property({ type: Boolean }) public multi = false; @property({ type: Boolean }) public activatable = false; @@ -32,9 +38,12 @@ export class HaButtonMenu extends LitElement {
diff --git a/src/components/ha-button-related-filter-menu.ts b/src/components/ha-button-related-filter-menu.ts index fb65469e12..7dcd892eb5 100644 --- a/src/components/ha-button-related-filter-menu.ts +++ b/src/components/ha-button-related-filter-menu.ts @@ -4,6 +4,7 @@ import { mdiFilterVariant } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; +import { stopPropagation } from "../common/dom/stop_propagation"; import { computeStateName } from "../common/entity/compute_state_name"; import { computeDeviceName } from "../data/device_registry"; import { findRelated, RelatedResult } from "../data/search"; @@ -65,6 +66,7 @@ export class HaRelatedFilterButtonMenu extends LitElement { .fullwidth=${this.narrow} .corner=${this.corner} @closed=${this._onClosed} + @input=${stopPropagation} > `; @@ -103,11 +108,17 @@ export class HaRelatedFilterButtonMenu extends LitElement { this._open = true; } - private _onClosed(): void { + private _onClosed(ev): void { + ev.stopPropagation(); this._open = false; } + private _preventDefault(ev) { + ev.preventDefault(); + } + private async _entityPicked(ev: CustomEvent) { + ev.stopPropagation(); const entityId = ev.detail.value; if (!entityId) { fireEvent(this, "related-changed", { value: undefined }); @@ -127,6 +138,7 @@ export class HaRelatedFilterButtonMenu extends LitElement { } private async _devicePicked(ev: CustomEvent) { + ev.stopPropagation(); const deviceId = ev.detail.value; if (!deviceId) { fireEvent(this, "related-changed", { value: undefined }); @@ -150,6 +162,7 @@ export class HaRelatedFilterButtonMenu extends LitElement { } private async _areaPicked(ev: CustomEvent) { + ev.stopPropagation(); const areaId = ev.detail.value; if (!areaId) { fireEvent(this, "related-changed", { value: undefined }); @@ -173,9 +186,7 @@ export class HaRelatedFilterButtonMenu extends LitElement { :host { display: inline-block; position: relative; - } - :host([narrow]) { - position: static; + --mdc-menu-min-width: 250px; } ha-area-picker, ha-device-picker, @@ -185,8 +196,15 @@ export class HaRelatedFilterButtonMenu extends LitElement { padding: 4px 16px; box-sizing: border-box; } + ha-area-picker { + padding-top: 16px; + } + ha-entity-picker { + padding-bottom: 16px; + } :host([narrow]) ha-area-picker, - :host([narrow]) ha-device-picker { + :host([narrow]) ha-device-picker, + :host([narrow]) ha-entity-picker { width: 100%; } `; diff --git a/src/components/ha-check-list-item.ts b/src/components/ha-check-list-item.ts new file mode 100644 index 0000000000..63f5827f67 --- /dev/null +++ b/src/components/ha-check-list-item.ts @@ -0,0 +1,24 @@ +import { css } from "lit"; +import { CheckListItemBase } from "@material/mwc-list/mwc-check-list-item-base"; +import { styles as controlStyles } from "@material/mwc-list/mwc-control-list-item.css"; +import { styles } from "@material/mwc-list/mwc-list-item.css"; +import { customElement } from "lit/decorators"; + +@customElement("ha-check-list-item") +export class HaCheckListItem extends CheckListItemBase { + static override styles = [ + styles, + controlStyles, + css` + :host { + --mdc-theme-secondary: var(--primary-color); + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-check-list-item": HaCheckListItem; + } +} diff --git a/src/components/ha-checkbox.ts b/src/components/ha-checkbox.ts index b3f5a1a2f4..0e33ee2a18 100644 --- a/src/components/ha-checkbox.ts +++ b/src/components/ha-checkbox.ts @@ -1,12 +1,18 @@ -import { Checkbox } from "@material/mwc-checkbox"; +import { CheckboxBase } from "@material/mwc-checkbox/mwc-checkbox-base"; +import { styles } from "@material/mwc-checkbox/mwc-checkbox.css"; +import { css } from "lit"; import { customElement } from "lit/decorators"; @customElement("ha-checkbox") -export class HaCheckbox extends Checkbox { - public firstUpdated() { - super.firstUpdated(); - this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)"); - } +export class HaCheckbox extends CheckboxBase { + static override styles = [ + styles, + css` + :host { + --mdc-theme-secondary: var(--primary-color); + } + `, + ]; } declare global { diff --git a/src/components/ha-code-editor.ts b/src/components/ha-code-editor.ts index 7ea60707c3..2ec8a2e627 100644 --- a/src/components/ha-code-editor.ts +++ b/src/components/ha-code-editor.ts @@ -1,8 +1,16 @@ +import type { + Completion, + CompletionContext, + CompletionResult, +} from "@codemirror/autocomplete"; import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view"; +import { HassEntities } from "home-assistant-js-websocket"; import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit"; import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { fireEvent } from "../common/dom/fire_event"; import { loadCodeMirror } from "../resources/codemirror.ondemand"; +import { HomeAssistant } from "../types"; declare global { interface HASSDomEvents { @@ -24,10 +32,15 @@ export class HaCodeEditor extends ReactiveElement { @property() public mode = "yaml"; + public hass?: HomeAssistant; + @property({ type: Boolean }) public autofocus = false; @property({ type: Boolean }) public readOnly = false; + @property({ type: Boolean, attribute: "autocomplete-entities" }) + public autocompleteEntities = false; + @property() public error = false; @state() private _value = ""; @@ -110,43 +123,92 @@ export class HaCodeEditor extends ReactiveElement { private async _load(): Promise { this._loadedCodeMirror = await loadCodeMirror(); + const extensions = [ + this._loadedCodeMirror.lineNumbers(), + this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true), + this._loadedCodeMirror.history(), + this._loadedCodeMirror.highlightSelectionMatches(), + this._loadedCodeMirror.highlightActiveLine(), + this._loadedCodeMirror.drawSelection(), + this._loadedCodeMirror.rectangularSelection(), + this._loadedCodeMirror.keymap.of([ + ...this._loadedCodeMirror.defaultKeymap, + ...this._loadedCodeMirror.searchKeymap, + ...this._loadedCodeMirror.historyKeymap, + ...this._loadedCodeMirror.tabKeyBindings, + saveKeyBinding, + ] as KeyBinding[]), + this._loadedCodeMirror.langCompartment.of(this._mode), + this._loadedCodeMirror.theme, + this._loadedCodeMirror.Prec.fallback( + this._loadedCodeMirror.highlightStyle + ), + this._loadedCodeMirror.readonlyCompartment.of( + this._loadedCodeMirror.EditorView.editable.of(!this.readOnly) + ), + this._loadedCodeMirror.EditorView.updateListener.of((update) => + this._onUpdate(update) + ), + ]; + + if (!this.readOnly && this.autocompleteEntities && this.hass) { + extensions.push( + this._loadedCodeMirror.autocompletion({ + override: [this._entityCompletions.bind(this)], + maxRenderedOptions: 10, + }) + ); + } this.codemirror = new this._loadedCodeMirror.EditorView({ state: this._loadedCodeMirror.EditorState.create({ doc: this._value, - extensions: [ - this._loadedCodeMirror.lineNumbers(), - this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true), - this._loadedCodeMirror.history(), - this._loadedCodeMirror.highlightSelectionMatches(), - this._loadedCodeMirror.highlightActiveLine(), - this._loadedCodeMirror.drawSelection(), - this._loadedCodeMirror.rectangularSelection(), - this._loadedCodeMirror.keymap.of([ - ...this._loadedCodeMirror.defaultKeymap, - ...this._loadedCodeMirror.searchKeymap, - ...this._loadedCodeMirror.historyKeymap, - ...this._loadedCodeMirror.tabKeyBindings, - saveKeyBinding, - ] as KeyBinding[]), - this._loadedCodeMirror.langCompartment.of(this._mode), - this._loadedCodeMirror.theme, - this._loadedCodeMirror.Prec.fallback( - this._loadedCodeMirror.highlightStyle - ), - this._loadedCodeMirror.readonlyCompartment.of( - this._loadedCodeMirror.EditorView.editable.of(!this.readOnly) - ), - this._loadedCodeMirror.EditorView.updateListener.of((update) => - this._onUpdate(update) - ), - ], + extensions, }), root: this.shadowRoot!, parent: this.shadowRoot!, }); } + private _getStates = memoizeOne((states: HassEntities): Completion[] => { + if (!states) { + return []; + } + const options = Object.keys(states).map((key) => ({ + type: "variable", + label: key, + detail: states[key].attributes.friendly_name, + info: `State: ${states[key].state}`, + })); + + return options; + }); + + private _entityCompletions( + context: CompletionContext + ): CompletionResult | null | Promise { + const entityWord = context.matchBefore(/[a-z_]{3,}\./); + + if ( + !entityWord || + (entityWord.from === entityWord.to && !context.explicit) + ) { + return null; + } + + const states = this._getStates(this.hass!.states); + + if (!states || !states.length) { + return null; + } + + return { + from: Number(entityWord.from), + options: states, + span: /^\w*.\w*$/, + }; + } + private _blockKeyboardShortcuts() { this.addEventListener("keydown", (ev) => ev.stopPropagation()); } @@ -163,10 +225,9 @@ export class HaCodeEditor extends ReactiveElement { fireEvent(this, "value-changed", { value: this._value }); } - // Only Lit 2.0 will use this static get styles(): CSSResultGroup { return css` - :host(.error-state) div.cm-wrap .cm-gutters { + :host(.error-state) .cm-gutters { border-color: var(--error-state-color, red); } `; diff --git a/src/components/ha-combo-box.ts b/src/components/ha-combo-box.ts index afcd9c8911..f8f1bd5cde 100644 --- a/src/components/ha-combo-box.ts +++ b/src/components/ha-combo-box.ts @@ -1,37 +1,78 @@ +import "@material/mwc-list/mwc-list-item"; import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-item/paper-item-body"; -import "@polymer/paper-listbox/paper-listbox"; -import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light"; +import "@vaadin/combo-box/theme/material/vaadin-combo-box-light"; +import type { ComboBoxLight } from "@vaadin/combo-box/vaadin-combo-box-light"; +import { registerStyles } from "@vaadin/vaadin-themable-mixin/register-styles"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers"; -import { customElement, property, query, state } from "lit/decorators"; +import { customElement, property, query } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; import { PolymerChangedEvent } from "../polymer-types"; import { HomeAssistant } from "../types"; import "./ha-icon-button"; +import "./ha-textfield"; -// eslint-disable-next-line lit/prefer-static-styles -const defaultRowRenderer: ComboBoxLitRenderer = (item) => html` - ${item}`; + :host([focused]:not([disabled])) { + background-color: rgba(var(--rgb-primary-text-color, 0, 0, 0), 0.12); + } + :host([selected]:not([disabled])) { + background-color: transparent; + color: var(--mdc-theme-primary); + --mdc-ripple-color: var(--mdc-theme-primary); + --mdc-theme-text-primary-on-background: var(--mdc-theme-primary); + } + :host([selected]:not([disabled])):before { + background-color: var(--mdc-theme-primary); + opacity: 0.12; + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + :host([selected][focused]:not([disabled])):before { + opacity: 0.24; + } + :host(:hover:not([disabled])) { + background-color: transparent; + } + [part="content"] { + width: 100%; + } + [part="checkmark"] { + display: none; + } + ` +); @customElement("ha-combo-box") export class HaComboBox extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public hass?: HomeAssistant; @property() public label?: string; @property() public value?: string; - @property() public items?: []; + @property() public placeholder?: string; - @property() public filteredItems?: []; + @property() public validationMessage?: string; + + @property({ attribute: "error-message" }) public errorMessage?: string; + + @property({ type: Boolean }) public invalid?: boolean; + + @property({ type: Boolean }) public icon?: boolean; + + @property() public items?: any[]; + + @property() public filteredItems?: any[]; @property({ attribute: "allow-custom-value", type: Boolean }) public allowCustomValue?: boolean; @@ -46,24 +87,25 @@ export class HaComboBox extends LitElement { @property({ type: Boolean }) public disabled?: boolean; - @state() private _opened?: boolean; + @property({ type: Boolean, reflect: true, attribute: "opened" }) + private _opened?: boolean; - @query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement; + @query("vaadin-combo-box-light", true) private _comboBox!: ComboBoxLight; public open() { this.updateComplete.then(() => { - (this._comboBox as any)?.open(); + this._comboBox?.open(); }); } public focus() { this.updateComplete.then(() => { - this.shadowRoot?.querySelector("paper-input")?.focus(); + this._comboBox?.inputElement?.focus(); }); } public get selectedItem() { - return (this._comboBox as any).selectedItem; + return this._comboBox.selectedItem; } protected render(): TemplateResult { @@ -72,55 +114,78 @@ export class HaComboBox extends LitElement { .itemValuePath=${this.itemValuePath} .itemIdPath=${this.itemIdPath} .itemLabelPath=${this.itemLabelPath} - .value=${this.value} + .value=${this.value || ""} .items=${this.items} .filteredItems=${this.filteredItems} .allowCustomValue=${this.allowCustomValue} .disabled=${this.disabled} - ${comboBoxRenderer(this.renderer || defaultRowRenderer)} + ${comboBoxRenderer(this.renderer || this._defaultRowRenderer)} @opened-changed=${this._openedChanged} @filter-changed=${this._filterChanged} @value-changed=${this._valueChanged} + attr-for-value="value" > - `} + .icon=${this.icon} + .invalid=${this.invalid} > - ${this.value - ? html` - - ` - : ""} - - - + + + ${this.value + ? html`` + : ""} + `; } + private _defaultRowRenderer: ComboBoxLitRenderer< + string | Record + > = (item) => + html` + ${this.itemLabelPath ? item[this.itemLabelPath] : item} + `; + private _clearValue(ev: Event) { ev.stopPropagation(); fireEvent(this, "value-changed", { value: undefined }); } + private _toggleOpen(ev: Event) { + if (this._opened) { + this._comboBox?.close(); + ev.stopPropagation(); + } else { + this._comboBox?.inputElement.focus(); + } + } + private _openedChanged(ev: PolymerChangedEvent) { - this._opened = ev.detail.value; + // delay this so we can handle click event before setting _opened + setTimeout(() => { + this._opened = ev.detail.value; + }, 0); // @ts-ignore fireEvent(this, ev.type, ev.detail); } @@ -141,11 +206,38 @@ export class HaComboBox extends LitElement { static get styles(): CSSResultGroup { return css` - paper-input > ha-icon-button { + :host { + display: block; + width: 100%; + } + vaadin-combo-box-light { + position: relative; + } + ha-textfield { + width: 100%; + } + ha-textfield > ha-icon-button { --mdc-icon-button-size: 24px; padding: 2px; color: var(--secondary-text-color); } + ha-svg-icon { + color: var(--input-dropdown-icon-color); + position: absolute; + cursor: pointer; + } + .toggle-button { + right: 12px; + top: -10px; + } + :host([opened]) .toggle-button { + color: var(--primary-color); + } + .clear-button { + --mdc-icon-size: 20px; + top: -7px; + right: 36px; + } `; } } diff --git a/src/components/ha-date-input.ts b/src/components/ha-date-input.ts index 852e4aa4c7..42308ee6c7 100644 --- a/src/components/ha-date-input.ts +++ b/src/components/ha-date-input.ts @@ -1,140 +1,78 @@ import { mdiCalendar } from "@mdi/js"; -import "@polymer/paper-input/paper-input"; -import "@vaadin/vaadin-date-picker/theme/material/vaadin-date-picker-light"; -import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; -import { customElement, property, query } from "lit/decorators"; +import { css, CSSResultGroup, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { formatDateNumeric } from "../common/datetime/format_date"; import { fireEvent } from "../common/dom/fire_event"; +import { HomeAssistant } from "../types"; import "./ha-svg-icon"; +import "./ha-textfield"; -const i18n = { - monthNames: [ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", - ], - weekdays: [ - "Sunday", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - ], - weekdaysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], - firstDayOfWeek: 0, - week: "Week", - calendar: "Calendar", - clear: "Clear", - today: "Today", - cancel: "Cancel", - formatTitle: (monthName, fullYear) => monthName + " " + fullYear, - formatDate: (d: { day: number; month: number; year: number }) => - [ - ("0000" + String(d.year)).slice(-4), - ("0" + String(d.month + 1)).slice(-2), - ("0" + String(d.day)).slice(-2), - ].join("-"), - parseDate: (text: string) => { - const parts = text.split("-"); - const today = new Date(); - let date; - let month = today.getMonth(); - let year = today.getFullYear(); - if (parts.length === 3) { - year = parseInt(parts[0]); - if (parts[0].length < 3 && year >= 0) { - year += year < 50 ? 2000 : 1900; - } - month = parseInt(parts[1]) - 1; - date = parseInt(parts[2]); - } else if (parts.length === 2) { - month = parseInt(parts[0]) - 1; - date = parseInt(parts[1]); - } else if (parts.length === 1) { - date = parseInt(parts[0]); - } +const loadDatePickerDialog = () => import("./ha-dialog-date-picker"); - if (date !== undefined) { - return { day: date, month, year }; - } - return undefined; - }, +export interface datePickerDialogParams { + value?: string; + min?: string; + max?: string; + locale?: string; + onChange: (value: string) => void; +} + +const showDatePickerDialog = ( + element: HTMLElement, + dialogParams: datePickerDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "ha-dialog-date-picker", + dialogImport: loadDatePickerDialog, + dialogParams, + }); }; @customElement("ha-date-input") export class HaDateInput extends LitElement { + @property({ attribute: false }) public locale!: HomeAssistant["locale"]; + @property() public value?: string; @property({ type: Boolean }) public disabled = false; @property() public label?: string; - @query("vaadin-date-picker-light", true) private _datePicker; - - private _inited = false; - - updated(changedProps: PropertyValues) { - if (changedProps.has("value")) { - this._datePicker.value = this.value; - this._inited = true; - } - } - render() { - return html` - - - - `; + + `; } - private _valueChanged(ev: CustomEvent) { - if ( - !this.value || - (this._inited && !this._compareStringDates(ev.detail.value, this.value)) - ) { - this.value = ev.detail.value; + private _openDialog() { + if (this.disabled) { + return; + } + showDatePickerDialog(this, { + min: "1970-01-01", + value: this.value, + onChange: (value) => this._valueChanged(value), + locale: this.locale.language, + }); + } + + private _valueChanged(value: string) { + if (this.value !== value) { + this.value = value; fireEvent(this, "change"); - fireEvent(this, "value-changed", { value: ev.detail.value }); + fireEvent(this, "value-changed", { value }); } } - private _compareStringDates(a: string, b: string): boolean { - const aParts = a.split("-"); - const bParts = b.split("-"); - let i = 0; - for (const aPart of aParts) { - if (Number(aPart) !== Number(bParts[i])) { - return false; - } - i++; - } - return true; - } - static get styles(): CSSResultGroup { return css` - paper-input { - width: 110px; - } ha-svg-icon { color: var(--secondary-text-color); } diff --git a/src/components/ha-date-range-picker.ts b/src/components/ha-date-range-picker.ts index 2940c3a883..3baef7c22e 100644 --- a/src/components/ha-date-range-picker.ts +++ b/src/components/ha-date-range-picker.ts @@ -3,7 +3,6 @@ import "@material/mwc-list/mwc-list"; import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import "@material/mwc-list/mwc-list-item"; import { mdiCalendar } from "@mdi/js"; -import "@polymer/paper-input/paper-input"; import { css, CSSResultGroup, @@ -19,6 +18,7 @@ import { computeRTLDirection } from "../common/util/compute_rtl"; import { HomeAssistant } from "../types"; import "./date-range-picker"; import "./ha-svg-icon"; +import "./ha-textfield"; export interface DateRangePickerRanges { [key: string]: [Date, Date]; @@ -61,7 +61,7 @@ export class HaDateRangePicker extends LitElement { >
- - + + >
${this.ranges ? html`
+ + today + + cancel + + ok + `; + } + + private _valueChanged(ev: CustomEvent) { + this._value = ev.detail.value; + } + + private _setToday() { + this._value = new Date().toISOString().split("T")[0]; + } + + private _setValue() { + this._params?.onChange(this._value!); + this.closeDialog(); + } + + static styles = [ + haStyleDialog, + css` + ha-dialog { + --dialog-content-padding: 0; + --justify-action-buttons: space-between; + } + app-datepicker { + --app-datepicker-accent-color: var(--primary-color); + --app-datepicker-bg-color: transparent; + --app-datepicker-color: var(--primary-text-color); + --app-datepicker-disabled-day-color: var(--disabled-text-color); + --app-datepicker-focused-day-color: var(--text-primary-color); + --app-datepicker-focused-year-bg-color: var(--primary-color); + --app-datepicker-selector-color: var(--secondary-text-color); + --app-datepicker-separator-color: var(--divider-color); + --app-datepicker-weekday-color: var(--secondary-text-color); + } + app-datepicker::part(calendar-day):focus { + outline: none; + } + @media all and (min-width: 450px) { + ha-dialog { + --mdc-dialog-min-width: 300px; + } + } + @media all and (max-width: 450px), all and (max-height: 500px) { + app-datepicker { + width: 100%; + } + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-dialog-date-picker": HaDialogDatePicker; + } +} diff --git a/src/components/ha-dialog.ts b/src/components/ha-dialog.ts index 34614bedc6..693bae6367 100644 --- a/src/components/ha-dialog.ts +++ b/src/components/ha-dialog.ts @@ -1,6 +1,7 @@ -import { Dialog } from "@material/mwc-dialog"; +import { DialogBase } from "@material/mwc-dialog/mwc-dialog-base"; +import { styles } from "@material/mwc-dialog/mwc-dialog.css"; import { mdiClose } from "@mdi/js"; -import { css, CSSResultGroup, html, TemplateResult } from "lit"; +import { css, html, TemplateResult } from "lit"; import { customElement } from "lit/decorators"; import { computeRTLDirection } from "../common/util/compute_rtl"; import type { HomeAssistant } from "../types"; @@ -21,8 +22,7 @@ export const createCloseHeading = ( `; @customElement("ha-dialog") -// @ts-expect-error -export class HaDialog extends Dialog { +export class HaDialog extends DialogBase { public scrollToPos(x: number, y: number) { this.contentElement?.scrollTo(x, y); } @@ -31,77 +31,75 @@ export class HaDialog extends Dialog { return html` ${super.renderHeading()} `; } - protected static get styles(): CSSResultGroup { - return [ - Dialog.styles, - css` - .mdc-dialog { - --mdc-dialog-scroll-divider-color: var(--divider-color); - z-index: var(--dialog-z-index, 7); - -webkit-backdrop-filter: var(--dialog-backdrop-filter, none); - backdrop-filter: var(--dialog-backdrop-filter, none); - } - .mdc-dialog__actions { - justify-content: var(--justify-action-buttons, flex-end); - padding-bottom: max(env(safe-area-inset-bottom), 8px); - } - .mdc-dialog__actions span:nth-child(1) { - flex: var(--secondary-action-button-flex, unset); - } - .mdc-dialog__actions span:nth-child(2) { - flex: var(--primary-action-button-flex, unset); - } - .mdc-dialog__container { - align-items: var(--vertial-align-dialog, center); - } - .mdc-dialog__title::before { - display: block; - height: 20px; - } - .mdc-dialog .mdc-dialog__content { - position: var(--dialog-content-position, relative); - padding: var(--dialog-content-padding, 20px 24px); - } - :host([hideactions]) .mdc-dialog .mdc-dialog__content { - padding-bottom: max( - var(--dialog-content-padding, 20px), - env(safe-area-inset-bottom) - ); - } - .mdc-dialog .mdc-dialog__surface { - position: var(--dialog-surface-position, relative); - top: var(--dialog-surface-top); - min-height: var(--mdc-dialog-min-height, auto); - border-radius: var( - --ha-dialog-border-radius, - var(--ha-card-border-radius, 4px) - ); - } - :host([flexContent]) .mdc-dialog .mdc-dialog__content { - display: flex; - flex-direction: column; - } - .header_button { - position: absolute; - right: 16px; - top: 10px; - text-decoration: none; - color: inherit; - } - .header_title { - margin-right: 40px; - } - [dir="rtl"].header_button { - right: auto; - left: 16px; - } - [dir="rtl"].header_title { - margin-left: 40px; - margin-right: 0px; - } - `, - ]; - } + static override styles = [ + styles, + css` + .mdc-dialog { + --mdc-dialog-scroll-divider-color: var(--divider-color); + z-index: var(--dialog-z-index, 7); + -webkit-backdrop-filter: var(--dialog-backdrop-filter, none); + backdrop-filter: var(--dialog-backdrop-filter, none); + } + .mdc-dialog__actions { + justify-content: var(--justify-action-buttons, flex-end); + padding-bottom: max(env(safe-area-inset-bottom), 8px); + } + .mdc-dialog__actions span:nth-child(1) { + flex: var(--secondary-action-button-flex, unset); + } + .mdc-dialog__actions span:nth-child(2) { + flex: var(--primary-action-button-flex, unset); + } + .mdc-dialog__container { + align-items: var(--vertial-align-dialog, center); + } + .mdc-dialog__title::before { + display: block; + height: 20px; + } + .mdc-dialog .mdc-dialog__content { + position: var(--dialog-content-position, relative); + padding: var(--dialog-content-padding, 20px 24px); + } + :host([hideactions]) .mdc-dialog .mdc-dialog__content { + padding-bottom: max( + var(--dialog-content-padding, 20px), + env(safe-area-inset-bottom) + ); + } + .mdc-dialog .mdc-dialog__surface { + position: var(--dialog-surface-position, relative); + top: var(--dialog-surface-top); + min-height: var(--mdc-dialog-min-height, auto); + border-radius: var( + --ha-dialog-border-radius, + var(--ha-card-border-radius, 4px) + ); + } + :host([flexContent]) .mdc-dialog .mdc-dialog__content { + display: flex; + flex-direction: column; + } + .header_button { + position: absolute; + right: 16px; + top: 10px; + text-decoration: none; + color: inherit; + } + .header_title { + margin-right: 40px; + } + [dir="rtl"].header_button { + right: auto; + left: 16px; + } + [dir="rtl"].header_title { + margin-left: 40px; + margin-right: 0px; + } + `, + ]; } declare global { diff --git a/src/components/ha-duration-input.ts b/src/components/ha-duration-input.ts index 3de83b1fd3..9ac4e72e2f 100644 --- a/src/components/ha-duration-input.ts +++ b/src/components/ha-duration-input.ts @@ -1,7 +1,8 @@ import { html, LitElement, TemplateResult } from "lit"; import { customElement, property, query } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; -import "./paper-time-input"; +import "./ha-base-time-input"; +import type { TimeChangedEvent } from "./ha-base-time-input"; export interface HaDurationData { hours?: number; @@ -32,110 +33,69 @@ class HaDurationInput extends LitElement { protected render(): TemplateResult { return html` - + .hours=${this._hours} + .minutes=${this._minutes} + .seconds=${this._seconds} + .milliseconds=${this._milliseconds} + @value-changed=${this._durationChanged} + noHoursLimit + hourLabel="hh" + minLabel="mm" + secLabel="ss" + millisecLabel="ms" + > `; } private get _hours() { - return this.data && this.data.hours ? Number(this.data.hours) : 0; + return this.data?.hours ? Number(this.data.hours) : 0; } private get _minutes() { - return this.data && this.data.minutes ? Number(this.data.minutes) : 0; + return this.data?.minutes ? Number(this.data.minutes) : 0; } private get _seconds() { - return this.data && this.data.seconds ? Number(this.data.seconds) : 0; + return this.data?.seconds ? Number(this.data.seconds) : 0; } private get _milliseconds() { - return this.data && this.data.milliseconds - ? Number(this.data.milliseconds) - : 0; + return this.data?.milliseconds ? Number(this.data.milliseconds) : 0; } - private _parseDuration(value) { - return value.toString().padStart(2, "0"); - } + private _durationChanged(ev: CustomEvent<{ value: TimeChangedEvent }>) { + ev.stopPropagation(); + const value = { ...ev.detail.value }; - private _parseDurationMillisec(value) { - return value.toString().padStart(3, "0"); - } - - private _hourChanged(ev) { - this._durationChanged(ev, "hours"); - } - - private _minChanged(ev) { - this._durationChanged(ev, "minutes"); - } - - private _secChanged(ev) { - this._durationChanged(ev, "seconds"); - } - - private _millisecChanged(ev) { - this._durationChanged(ev, "milliseconds"); - } - - private _durationChanged(ev, unit) { - let value = Number(ev.detail.value); - - if (value === this[`_${unit}`]) { - return; + if (!this.enableMillisecond && !value.milliseconds) { + // @ts-ignore + delete value.milliseconds; + } else if (value.milliseconds > 999) { + value.seconds += Math.floor(value.milliseconds / 1000); + value.milliseconds %= 1000; } - let hours = this._hours; - let minutes = this._minutes; - - if (unit === "seconds" && value > 59) { - minutes += Math.floor(value / 60); - value %= 60; + if (value.seconds > 59) { + value.minutes += Math.floor(value.seconds / 60); + value.seconds %= 60; } - if (unit === "minutes" && value > 59) { - hours += Math.floor(value / 60); - value %= 60; + if (value.minutes > 59) { + value.hours += Math.floor(value.minutes / 60); + value.minutes %= 60; } - const newValue: HaDurationData = { - hours, - minutes, - seconds: this._seconds, - }; - - if (this.enableMillisecond || this._milliseconds) { - newValue.milliseconds = this._milliseconds; - } - - newValue[unit] = value; - fireEvent(this, "value-changed", { - value: newValue, + value, }); } } diff --git a/src/components/ha-form/ha-form-float.ts b/src/components/ha-form/ha-form-float.ts index 7cae936ef7..ce28ee7cae 100644 --- a/src/components/ha-form/ha-form-float.ts +++ b/src/components/ha-form/ha-form-float.ts @@ -1,21 +1,21 @@ -import "@material/mwc-textfield"; -import type { TextField } from "@material/mwc-textfield"; import { css, html, LitElement, TemplateResult, PropertyValues } from "lit"; import { customElement, property, query } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; +import type { HaTextField } from "../ha-textfield"; +import "../ha-textfield"; import { HaFormElement, HaFormFloatData, HaFormFloatSchema } from "./types"; @customElement("ha-form-float") export class HaFormFloat extends LitElement implements HaFormElement { - @property() public schema!: HaFormFloatSchema; + @property({ attribute: false }) public schema!: HaFormFloatSchema; - @property() public data!: HaFormFloatData; + @property({ attribute: false }) public data!: HaFormFloatData; @property() public label!: string; @property({ type: Boolean }) public disabled = false; - @query("mwc-textfield") private _input?: HTMLElement; + @query("ha-textfield") private _input?: HaTextField; public focus() { if (this._input) { @@ -25,7 +25,7 @@ export class HaFormFloat extends LitElement implements HaFormElement { protected render(): TemplateResult { return html` - + > `; } @@ -46,7 +46,7 @@ export class HaFormFloat extends LitElement implements HaFormElement { } private _valueChanged(ev: Event) { - const source = ev.target as TextField; + const source = ev.target as HaTextField; const rawValue = source.value.replace(",", "."); let value: number | undefined; @@ -81,7 +81,7 @@ export class HaFormFloat extends LitElement implements HaFormElement { :host([own-margin]) { margin-bottom: 5px; } - mwc-textfield { + ha-textfield { display: block; } `; diff --git a/src/components/ha-form/ha-form-grid.ts b/src/components/ha-form/ha-form-grid.ts new file mode 100644 index 0000000000..82baf2b3b4 --- /dev/null +++ b/src/components/ha-form/ha-form-grid.ts @@ -0,0 +1,95 @@ +import "./ha-form"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property } from "lit/decorators"; +import type { + HaFormGridSchema, + HaFormDataContainer, + HaFormElement, + HaFormSchema, +} from "./types"; +import type { HomeAssistant } from "../../types"; + +@customElement("ha-form-grid") +export class HaFormGrid extends LitElement implements HaFormElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public data!: HaFormDataContainer; + + @property({ attribute: false }) public schema!: HaFormGridSchema; + + @property({ type: Boolean }) public disabled = false; + + @property() public computeLabel?: ( + schema: HaFormSchema, + data?: HaFormDataContainer + ) => string; + + @property() public computeHelper?: (schema: HaFormSchema) => string; + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + this.setAttribute("own-margin", ""); + } + + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + if (changedProps.has("schema")) { + if (this.schema.column_min_width) { + this.style.setProperty( + "--form-grid-min-width", + this.schema.column_min_width + ); + } else { + this.style.setProperty("--form-grid-min-width", ""); + } + } + } + + protected render(): TemplateResult { + return html` + ${this.schema.schema.map( + (item) => + html` + + ` + )} + `; + } + + static get styles(): CSSResultGroup { + return css` + :host { + display: grid !important; + grid-template-columns: repeat( + var(--form-grid-column-count, auto-fit), + minmax(var(--form-grid-min-width, 200px), 1fr) + ); + grid-gap: 8px; + } + :host > ha-form { + display: block; + margin-bottom: 24px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-form-grid": HaFormGrid; + } +} diff --git a/src/components/ha-form/ha-form-integer.ts b/src/components/ha-form/ha-form-integer.ts index e8ec34f13f..5873eff158 100644 --- a/src/components/ha-form/ha-form-integer.ts +++ b/src/components/ha-form/ha-form-integer.ts @@ -1,6 +1,3 @@ -import "@material/mwc-textfield"; -import type { TextField } from "@material/mwc-textfield"; -import type { Slider } from "@material/mwc-slider"; import { css, CSSResultGroup, @@ -14,18 +11,21 @@ import { fireEvent } from "../../common/dom/fire_event"; import { HaCheckbox } from "../ha-checkbox"; import { HaFormElement, HaFormIntegerData, HaFormIntegerSchema } from "./types"; import "../ha-slider"; +import { HaTextField } from "../ha-textfield"; @customElement("ha-form-integer") export class HaFormInteger extends LitElement implements HaFormElement { - @property() public schema!: HaFormIntegerSchema; + @property({ attribute: false }) public schema!: HaFormIntegerSchema; - @property() public data?: HaFormIntegerData; + @property({ attribute: false }) public data?: HaFormIntegerData; @property() public label?: string; @property({ type: Boolean }) public disabled = false; - @query("paper-input ha-slider") private _input?: HTMLElement; + @query("ha-textfield ha-slider") private _input?: + | HaTextField + | HTMLInputElement; private _lastValue?: HaFormIntegerData; @@ -45,7 +45,7 @@ export class HaFormInteger extends LitElement implements HaFormElement {
${this.label}
- ${this.schema.optional + ${!this.schema.required ? html`
@@ -70,7 +70,7 @@ export class HaFormInteger extends LitElement implements HaFormElement { } return html` - + > `; } @@ -100,7 +100,7 @@ export class HaFormInteger extends LitElement implements HaFormElement { return this.data; } - if (this.schema.optional) { + if (!this.schema.required) { return this.schema.valueMin || 0; } @@ -138,7 +138,7 @@ export class HaFormInteger extends LitElement implements HaFormElement { } private _valueChanged(ev: Event) { - const source = ev.target as TextField | Slider; + const source = ev.target as HaTextField | HTMLInputElement; const rawValue = source.value; let value: number | undefined; @@ -172,7 +172,7 @@ export class HaFormInteger extends LitElement implements HaFormElement { ha-slider { flex: 1; } - mwc-textfield { + ha-textfield { display: block; } `; diff --git a/src/components/ha-form/ha-form-multi_select.ts b/src/components/ha-form/ha-form-multi_select.ts index 3023a1a15f..a591f710bc 100644 --- a/src/components/ha-form/ha-form-multi_select.ts +++ b/src/components/ha-form/ha-form-multi_select.ts @@ -1,25 +1,27 @@ import { mdiMenuDown, mdiMenuUp } from "@mdi/js"; -import "@material/mwc-textfield"; -import "@material/mwc-formfield"; import { css, CSSResultGroup, html, LitElement, - TemplateResult, PropertyValues, + TemplateResult, } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; import "../ha-button-menu"; +import "../ha-check-list-item"; +import type { HaCheckListItem } from "../ha-check-list-item"; +import "../ha-checkbox"; +import type { HaCheckbox } from "../ha-checkbox"; +import "../ha-formfield"; import "../ha-svg-icon"; +import "../ha-textfield"; import { HaFormElement, HaFormMultiSelectData, HaFormMultiSelectSchema, } from "./types"; -import "../ha-checkbox"; -import type { HaCheckbox } from "../ha-checkbox"; function optionValue(item: string | string[]): string { return Array.isArray(item) ? item[0] : item; @@ -57,23 +59,23 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement { : Object.entries(this.schema.options); const data = this.data || []; - const renderedOptions = options.map((item: string | [string, string]) => { - const value = optionValue(item); - return html` - - - - `; - }); - // We will just render all checkboxes. if (options.length < SHOW_ALL_ENTRIES_LIMIT) { - return html`
${this.label}${renderedOptions}
`; + return html`
+ ${this.label}${options.map((item: string | [string, string]) => { + const value = optionValue(item); + return html` + + + + `; + })} +
`; } return html` @@ -83,8 +85,10 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement { corner="BOTTOM_START" @opened=${this._handleOpen} @closed=${this._handleClose} + multi + activatable > - + > - ${renderedOptions} + ${options.map((item: string | [string, string]) => { + const value = optionValue(item); + const selected = data.includes(value); + return html` + ${optionLabel(item)} + `; + })} `; } @@ -105,7 +122,7 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement { protected firstUpdated() { this.updateComplete.then(() => { const { formElement, mdcRoot } = - this.shadowRoot?.querySelector("mwc-textfield") || ({} as any); + this.shadowRoot?.querySelector("ha-textfield") || ({} as any); if (formElement) { formElement.style.textOverflow = "ellipsis"; } @@ -125,9 +142,23 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement { } } + private _selectedChanged(ev: CustomEvent): void { + ev.stopPropagation(); + if (ev.detail.source === "property") { + return; + } + this._handleValueChanged( + (ev.target as HaCheckListItem).value, + ev.detail.selected + ); + } + private _valueChanged(ev: CustomEvent): void { const { value, checked } = ev.target as HaCheckbox; + this._handleValueChanged(value, checked); + } + private _handleValueChanged(value, checked: boolean): void { let newValue: string[]; if (checked) { @@ -171,11 +202,11 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement { display: block; cursor: pointer; } - mwc-formfield { + ha-formfield { display: block; padding-right: 16px; } - mwc-textfield { + ha-textfield { display: block; pointer-events: none; } diff --git a/src/components/ha-form/ha-form-select.ts b/src/components/ha-form/ha-form-select.ts index 2df5d03242..2e342a39a2 100644 --- a/src/components/ha-form/ha-form-select.ts +++ b/src/components/ha-form/ha-form-select.ts @@ -1,14 +1,13 @@ -import "@material/mwc-select"; -import type { Select } from "@material/mwc-select"; import "@material/mwc-list/mwc-list-item"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, query } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; -import "../ha-radio"; -import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./types"; - import { stopPropagation } from "../../common/dom/stop_propagation"; +import "../ha-radio"; import type { HaRadio } from "../ha-radio"; +import "../ha-select"; +import type { HaSelect } from "../ha-select"; +import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./types"; @customElement("ha-form-select") export class HaFormSelect extends LitElement implements HaFormElement { @@ -20,7 +19,7 @@ export class HaFormSelect extends LitElement implements HaFormElement { @property({ type: Boolean }) public disabled = false; - @query("mwc-select", true) private _input?: HTMLElement; + @query("ha-select", true) private _input?: HTMLElement; public focus() { if (this._input) { @@ -29,7 +28,7 @@ export class HaFormSelect extends LitElement implements HaFormElement { } protected render(): TemplateResult { - if (!this.schema.optional && this.schema.options!.length < 6) { + if (this.schema.required && this.schema.options!.length < 6) { return html`
${this.label} @@ -50,7 +49,7 @@ export class HaFormSelect extends LitElement implements HaFormElement { } return html` - - ${this.schema.optional + ${!this.schema.required ? html`` : ""} ${this.schema.options!.map( @@ -67,13 +66,13 @@ export class HaFormSelect extends LitElement implements HaFormElement { ${label} ` )} - + `; } private _valueChanged(ev: CustomEvent) { ev.stopPropagation(); - let value: string | undefined = (ev.target as Select | HaRadio).value; + let value: string | undefined = (ev.target as HaSelect | HaRadio).value; if (value === this.data) { return; @@ -90,7 +89,7 @@ export class HaFormSelect extends LitElement implements HaFormElement { static get styles(): CSSResultGroup { return css` - mwc-select, + ha-select, mwc-formfield { display: block; } diff --git a/src/components/ha-form/ha-form-string.ts b/src/components/ha-form/ha-form-string.ts index fc787e4e63..e82a96519e 100644 --- a/src/components/ha-form/ha-form-string.ts +++ b/src/components/ha-form/ha-form-string.ts @@ -1,17 +1,17 @@ import { mdiEye, mdiEyeOff } from "@mdi/js"; -import "@material/mwc-textfield"; -import type { TextField } from "@material/mwc-textfield"; import { css, CSSResultGroup, html, LitElement, - TemplateResult, PropertyValues, + TemplateResult, } from "lit"; -import { customElement, property, state, query } from "lit/decorators"; +import { customElement, property, query, state } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; import "../ha-icon-button"; +import "../ha-textfield"; +import type { HaTextField } from "../ha-textfield"; import type { HaFormElement, HaFormStringData, @@ -32,7 +32,7 @@ export class HaFormString extends LitElement implements HaFormElement { @state() private _unmaskedPassword = false; - @query("mwc-textfield") private _input?: HTMLElement; + @query("ha-textfield") private _input?: HaTextField; public focus(): void { if (this._input) { @@ -45,7 +45,7 @@ export class HaFormString extends LitElement implements HaFormElement { this.schema.name.includes(field) ); return html` - + > ${isPassword ? html` (obj ? obj[item.name] : null); +const getValue = (obj, item) => + obj ? (!item.name ? obj : obj[item.name]) : null; + +let selectorImported = false; @customElement("ha-form") export class HaForm extends LitElement implements HaFormElement { - @property() public data!: HaFormDataContainer; + @property({ attribute: false }) public hass!: HomeAssistant; - @property() public schema!: HaFormSchema[]; + @property({ attribute: false }) public data!: HaFormDataContainer; + + @property({ attribute: false }) public schema!: HaFormSchema[]; @property() public error?: Record; @@ -27,7 +41,12 @@ export class HaForm extends LitElement implements HaFormElement { @property() public computeError?: (schema: HaFormSchema, error) => string; - @property() public computeLabel?: (schema: HaFormSchema) => string; + @property() public computeLabel?: ( + schema: HaFormSchema, + data?: HaFormDataContainer + ) => string; + + @property() public computeHelper?: (schema: HaFormSchema) => string; public focus() { const root = this.shadowRoot?.querySelector(".root"); @@ -42,7 +61,19 @@ export class HaForm extends LitElement implements HaFormElement { } } - protected render() { + willUpdate(changedProperties: PropertyValues) { + super.willUpdate(changedProperties); + if ( + !selectorImported && + changedProperties.has("schema") && + this.schema?.some((item) => "selector" in item) + ) { + selectorImported = true; + import("../ha-selector/ha-selector"); + } + } + + protected render(): TemplateResult { return html`
${this.error && this.error.base @@ -54,6 +85,7 @@ export class HaForm extends LitElement implements HaFormElement { : ""} ${this.schema.map((item) => { const error = getValue(this.error, item); + return html` ${error ? html` @@ -62,12 +94,26 @@ export class HaForm extends LitElement implements HaFormElement { ` : ""} - ${dynamicElement(`ha-form-${item.type}`, { - schema: item, - data: getValue(this.data, item), - label: this._computeLabel(item), - disabled: this.disabled, - })} + ${"selector" in item + ? html`` + : dynamicElement(`ha-form-${item.type}`, { + schema: item, + data: getValue(this.data, item), + label: this._computeLabel(item, this.data), + disabled: this.disabled, + hass: this.hass, + computeLabel: this.computeLabel, + computeHelper: this.computeHelper, + })} `; })}
@@ -80,21 +126,30 @@ export class HaForm extends LitElement implements HaFormElement { root.addEventListener("value-changed", (ev) => { ev.stopPropagation(); const schema = (ev.target as HaFormElement).schema as HaFormSchema; + + const newValue = !schema.name + ? ev.detail.value + : { [schema.name]: ev.detail.value }; + fireEvent(this, "value-changed", { - value: { ...this.data, [schema.name]: ev.detail.value }, + value: { ...this.data, ...newValue }, }); }); return root; } - private _computeLabel(schema: HaFormSchema) { + private _computeLabel(schema: HaFormSchema, data: HaFormDataContainer) { return this.computeLabel - ? this.computeLabel(schema) + ? this.computeLabel(schema, data) : schema ? schema.name : ""; } + private _computeHelper(schema: HaFormSchema) { + return this.computeHelper ? this.computeHelper(schema) : ""; + } + private _computeError(error, schema: HaFormSchema | HaFormSchema[]) { return this.computeError ? this.computeError(error, schema) : error; } diff --git a/src/components/ha-form/types.ts b/src/components/ha-form/types.ts index c52ba0b947..fc45bc3ef1 100644 --- a/src/components/ha-form/types.ts +++ b/src/components/ha-form/types.ts @@ -1,4 +1,5 @@ import type { LitElement } from "lit"; +import { Selector } from "../../data/selector"; import type { HaDurationData } from "../ha-duration-input"; export type HaFormSchema = @@ -9,14 +10,32 @@ export type HaFormSchema = | HaFormBooleanSchema | HaFormSelectSchema | HaFormMultiSelectSchema - | HaFormTimeSchema; + | HaFormTimeSchema + | HaFormSelector + | HaFormGridSchema; export interface HaFormBaseSchema { name: string; + // This value is applied if no data is submitted for this field default?: HaFormData; required?: boolean; - optional?: boolean; - description?: { suffix?: string; suggested_value?: HaFormData }; + description?: { + suffix?: string; + // This value will be set initially when form is loaded + suggested_value?: HaFormData; + }; +} + +export interface HaFormGridSchema extends HaFormBaseSchema { + type: "grid"; + name: ""; + column_min_width?: string; + schema: HaFormSchema[]; +} + +export interface HaFormSelector extends HaFormBaseSchema { + type?: never; + selector: Selector; } export interface HaFormConstantSchema extends HaFormBaseSchema { @@ -38,7 +57,7 @@ export interface HaFormSelectSchema extends HaFormBaseSchema { export interface HaFormMultiSelectSchema extends HaFormBaseSchema { type: "multi_select"; - options: Record | string[]; + options: Record | string[] | Array<[string, string]>; } export interface HaFormFloatSchema extends HaFormBaseSchema { diff --git a/src/components/ha-formfield.ts b/src/components/ha-formfield.ts index 7ca076e7ea..241f2ec276 100644 --- a/src/components/ha-formfield.ts +++ b/src/components/ha-formfield.ts @@ -1,11 +1,11 @@ -import { Formfield } from "@material/mwc-formfield"; -import { css, CSSResultGroup } from "lit"; +import { FormfieldBase } from "@material/mwc-formfield/mwc-formfield-base"; +import { styles } from "@material/mwc-formfield/mwc-formfield.css"; +import { css } from "lit"; import { customElement } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; @customElement("ha-formfield") -// @ts-expect-error -export class HaFormfield extends Formfield { +export class HaFormfield extends FormfieldBase { protected _labelClick() { const input = this.input; if (input) { @@ -23,20 +23,18 @@ export class HaFormfield extends Formfield { } } - protected static get styles(): CSSResultGroup { - return [ - Formfield.styles, - css` - :host(:not([alignEnd])) ::slotted(ha-switch) { - margin-right: 10px; - } - :host([dir="rtl"]:not([alignEnd])) ::slotted(ha-switch) { - margin-left: 10px; - margin-right: auto; - } - `, - ]; - } + static override styles = [ + styles, + css` + :host(:not([alignEnd])) ::slotted(ha-switch) { + margin-right: 10px; + } + :host([dir="rtl"]:not([alignEnd])) ::slotted(ha-switch) { + margin-left: 10px; + margin-right: auto; + } + `, + ]; } declare global { diff --git a/src/components/ha-hls-player.ts b/src/components/ha-hls-player.ts index fd16491efa..0be5f97535 100644 --- a/src/components/ha-hls-player.ts +++ b/src/components/ha-hls-player.ts @@ -43,6 +43,8 @@ class HaHLSPlayer extends LitElement { @state() private _error?: string; + @state() private _errorIsFatal = false; + private _hlsPolyfillInstance?: HlsLite; private _exoPlayer = false; @@ -53,6 +55,7 @@ class HaHLSPlayer extends LitElement { super.connectedCallback(); HaHLSPlayer.streamCount += 1; if (this.hasUpdated) { + this._resetError(); this._startHls(); } } @@ -64,16 +67,23 @@ class HaHLSPlayer extends LitElement { } protected render(): TemplateResult { - if (this._error) { - return html`${this._error}`; - } return html` - + ${this._error + ? html` + ${this._error} + ` + : ""} + ${!this._errorIsFatal + ? html`` + : ""} `; } @@ -87,12 +97,11 @@ class HaHLSPlayer extends LitElement { } this._cleanUp(); + this._resetError(); this._startHls(); } private async _startHls(): Promise { - this._error = undefined; - const masterPlaylistPromise = fetch(this.url); const Hls: typeof HlsType = (await import("hls.js/dist/hls.light.min")) @@ -110,8 +119,8 @@ class HaHLSPlayer extends LitElement { } if (!hlsSupported) { - this._error = this.hass.localize( - "ui.components.media-browser.video_not_supported" + this._setFatalError( + this.hass.localize("ui.components.media-browser.video_not_supported") ); return; } @@ -219,9 +228,16 @@ class HaHLSPlayer extends LitElement { this._hlsPolyfillInstance = hls; hls.attachMedia(videoEl); hls.on(Hls.Events.MEDIA_ATTACHED, () => { + this._resetError(); hls.loadSource(url); }); - hls.on(Hls.Events.ERROR, (_, data: any) => { + hls.on(Hls.Events.FRAG_LOADED, (_event, _data: any) => { + this._resetError(); + }); + hls.on(Hls.Events.ERROR, (_event, data: any) => { + // Some errors are recovered automatically by the hls player itself, and the others handled + // in this function require special actions to recover. Errors retried in this function + // are done with backoff to not cause unecessary failures. if (!data.fatal) { return; } @@ -241,22 +257,22 @@ class HaHLSPlayer extends LitElement { error += " (" + data.response.code + ")"; } } - this._error = error; - return; + this._setRetryableError(error); + break; } case Hls.ErrorDetails.MANIFEST_LOAD_TIMEOUT: - this._error = "Timeout while starting stream"; - return; + this._setRetryableError("Timeout while starting stream"); + break; default: - this._error = "Unknown stream network error (" + data.details + ")"; - return; + this._setRetryableError("Stream network error"); + break; } - this._error = "Error with media stream contents (" + data.details + ")"; + hls.startLoad(); } else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) { - this._error = "Error with media stream contents (" + data.details + ")"; + this._setRetryableError("Error with media stream contents"); + hls.recoverMediaError(); } else { - this._error = - "Unknown error with stream (" + data.type + ", " + data.details + ")"; + this._setFatalError("Error playing stream"); } }); } @@ -284,6 +300,21 @@ class HaHLSPlayer extends LitElement { } } + private _resetError() { + this._error = undefined; + this._errorIsFatal = false; + } + + private _setFatalError(errorMessage: string) { + this._error = errorMessage; + this._errorIsFatal = true; + } + + private _setRetryableError(errorMessage: string) { + this._error = errorMessage; + this._errorIsFatal = false; + } + static get styles(): CSSResultGroup { return css` :host, @@ -296,10 +327,14 @@ class HaHLSPlayer extends LitElement { max-height: var(--video-max-height, calc(100vh - 97px)); } - ha-alert { + .fatal { display: block; padding: 100px 16px; } + + .retry { + display: block; + } `; } } diff --git a/src/components/ha-icon-picker.ts b/src/components/ha-icon-picker.ts index 93da7a5d87..57ec368cdc 100644 --- a/src/components/ha-icon-picker.ts +++ b/src/components/ha-icon-picker.ts @@ -1,16 +1,13 @@ -import { mdiCheck, mdiMenuDown, mdiMenuUp } from "@mdi/js"; -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-item/paper-icon-item"; -import "@polymer/paper-item/paper-item-body"; -import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light"; import { css, html, LitElement, TemplateResult } from "lit"; -import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers"; +import { ComboBoxLitRenderer } from "lit-vaadin-helpers"; import { customElement, property, query, state } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; import { customIcons } from "../data/custom_icons"; import { PolymerChangedEvent } from "../polymer-types"; +import { HomeAssistant } from "../types"; +import "./ha-combo-box"; +import type { HaComboBox } from "./ha-combo-box"; import "./ha-icon"; -import "./ha-icon-button"; type IconItem = { icon: string; @@ -19,35 +16,17 @@ type IconItem = { let iconItems: IconItem[] = []; // eslint-disable-next-line lit/prefer-static-styles -const rowRenderer: ComboBoxLitRenderer = (item) => html` - - - - - ${item.icon} - `; +const rowRenderer: ComboBoxLitRenderer = (item) => html` + + ${item.icon} +`; @customElement("ha-icon-picker") export class HaIconPicker extends LitElement { + @property() public hass?: HomeAssistant; + @property() public value?: string; @property() public label?: string; @@ -64,51 +43,40 @@ export class HaIconPicker extends LitElement { @state() private _opened = false; - @query("vaadin-combo-box-light", true) private comboBox!: HTMLElement; + @query("ha-combo-box", true) private comboBox!: HaComboBox; protected render(): TemplateResult { return html` - - - ${this._value || this.placeholder - ? html` - - - ` - : this.fallbackPath - ? html`` - : ""} - - - + ${this._value || this.placeholder + ? html` + + + ` + : this.fallbackPath + ? html`` + : ""} + `; } @@ -150,6 +118,7 @@ export class HaIconPicker extends LitElement { } private _valueChanged(ev: PolymerChangedEvent) { + ev.stopPropagation(); this._setValue(ev.detail.value); } @@ -158,7 +127,7 @@ export class HaIconPicker extends LitElement { fireEvent( this, "value-changed", - { value }, + { value: this._value }, { bubbles: false, composed: false, @@ -205,17 +174,13 @@ export class HaIconPicker extends LitElement { return css` ha-icon, ha-svg-icon { + color: var(--primary-text-color); position: relative; bottom: 2px; } *[slot="prefix"] { margin-right: 8px; } - paper-input > ha-icon-button { - --mdc-icon-button-size: 24px; - padding: 2px; - color: var(--secondary-text-color); - } `; } } diff --git a/src/components/ha-paper-dropdown-menu.ts b/src/components/ha-paper-dropdown-menu.ts deleted file mode 100644 index 72f2f6b15c..0000000000 --- a/src/components/ha-paper-dropdown-menu.ts +++ /dev/null @@ -1,28 +0,0 @@ -import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; -import { PolymerElement } from "@polymer/polymer"; -import { Constructor } from "../types"; - -const paperDropdownClass = customElements.get( - "paper-dropdown-menu" -) as Constructor; - -// patches paper drop down to properly support RTL - https://github.com/PolymerElements/paper-dropdown-menu/issues/183 -export class HaPaperDropdownClass extends paperDropdownClass { - public ready() { - super.ready(); - // wait to check for direction since otherwise direction is wrong even though top level is RTL - setTimeout(() => { - if (window.getComputedStyle(this).direction === "rtl") { - this.style.textAlign = "right"; - } - }, 100); - } -} - -declare global { - interface HTMLElementTagNameMap { - "ha-paper-dropdown-menu": HaPaperDropdownClass; - } -} - -customElements.define("ha-paper-dropdown-menu", HaPaperDropdownClass); diff --git a/src/components/ha-picture-upload.ts b/src/components/ha-picture-upload.ts index b18852cc3c..15d2fef7ae 100644 --- a/src/components/ha-picture-upload.ts +++ b/src/components/ha-picture-upload.ts @@ -1,5 +1,4 @@ import { mdiImagePlus } from "@mdi/js"; -import "@polymer/paper-input/paper-input-container"; import { html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; diff --git a/src/components/ha-qr-scanner.ts b/src/components/ha-qr-scanner.ts index 4b4c3d859c..87d0399914 100644 --- a/src/components/ha-qr-scanner.ts +++ b/src/components/ha-qr-scanner.ts @@ -1,7 +1,5 @@ +import "@material/mwc-button/mwc-button"; import "@material/mwc-list/mwc-list-item"; -import "@material/mwc-select/mwc-select"; -import "@material/mwc-textfield/mwc-textfield"; -import type { TextField } from "@material/mwc-textfield/mwc-textfield"; import { mdiCamera } from "@mdi/js"; import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; import { customElement, property, query, state } from "lit/decorators"; @@ -11,7 +9,8 @@ import { stopPropagation } from "../common/dom/stop_propagation"; import { LocalizeFunc } from "../common/translations/localize"; import "./ha-alert"; import "./ha-button-menu"; -import "@material/mwc-button/mwc-button"; +import "./ha-textfield"; +import type { HaTextField } from "./ha-textfield"; @customElement("ha-qr-scanner") class HaQrScanner extends LitElement { @@ -29,7 +28,7 @@ class HaQrScanner extends LitElement { @query("#canvas-container", true) private _canvasContainer!: HTMLDivElement; - @query("mwc-textfield") private _manualInput?: TextField; + @query("ha-textfield") private _manualInput?: HaTextField; public disconnectedCallback(): void { super.disconnectedCallback(); @@ -102,11 +101,11 @@ class HaQrScanner extends LitElement {

${this.localize("ui.components.qr-scanner.manual_input")}

- + > ${this.localize("ui.common.submit")} @@ -161,7 +160,7 @@ class HaQrScanner extends LitElement { private _manualKeyup(ev: KeyboardEvent) { if (ev.key === "Enter") { - this._qrCodeScanned((ev.target as TextField).value); + this._qrCodeScanned((ev.target as HaTextField).value); } } @@ -199,7 +198,7 @@ class HaQrScanner extends LitElement { display: flex; align-items: center; } - mwc-textfield { + ha-textfield { flex: 1; margin-right: 8px; } diff --git a/src/components/ha-radio.ts b/src/components/ha-radio.ts index a2fb2b5929..f551d4fedc 100644 --- a/src/components/ha-radio.ts +++ b/src/components/ha-radio.ts @@ -1,12 +1,18 @@ -import { Radio } from "@material/mwc-radio"; +import { RadioBase } from "@material/mwc-radio/mwc-radio-base"; +import { styles } from "@material/mwc-radio/mwc-radio.css"; +import { css } from "lit"; import { customElement } from "lit/decorators"; @customElement("ha-radio") -export class HaRadio extends Radio { - public firstUpdated() { - super.firstUpdated(); - this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)"); - } +export class HaRadio extends RadioBase { + static override styles = [ + styles, + css` + :host { + --mdc-theme-secondary: var(--primary-color); + } + `, + ]; } declare global { diff --git a/src/components/ha-select.ts b/src/components/ha-select.ts new file mode 100644 index 0000000000..a6e69ee858 --- /dev/null +++ b/src/components/ha-select.ts @@ -0,0 +1,47 @@ +import { SelectBase } from "@material/mwc-select/mwc-select-base"; +import { styles } from "@material/mwc-select/mwc-select.css"; +import { html, nothing } from "lit"; +import { customElement, property } from "lit/decorators"; +import { debounce } from "../common/util/debounce"; +import { nextRender } from "../common/util/render-status"; + +@customElement("ha-select") +export class HaSelect extends SelectBase { + // @ts-ignore + @property({ type: Boolean }) public icon?: boolean; + + protected override renderLeadingIcon() { + if (!this.icon) { + return nothing; + } + + return html``; + } + + static override styles = [styles]; + + connectedCallback() { + super.connectedCallback(); + window.addEventListener("translations-updated", this._translationsUpdated); + } + + disconnectedCallback() { + super.disconnectedCallback(); + window.removeEventListener( + "translations-updated", + this._translationsUpdated + ); + } + + private _translationsUpdated = debounce(async () => { + await nextRender(); + this.layoutOptions(); + }, 500); +} +declare global { + interface HTMLElementTagNameMap { + "ha-select": HaSelect; + } +} diff --git a/src/components/ha-selector/ha-selector-addon.ts b/src/components/ha-selector/ha-selector-addon.ts index 30214933f1..47bb9c045b 100644 --- a/src/components/ha-selector/ha-selector-addon.ts +++ b/src/components/ha-selector/ha-selector-addon.ts @@ -1,4 +1,4 @@ -import { html, LitElement } from "lit"; +import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { AddonSelector } from "../../data/selector"; import { HomeAssistant } from "../../types"; @@ -22,6 +22,12 @@ export class HaAddonSelector extends LitElement { allow-custom-entity >`; } + + static styles = css` + ha-addon-picker { + width: 100%; + } + `; } declare global { diff --git a/src/components/ha-selector/ha-selector-attribute.ts b/src/components/ha-selector/ha-selector-attribute.ts new file mode 100644 index 0000000000..5739089061 --- /dev/null +++ b/src/components/ha-selector/ha-selector-attribute.ts @@ -0,0 +1,38 @@ +import "../entity/ha-entity-attribute-picker"; +import { html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { AttributeSelector } from "../../data/selector"; +import { SubscribeMixin } from "../../mixins/subscribe-mixin"; +import { HomeAssistant } from "../../types"; + +@customElement("ha-selector-attribute") +export class HaSelectorAttribute extends SubscribeMixin(LitElement) { + @property() public hass!: HomeAssistant; + + @property() public selector!: AttributeSelector; + + @property() public value?: any; + + @property() public label?: string; + + @property({ type: Boolean }) public disabled = false; + + protected render() { + return html` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-attribute": HaSelectorAttribute; + } +} diff --git a/src/components/ha-selector/ha-selector-boolean.ts b/src/components/ha-selector/ha-selector-boolean.ts index f140ba99e4..4ac5918b4c 100644 --- a/src/components/ha-selector/ha-selector-boolean.ts +++ b/src/components/ha-selector/ha-selector-boolean.ts @@ -35,9 +35,12 @@ export class HaBooleanSelector extends LitElement { static get styles(): CSSResultGroup { return css` + :host { + height: 56px; + display: flex; + } ha-formfield { width: 100%; - margin: 16px 0; --mdc-typography-body2-font-size: 1em; } `; diff --git a/src/components/ha-selector/ha-selector-duration.ts b/src/components/ha-selector/ha-selector-duration.ts new file mode 100644 index 0000000000..1471750d90 --- /dev/null +++ b/src/components/ha-selector/ha-selector-duration.ts @@ -0,0 +1,37 @@ +import "../ha-duration-input"; +import { html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { DurationSelector } from "../../data/selector"; +import { HomeAssistant } from "../../types"; + +@customElement("ha-selector-duration") +export class HaTimeDuration extends LitElement { + @property() public hass!: HomeAssistant; + + @property() public selector!: DurationSelector; + + @property() public value?: string; + + @property() public label?: string; + + @property({ type: Boolean }) public disabled = false; + + @property({ type: Boolean }) public required = true; + + protected render() { + return html` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-duration": HaTimeDuration; + } +} diff --git a/src/components/ha-selector/ha-selector-entity.ts b/src/components/ha-selector/ha-selector-entity.ts index 2facd604ba..9159a5dd7b 100644 --- a/src/components/ha-selector/ha-selector-entity.ts +++ b/src/components/ha-selector/ha-selector-entity.ts @@ -50,7 +50,12 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) { private _filterEntities = (entity: HassEntity): boolean => { if (this.selector.entity?.domain) { - if (computeStateDomain(entity) !== this.selector.entity.domain) { + const filterDomain = this.selector.entity.domain; + const entityDomain = computeStateDomain(entity); + if ( + (Array.isArray(filterDomain) && !filterDomain.includes(entityDomain)) || + entityDomain !== filterDomain + ) { return false; } } diff --git a/src/components/ha-selector/ha-selector-icon.ts b/src/components/ha-selector/ha-selector-icon.ts new file mode 100644 index 0000000000..0e4a712588 --- /dev/null +++ b/src/components/ha-selector/ha-selector-icon.ts @@ -0,0 +1,41 @@ +import "../ha-icon-picker"; +import { html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { HomeAssistant } from "../../types"; +import { IconSelector } from "../../data/selector"; +import { fireEvent } from "../../common/dom/fire_event"; + +@customElement("ha-selector-icon") +export class HaIconSelector extends LitElement { + @property() public hass!: HomeAssistant; + + @property() public selector!: IconSelector; + + @property() public value?: string; + + @property() public label?: string; + + @property({ type: Boolean, reflect: true }) public disabled = false; + + protected render() { + return html` + + `; + } + + private _valueChanged(ev: CustomEvent) { + fireEvent(this, "value-changed", { value: ev.detail.value }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-icon": HaIconSelector; + } +} diff --git a/src/components/ha-selector/ha-selector-media.ts b/src/components/ha-selector/ha-selector-media.ts new file mode 100644 index 0000000000..bbfb203050 --- /dev/null +++ b/src/components/ha-selector/ha-selector-media.ts @@ -0,0 +1,264 @@ +import { mdiPlayBox, mdiPlus } from "@mdi/js"; +import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; +import { fireEvent } from "../../common/dom/fire_event"; +import { supportsFeature } from "../../common/entity/supports-feature"; +import { getSignedPath } from "../../data/auth"; +import { + MediaClassBrowserSettings, + MediaPickedEvent, + SUPPORT_BROWSE_MEDIA, +} from "../../data/media-player"; +import type { MediaSelector, MediaSelectorValue } from "../../data/selector"; +import type { HomeAssistant } from "../../types"; +import "../ha-alert"; +import "../ha-form/ha-form"; +import type { HaFormSchema } from "../ha-form/types"; +import { showMediaBrowserDialog } from "../media-player/show-media-browser-dialog"; + +const MANUAL_SCHEMA = [ + { name: "media_content_id", required: false, selector: { text: {} } }, + { name: "media_content_type", required: false, selector: { text: {} } }, +]; + +@customElement("ha-selector-media") +export class HaMediaSelector extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public selector!: MediaSelector; + + @property({ attribute: false }) public value?: MediaSelectorValue; + + @property() public label?: string; + + @property({ type: Boolean, reflect: true }) public disabled = false; + + @state() private _thumbnailUrl?: string | null; + + willUpdate(changedProps: PropertyValues) { + if (changedProps.has("value")) { + const thumbnail = this.value?.metadata?.thumbnail; + const oldThumbnail = (changedProps.get("value") as this["value"]) + ?.metadata?.thumbnail; + if (thumbnail === oldThumbnail) { + return; + } + if (thumbnail && thumbnail.startsWith("/")) { + this._thumbnailUrl = undefined; + // Thumbnails served by local API require authentication + getSignedPath(this.hass, thumbnail).then((signedPath) => { + this._thumbnailUrl = signedPath.path; + }); + } else { + this._thumbnailUrl = thumbnail; + } + } + } + + protected render() { + const stateObj = this.value?.entity_id + ? this.hass.states[this.value.entity_id] + : undefined; + + const supportsBrowse = + !this.value?.entity_id || + (stateObj && supportsFeature(stateObj, SUPPORT_BROWSE_MEDIA)); + + return html` + ${!supportsBrowse + ? html` + ${this.hass.localize( + "ui.components.selectors.media.browse_not_supported" + )} + + ` + : html` +
+ ${this.value?.metadata?.thumbnail + ? html` +
+ ` + : html` +
+ +
+ `} +
+
+ ${!this.value?.media_content_id + ? this.hass.localize("ui.components.selectors.media.pick_media") + : this.value.metadata?.title || this.value.media_content_id} +
+
`}`; + } + + private _computeLabelCallback = (schema: HaFormSchema): string => + this.hass.localize(`ui.components.selectors.media.${schema.name}`); + + private _entityChanged(ev: CustomEvent) { + ev.stopPropagation(); + fireEvent(this, "value-changed", { + value: { + entity_id: ev.detail.value, + media_content_id: "", + media_content_type: "", + }, + }); + } + + private _pickMedia() { + showMediaBrowserDialog(this, { + action: "pick", + entityId: this.value!.entity_id!, + navigateIds: this.value!.metadata?.navigateIds, + mediaPickedCallback: (pickedMedia: MediaPickedEvent) => { + fireEvent(this, "value-changed", { + value: { + ...this.value, + media_content_id: pickedMedia.item.media_content_id, + media_content_type: pickedMedia.item.media_content_type, + metadata: { + title: pickedMedia.item.title, + thumbnail: pickedMedia.item.thumbnail, + media_class: pickedMedia.item.media_class, + children_media_class: pickedMedia.item.children_media_class, + navigateIds: pickedMedia.navigateIds?.map((id) => ({ + media_content_type: id.media_content_type, + media_content_id: id.media_content_id, + })), + }, + }, + }); + }, + }); + } + + static get styles(): CSSResultGroup { + return css` + ha-entity-picker { + display: block; + margin-bottom: 16px; + } + mwc-button { + margin-top: 8px; + } + ha-alert { + display: block; + margin-bottom: 16px; + } + ha-card { + position: relative; + width: 200px; + box-sizing: border-box; + cursor: pointer; + } + ha-card.disabled { + pointer-events: none; + color: var(--disabled-text-color); + } + ha-card .thumbnail { + width: 100%; + position: relative; + box-sizing: border-box; + transition: padding-bottom 0.1s ease-out; + padding-bottom: 100%; + } + ha-card .thumbnail.portrait { + padding-bottom: 150%; + } + ha-card .image { + border-radius: 3px 3px 0 0; + } + .folder { + --mdc-icon-size: calc(var(--media-browse-item-size, 175px) * 0.4); + } + .title { + font-size: 16px; + padding-top: 16px; + overflow: hidden; + text-overflow: ellipsis; + margin-bottom: 16px; + padding-left: 16px; + padding-right: 4px; + white-space: nowrap; + } + .image { + position: absolute; + top: 0; + right: 0; + left: 0; + bottom: 0; + background-size: cover; + background-repeat: no-repeat; + background-position: center; + } + .centered-image { + margin: 0 8px; + background-size: contain; + } + .icon-holder { + display: flex; + justify-content: center; + align-items: center; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-media": HaMediaSelector; + } +} diff --git a/src/components/ha-selector/ha-selector-number.ts b/src/components/ha-selector/ha-selector-number.ts index 244f97e2ce..0094192f1b 100644 --- a/src/components/ha-selector/ha-selector-number.ts +++ b/src/components/ha-selector/ha-selector-number.ts @@ -1,4 +1,3 @@ -import "@polymer/paper-input/paper-input"; import { css, CSSResultGroup, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; @@ -6,6 +5,7 @@ import { fireEvent } from "../../common/dom/fire_event"; import { NumberSelector } from "../../data/selector"; import { HomeAssistant } from "../../types"; import "../ha-slider"; +import "../ha-textfield"; @customElement("ha-selector-number") export class HaNumberSelector extends LitElement { @@ -19,56 +19,58 @@ export class HaNumberSelector extends LitElement { @property() public label?: string; + @property({ type: Boolean }) public required = true; + @property({ type: Boolean }) public disabled = false; protected render() { - return html`${this.label} - ${this.selector.number.mode !== "box" - ? html` - ` + return html`${this.selector.number.mode !== "box" + ? html`${this.label} + ` : ""} - - ${this.selector.number.unit_of_measurement - ? html`
- ${this.selector.number.unit_of_measurement} -
` - : ""} -
`; + `; } private get _value() { - return this.value || 0; + return this.value ?? (this.selector.number.min || 0); } private _handleInputChange(ev) { ev.stopPropagation(); const value = - ev.detail.value === "" || isNaN(ev.detail.value) - ? undefined - : Number(ev.detail.value); + ev.target.value === "" || isNaN(ev.target.value) + ? this.required + ? this.selector.number.min || 0 + : undefined + : Number(ev.target.value); if (this.value === value) { return; } @@ -94,7 +96,11 @@ export class HaNumberSelector extends LitElement { ha-slider { flex: 1; } + ha-textfield { + --ha-textfield-input-width: 40px; + } .single { + --ha-textfield-input-width: unset; flex: 1; } `; diff --git a/src/components/ha-selector/ha-selector-object.ts b/src/components/ha-selector/ha-selector-object.ts index e003f28251..0fa42b9588 100644 --- a/src/components/ha-selector/ha-selector-object.ts +++ b/src/components/ha-selector/ha-selector-object.ts @@ -18,6 +18,7 @@ export class HaObjectSelector extends LitElement { protected render() { return html` - - ${this.selector.select.options.map( - (item: string) => html` - ${item} - ` - )} - - `; + ${this.selector.select.options.map((item: string | SelectOption) => { + const value = typeof item === "object" ? item.value : item; + const label = typeof item === "object" ? item.label : item; + + return html`${label}`; + })} + `; } private _valueChanged(ev) { - if (this.disabled || !ev.detail.value) { + ev.stopPropagation(); + if (this.disabled || !ev.target.value) { return; } fireEvent(this, "value-changed", { - value: ev.detail.value.itemValue, + value: ev.target.value, }); } static get styles(): CSSResultGroup { return css` - ha-paper-dropdown-menu { + ha-select { width: 100%; - min-width: 200px; - display: block; - } - paper-listbox { - min-width: 200px; - } - paper-item { - cursor: pointer; } `; } diff --git a/src/components/ha-selector/ha-selector-target.ts b/src/components/ha-selector/ha-selector-target.ts index 6ed0251665..4d81be67df 100644 --- a/src/components/ha-selector/ha-selector-target.ts +++ b/src/components/ha-selector/ha-selector-target.ts @@ -1,8 +1,3 @@ -import "@material/mwc-list/mwc-list"; -import "@material/mwc-list/mwc-list-item"; -import "@material/mwc-tab-bar/mwc-tab-bar"; -import "@material/mwc-tab/mwc-tab"; -import "@polymer/paper-input/paper-input"; import { HassEntity, HassServiceTarget, diff --git a/src/components/ha-selector/ha-selector-text.ts b/src/components/ha-selector/ha-selector-text.ts index 25cddf58d8..2e6c879d9e 100644 --- a/src/components/ha-selector/ha-selector-text.ts +++ b/src/components/ha-selector/ha-selector-text.ts @@ -1,10 +1,12 @@ -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-input/paper-textarea"; -import { html, LitElement } from "lit"; -import { customElement, property } from "lit/decorators"; +import { mdiEye, mdiEyeOff } from "@mdi/js"; +import { css, CSSResultGroup, html, LitElement } from "lit"; +import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; import { StringSelector } from "../../data/selector"; import { HomeAssistant } from "../../types"; +import "../ha-icon-button"; +import "../ha-textarea"; +import "../ha-textfield"; @customElement("ha-selector-text") export class HaTextSelector extends LitElement { @@ -20,36 +22,84 @@ export class HaTextSelector extends LitElement { @property({ type: Boolean }) public disabled = false; + @property({ type: Boolean }) public required = true; + + @state() private _unmaskedPassword = false; + protected render() { if (this.selector.text?.multiline) { - return html``; + .required=${this.required} + autogrow + >`; } - return html``; + return html`
` + : this.selector.text?.suffix} + .required=${this.required} + > + ${this.selector.text?.type === "password" + ? html`` + : ""}`; + } + + private _toggleUnmaskedPassword(): void { + this._unmaskedPassword = !this._unmaskedPassword; } private _handleChange(ev) { - const value = ev.target.value; + let value = ev.target.value; if (this.value === value) { return; } + if (value === "" && !this.required) { + value = undefined; + } + fireEvent(this, "value-changed", { value }); } + + static get styles(): CSSResultGroup { + return css` + :host { + display: block; + position: relative; + } + ha-textarea, + ha-textfield { + width: 100%; + } + ha-icon-button { + position: absolute; + top: 16px; + right: 16px; + --mdc-icon-button-size: 24px; + --mdc-icon-size: 20px; + color: var(--secondary-text-color); + } + `; + } } declare global { diff --git a/src/components/ha-selector/ha-selector-theme.ts b/src/components/ha-selector/ha-selector-theme.ts new file mode 100644 index 0000000000..d25539908f --- /dev/null +++ b/src/components/ha-selector/ha-selector-theme.ts @@ -0,0 +1,34 @@ +import "../../panels/lovelace/components/hui-theme-select-editor"; +import { html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import type { HomeAssistant } from "../../types"; +import type { ThemeSelector } from "../../data/selector"; + +@customElement("ha-selector-theme") +export class HaThemeSelector extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public selector!: ThemeSelector; + + @property() public value?: string; + + @property() public label?: string; + + @property({ type: Boolean, reflect: true }) public disabled = false; + + protected render() { + return html` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-theme": HaThemeSelector; + } +} diff --git a/src/components/ha-selector/ha-selector-time.ts b/src/components/ha-selector/ha-selector-time.ts index f1a116d371..ff829ff12b 100644 --- a/src/components/ha-selector/ha-selector-time.ts +++ b/src/components/ha-selector/ha-selector-time.ts @@ -22,7 +22,7 @@ export class HaTimeSelector extends LitElement { .value=${this.value} .locale=${this.hass.locale} .disabled=${this.disabled} - hide-label + .label=${this.label} enable-second > `; diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index d426ec0b49..5f6a31aebc 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -1,13 +1,15 @@ import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { dynamicElement } from "../../common/dom/dynamic-element-directive"; -import { Selector } from "../../data/selector"; -import { HomeAssistant } from "../../types"; +import type { Selector } from "../../data/selector"; +import type { HomeAssistant } from "../../types"; import "./ha-selector-action"; import "./ha-selector-addon"; import "./ha-selector-area"; +import "./ha-selector-attribute"; import "./ha-selector-boolean"; import "./ha-selector-device"; +import "./ha-selector-duration"; import "./ha-selector-entity"; import "./ha-selector-number"; import "./ha-selector-object"; @@ -15,6 +17,9 @@ import "./ha-selector-select"; import "./ha-selector-target"; import "./ha-selector-text"; import "./ha-selector-time"; +import "./ha-selector-icon"; +import "./ha-selector-media"; +import "./ha-selector-theme"; @customElement("ha-selector") export class HaSelector extends LitElement { @@ -26,16 +31,16 @@ export class HaSelector extends LitElement { @property() public label?: string; + @property() public helper?: string; + @property() public placeholder?: any; @property({ type: Boolean }) public disabled = false; + @property({ type: Boolean }) public required = true; + public focus() { - const input = this.shadowRoot!.getElementById("selector"); - if (!input) { - return; - } - (input as HTMLElement).focus(); + this.shadowRoot?.getElementById("selector")?.focus(); } private get _type() { @@ -51,6 +56,8 @@ export class HaSelector extends LitElement { label: this.label, placeholder: this.placeholder, disabled: this.disabled, + required: this.required, + helper: this.helper, id: "selector", })} `; diff --git a/src/components/ha-service-control.ts b/src/components/ha-service-control.ts index 87b27a36f8..ee1692e567 100644 --- a/src/components/ha-service-control.ts +++ b/src/components/ha-service-control.ts @@ -67,7 +67,7 @@ export class HaServiceControl extends LitElement { @query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor; - protected updated(changedProperties: PropertyValues) { + protected willUpdate(changedProperties: PropertyValues) { if (!changedProperties.has("value")) { return; } @@ -286,6 +286,7 @@ export class HaServiceControl extends LitElement { : ""} ${shouldRenderServiceDataYaml ? html` = ( item - // eslint-disable-next-line lit/prefer-static-styles -) => html` - - - - ${item.name} - ${item.name === item.service ? "" : item.service} - - `; +) => html` + ${item.name} + ${item.name === item.service ? "" : item.service} +`; class HaServicePicker extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; diff --git a/src/components/ha-slider.js b/src/components/ha-slider.js index 57caa35a8d..540fdf531e 100644 --- a/src/components/ha-slider.js +++ b/src/components/ha-slider.js @@ -3,7 +3,7 @@ import "@polymer/paper-slider"; const PaperSliderClass = customElements.get("paper-slider"); let subTemplate; -class HaSlider extends PaperSliderClass { +export class HaSlider extends PaperSliderClass { static get template() { if (!subTemplate) { subTemplate = PaperSliderClass.template.cloneNode(true); diff --git a/src/components/ha-switch.ts b/src/components/ha-switch.ts index 22400b0c95..262359c9ac 100644 --- a/src/components/ha-switch.ts +++ b/src/components/ha-switch.ts @@ -1,11 +1,11 @@ -import { Switch } from "@material/mwc-switch/deprecated"; -import { css, CSSResultGroup } from "lit"; +import { SwitchBase } from "@material/mwc-switch/deprecated/mwc-switch-base"; +import { styles } from "@material/mwc-switch/deprecated/mwc-switch.css"; +import { css } from "lit"; import { customElement, property } from "lit/decorators"; import { forwardHaptic } from "../data/haptics"; @customElement("ha-switch") -// @ts-expect-error -export class HaSwitch extends Switch { +export class HaSwitch extends SwitchBase { // Generate a haptic vibration. // Only set to true if the new value of the switch is applied right away when toggling. // Do not add haptic when a user is required to press save. @@ -13,10 +13,6 @@ export class HaSwitch extends Switch { protected firstUpdated() { super.firstUpdated(); - this.style.setProperty( - "--mdc-theme-secondary", - "var(--switch-checked-color)" - ); this.addEventListener("change", () => { if (this.haptic) { forwardHaptic("light"); @@ -24,29 +20,30 @@ export class HaSwitch extends Switch { }); } - static get styles(): CSSResultGroup { - return [ - Switch.styles, - css` - .mdc-switch.mdc-switch--checked .mdc-switch__thumb { - background-color: var(--switch-checked-button-color); - border-color: var(--switch-checked-button-color); - } - .mdc-switch.mdc-switch--checked .mdc-switch__track { - background-color: var(--switch-checked-track-color); - border-color: var(--switch-checked-track-color); - } - .mdc-switch:not(.mdc-switch--checked) .mdc-switch__thumb { - background-color: var(--switch-unchecked-button-color); - border-color: var(--switch-unchecked-button-color); - } - .mdc-switch:not(.mdc-switch--checked) .mdc-switch__track { - background-color: var(--switch-unchecked-track-color); - border-color: var(--switch-unchecked-track-color); - } - `, - ]; - } + static override styles = [ + styles, + css` + :host { + --mdc-theme-secondary: var(--switch-checked-color); + } + .mdc-switch.mdc-switch--checked .mdc-switch__thumb { + background-color: var(--switch-checked-button-color); + border-color: var(--switch-checked-button-color); + } + .mdc-switch.mdc-switch--checked .mdc-switch__track { + background-color: var(--switch-checked-track-color); + border-color: var(--switch-checked-track-color); + } + .mdc-switch:not(.mdc-switch--checked) .mdc-switch__thumb { + background-color: var(--switch-unchecked-button-color); + border-color: var(--switch-unchecked-button-color); + } + .mdc-switch:not(.mdc-switch--checked) .mdc-switch__track { + background-color: var(--switch-unchecked-track-color); + border-color: var(--switch-unchecked-track-color); + } + `, + ]; } declare global { diff --git a/src/components/ha-textarea.ts b/src/components/ha-textarea.ts new file mode 100644 index 0000000000..2a3bf1a040 --- /dev/null +++ b/src/components/ha-textarea.ts @@ -0,0 +1,60 @@ +import { TextAreaBase } from "@material/mwc-textarea/mwc-textarea-base"; +import { styles as textfieldStyles } from "@material/mwc-textfield/mwc-textfield.css"; +import { styles as textareaStyles } from "@material/mwc-textarea/mwc-textarea.css"; +import { css, PropertyValues } from "lit"; +import { customElement, property } from "lit/decorators"; + +@customElement("ha-textarea") +export class HaTextArea extends TextAreaBase { + @property({ type: Boolean, reflect: true }) autogrow = false; + + updated(changedProperties: PropertyValues) { + super.updated(changedProperties); + if (this.autogrow && changedProperties.has("value")) { + this.mdcRoot.dataset.value = this.value + '=\u200B"'; // add a zero-width space to correctly wrap + } + } + + static override styles = [ + textfieldStyles, + textareaStyles, + css` + :host([autogrow]) { + max-height: 200px; + } + :host([autogrow]) .mdc-text-field { + position: relative; + min-height: 74px; + min-width: 178px; + } + :host([autogrow]) .mdc-text-field:after { + content: attr(data-value); + margin-top: 23px; + margin-bottom: 9px; + line-height: 1.5rem; + min-height: 42px; + padding: 0px 32px 0 16px; + letter-spacing: var( + --mdc-typography-subtitle1-letter-spacing, + 0.009375em + ); + visibility: hidden; + white-space: pre-wrap; + } + :host([autogrow]) .mdc-text-field__input { + position: absolute; + height: calc(100% - 32px); + } + :host([autogrow]) .mdc-text-field.mdc-text-field--no-label:after { + margin-top: 16px; + margin-bottom: 16px; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-textarea": HaTextArea; + } +} diff --git a/src/components/ha-textfield.ts b/src/components/ha-textfield.ts index 55daec0349..f25225a9ed 100644 --- a/src/components/ha-textfield.ts +++ b/src/components/ha-textfield.ts @@ -1,10 +1,32 @@ -import { TextField } from "@material/mwc-textfield"; -import { TemplateResult, html } from "lit"; -import { customElement } from "lit/decorators"; +import { TextFieldBase } from "@material/mwc-textfield/mwc-textfield-base"; +import { styles } from "@material/mwc-textfield/mwc-textfield.css"; +import { TemplateResult, html, PropertyValues, css } from "lit"; +import { customElement, property } from "lit/decorators"; @customElement("ha-textfield") -export class HaTextField extends TextField { - override renderIcon(_icon: string, isTrailingIcon = false): TemplateResult { +export class HaTextField extends TextFieldBase { + @property({ type: Boolean }) public invalid?: boolean; + + @property({ attribute: "error-message" }) public errorMessage?: string; + + override updated(changedProperties: PropertyValues) { + super.updated(changedProperties); + if ( + (changedProperties.has("invalid") && + (this.invalid || changedProperties.get("invalid") !== undefined)) || + changedProperties.has("errorMessage") + ) { + this.setCustomValidity( + this.invalid ? this.errorMessage || "Invalid" : "" + ); + this.reportValidity(); + } + } + + protected override renderIcon( + _icon: string, + isTrailingIcon = false + ): TemplateResult { const type = isTrailingIcon ? "trailing" : "leading"; return html` @@ -16,6 +38,46 @@ export class HaTextField extends TextField { `; } + + static override styles = [ + styles, + css` + .mdc-text-field__input { + width: var(--ha-textfield-input-width, 100%); + } + .mdc-text-field:not(.mdc-text-field--with-leading-icon) { + padding: var(--text-field-padding, 0px 16px); + } + .mdc-text-field__affix--suffix { + padding-left: var(--text-field-suffix-padding-left, 12px); + padding-right: var(--text-field-suffix-padding-right, 0px); + } + + input { + text-align: var(--text-field-text-align); + } + + /* Chrome, Safari, Edge, Opera */ + :host([no-spinner]) input::-webkit-outer-spin-button, + :host([no-spinner]) input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + + /* Firefox */ + :host([no-spinner]) input[type="number"] { + -moz-appearance: textfield; + } + + .mdc-text-field__ripple { + overflow: hidden; + } + + .mdc-text-field { + overflow: var(--text-field-overflow); + } + `, + ]; } declare global { diff --git a/src/components/ha-time-input.ts b/src/components/ha-time-input.ts index 1b14f40579..5efd5a13f3 100644 --- a/src/components/ha-time-input.ts +++ b/src/components/ha-time-input.ts @@ -2,12 +2,13 @@ import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { useAmPm } from "../common/datetime/use_am_pm"; import { fireEvent } from "../common/dom/fire_event"; -import "./paper-time-input"; +import "./ha-base-time-input"; import { FrontendLocaleData } from "../data/translation"; +import type { TimeChangedEvent } from "./ha-base-time-input"; @customElement("ha-time-input") export class HaTimeInput extends LitElement { - @property() public locale!: FrontendLocaleData; + @property({ attribute: false }) public locale!: FrontendLocaleData; @property() public value?: string; @@ -15,9 +16,6 @@ export class HaTimeInput extends LitElement { @property({ type: Boolean }) public disabled = false; - @property({ type: Boolean, attribute: "hide-label" }) public hideLabel = - false; - @property({ type: Boolean, attribute: "enable-second" }) public enableSecond = false; @@ -35,40 +33,44 @@ export class HaTimeInput extends LitElement { } return html` - = 12 ? "PM" : "AM")} .disabled=${this.disabled} - @change=${this._timeChanged} - @am-pm-changed=${this._timeChanged} - .hideLabel=${this.hideLabel} + @value-changed=${this._timeChanged} .enableSecond=${this.enableSecond} - > + > `; } - private _timeChanged(ev) { - let value = ev.target.value; + private _timeChanged(ev: CustomEvent<{ value: TimeChangedEvent }>) { + ev.stopPropagation(); + const eventValue = ev.detail.value; + const useAMPM = useAmPm(this.locale); - let hours = Number(ev.target.hour || 0); - if (value && useAMPM) { - if (ev.target.amPm === "PM" && hours < 12) { + let hours = eventValue.hours || 0; + if (eventValue && useAMPM) { + if (eventValue.amPm === "PM" && hours < 12) { hours += 12; } - if (ev.target.amPm === "AM" && hours === 12) { + if (eventValue.amPm === "AM" && hours === 12) { hours = 0; } - value = `${hours.toString().padStart(2, "0")}:${ev.target.min || "00"}:${ - ev.target.sec || "00" - }`; } + const value = `${hours.toString().padStart(2, "0")}:${ + eventValue.minutes ? eventValue.minutes.toString().padStart(2, "0") : "00" + }:${ + eventValue.seconds ? eventValue.seconds.toString().padStart(2, "0") : "00" + }`; + if (value === this.value) { return; } + this.value = value; fireEvent(this, "change"); fireEvent(this, "value-changed", { diff --git a/src/components/ha-web-rtc-player.ts b/src/components/ha-web-rtc-player.ts index e65017265e..b55a33eb56 100644 --- a/src/components/ha-web-rtc-player.ts +++ b/src/components/ha-web-rtc-player.ts @@ -43,7 +43,7 @@ class HaWebRtcPlayer extends LitElement { private _remoteStream?: MediaStream; - protected render(): TemplateResult { + protected override render(): TemplateResult { if (this._error) { return html`${this._error}`; } @@ -58,12 +58,19 @@ class HaWebRtcPlayer extends LitElement { `; } - public disconnectedCallback() { + public override connectedCallback() { + super.connectedCallback(); + if (this.hasUpdated) { + this._startWebRtc(); + } + } + + public override disconnectedCallback() { super.disconnectedCallback(); this._cleanUp(); } - protected updated(changedProperties: PropertyValues) { + protected override updated(changedProperties: PropertyValues) { if (!changedProperties.has("entityid")) { return; } diff --git a/src/components/ha-yaml-editor.ts b/src/components/ha-yaml-editor.ts index 03fa830724..d315e804ef 100644 --- a/src/components/ha-yaml-editor.ts +++ b/src/components/ha-yaml-editor.ts @@ -2,6 +2,7 @@ import { DEFAULT_SCHEMA, dump, load, Schema } from "js-yaml"; import { html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; +import type { HomeAssistant } from "../types"; import "./ha-code-editor"; const isEmpty = (obj: Record): boolean => { @@ -18,6 +19,8 @@ const isEmpty = (obj: Record): boolean => { @customElement("ha-yaml-editor") export class HaYamlEditor extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + @property() public value?: any; @property({ attribute: false }) public yamlSchema: Schema = DEFAULT_SCHEMA; @@ -56,8 +59,10 @@ export class HaYamlEditor extends LitElement { return html` ${this.label ? html`

${this.label}

` : ""} (); + + private _filesChanged = false; + + public showDialog(params: MediaManageDialogParams): void { + this._params = params; + this._refreshMedia(); + } + + public closeDialog() { + if (this._filesChanged && this._params!.onClose) { + this._params!.onClose(); + } + this._params = undefined; + this._currentItem = undefined; + this._uploading = false; + this._deleting = false; + this._filesChanged = false; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult { + if (!this._params) { + return html``; + } + + const children = + this._currentItem?.children?.filter((child) => !child.can_expand) || []; + + let fileIndex = 0; + + return html` + + + ${this._selected.size === 0 + ? html` + + ${this.hass.localize( + "ui.components.media-browser.file_management.title" + )} + + + + ${this._uploading + ? "" + : html` + + `} + ` + : html` + + + + + ${this._deleting + ? "" + : html` + + + + `} + `} + + ${!this._currentItem + ? html` +
+ +
+ ` + : !children.length + ? html`
+

+ ${this.hass.localize( + "ui.components.media-browser.file_management.no_items" + )} +

+ ${this._currentItem?.children?.length + ? html`${this.hass.localize( + "ui.components.media-browser.file_management.folders_not_supported" + )}` + : ""} +
` + : html` + + ${repeat( + children, + (item) => item.media_content_id, + (item) => { + const icon = html` + + `; + return html` + + ${icon} ${item.title} + + `; + } + )} + + `} +
+ `; + } + + private _handleSelected(ev) { + this._selected = ev.detail.index; + } + + private _startUploading() { + this._uploading = true; + this._filesChanged = true; + } + + private _doneUploading() { + this._uploading = false; + this._refreshMedia(); + } + + private _handleDeselectAll() { + if (this._selected.size) { + this._selected = new Set(); + } + } + + private async _handleDelete() { + if ( + !(await showConfirmationDialog(this, { + text: this.hass.localize( + "ui.components.media-browser.file_management.confirm_delete", + { count: this._selected.size } + ), + warning: true, + })) + ) { + return; + } + this._filesChanged = true; + this._deleting = true; + + const toDelete: MediaPlayerItem[] = []; + let fileIndex = 0; + this._currentItem!.children!.forEach((item) => { + if (item.can_expand) { + return; + } + if (this._selected.has(fileIndex++)) { + toDelete.push(item); + } + }); + + try { + await Promise.all( + toDelete.map(async (item) => { + await removeLocalMedia(this.hass, item.media_content_id); + this._currentItem = { + ...this._currentItem!, + children: this._currentItem!.children!.filter((i) => i !== item), + }; + }) + ); + } finally { + this._deleting = false; + this._selected = new Set(); + } + } + + private async _refreshMedia() { + this._selected = new Set(); + this._currentItem = undefined; + this._currentItem = await browseLocalMediaPlayer( + this.hass, + this._params!.currentItem.media_content_id + ); + } + + static get styles(): CSSResultGroup { + return [ + haStyleDialog, + css` + ha-dialog { + --dialog-z-index: 8; + --dialog-content-padding: 0; + } + + @media (min-width: 800px) { + ha-dialog { + --mdc-dialog-max-width: 800px; + --dialog-surface-position: fixed; + --dialog-surface-top: 40px; + --mdc-dialog-max-height: calc(100vh - 72px); + } + } + + ha-header-bar { + --mdc-theme-on-primary: var(--primary-text-color); + --mdc-theme-primary: var(--mdc-theme-surface); + flex-shrink: 0; + border-bottom: 1px solid var(--divider-color, rgba(0, 0, 0, 0.12)); + } + + ha-media-upload-button, + mwc-button { + --mdc-theme-primary: var(--mdc-theme-on-primary); + } + + .danger { + --mdc-theme-primary: var(--error-color); + } + + ha-svg-icon[slot="icon"] { + vertical-align: middle; + } + + .refresh { + display: flex; + height: 200px; + justify-content: center; + align-items: center; + } + + .no-items { + text-align: center; + padding: 16px; + } + .folders { + color: var(--secondary-text-color); + font-style: italic; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-media-manage": DialogMediaManage; + } +} diff --git a/src/components/media-player/dialog-media-player-browse.ts b/src/components/media-player/dialog-media-player-browse.ts index 31acf550aa..01f7efbbd8 100644 --- a/src/components/media-player/dialog-media-player-browse.ts +++ b/src/components/media-player/dialog-media-player-browse.ts @@ -1,7 +1,7 @@ import "../ha-header-bar"; import { mdiArrowLeft, mdiClose } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { customElement, property, query, state } from "lit/decorators"; import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event"; import { computeRTLDirection } from "../../common/util/compute_rtl"; import type { @@ -13,7 +13,11 @@ import { haStyleDialog } from "../../resources/styles"; import type { HomeAssistant } from "../../types"; import "../ha-dialog"; import "./ha-media-player-browse"; -import type { MediaPlayerItemId } from "./ha-media-player-browse"; +import "./ha-media-manage-button"; +import type { + HaMediaPlayerBrowse, + MediaPlayerItemId, +} from "./ha-media-player-browse"; import { MediaPlayerBrowseDialogParams } from "./show-media-browser-dialog"; @customElement("dialog-media-player-browse") @@ -26,12 +30,14 @@ class DialogMediaPlayerBrowse extends LitElement { @state() private _params?: MediaPlayerBrowseDialogParams; + @query("ha-media-player-browse") private _browser!: HaMediaPlayerBrowse; + public showDialog(params: MediaPlayerBrowseDialogParams): void { this._params = params; - this._navigateIds = [ + this._navigateIds = params.navigateIds || [ { - media_content_id: this._params.mediaContentId, - media_content_type: this._params.mediaContentType, + media_content_id: undefined, + media_content_type: undefined, }, ]; } @@ -80,6 +86,12 @@ class DialogMediaPlayerBrowse extends LitElement { : this._currentItem.title} + +
+ + + ${this._cloudDefaultOptions ? this._renderCloudOptions() : ""} +
+
+ ${this._cloudDefaultOptions && + (this._cloudDefaultOptions![0] !== this._cloudOptions![0] || + this._cloudDefaultOptions![1] !== this._cloudOptions![1]) + ? html` + + ` + : html``} + + + ${this.hass.localize( + `ui.components.media-browser.tts.action_${this.action}` + )} + +
+ `; + } + + private _renderCloudOptions() { + if (!this._cloudTTSInfo || !this._cloudOptions) { + return ""; + } + const languages = this.getLanguages(this._cloudTTSInfo); + const selectedVoice = this._cloudOptions; + const genders = this.getSupportedGenders( + selectedVoice[0], + this._cloudTTSInfo, + this.hass.localize + ); + + return html` +
+ + ${languages.map( + ([key, label]) => + html`${label}` + )} + + + + ${genders.map( + ([key, label]) => + html`${label}` + )} + +
+ `; + } + + protected override willUpdate(changedProps: PropertyValues): void { + super.willUpdate(changedProps); + + if (changedProps.has("item")) { + if (this.item.media_content_id) { + const params = new URLSearchParams( + this.item.media_content_id.split("?")[1] + ); + const message = params.get("message"); + const language = params.get("language"); + const gender = params.get("gender"); + if (message) { + this._message = message; + } + if (language && gender) { + this._cloudOptions = [language, gender]; + } + } + + if (this.isCloudItem && !this._cloudTTSInfo) { + getCloudTTSInfo(this.hass).then((info) => { + this._cloudTTSInfo = info; + }); + fetchCloudStatus(this.hass).then((status) => { + if (status.logged_in) { + this._cloudDefaultOptions = status.prefs.tts_default_voice; + if (!this._cloudOptions) { + this._cloudOptions = { ...this._cloudDefaultOptions }; + } + } + }); + } + } + + if (changedProps.has("message")) { + return; + } + + // Re-rendering can reset message because textarea content is newer than local storage. + // But we don't want to write every keystroke to local storage. + // So instead we just do it when we're going to render. + const message = this.shadowRoot!.querySelector("ha-textarea")?.value; + if (message !== undefined && message !== this._message) { + this._message = message; + } + } + + async _handleLanguageChange(ev) { + if (ev.target.value === this._cloudOptions![0]) { + return; + } + this._cloudOptions = [ev.target.value, this._cloudOptions![1]]; + } + + async _handleGenderChange(ev) { + if (ev.target.value === this._cloudOptions![1]) { + return; + } + this._cloudOptions = [this._cloudOptions![0], ev.target.value]; + } + + private getLanguages = memoizeOne(getCloudTtsLanguages); + + private getSupportedGenders = memoizeOne(getCloudTtsSupportedGenders); + + private get isCloudItem(): boolean { + return this.item.media_content_id.startsWith("media-source://tts/cloud"); + } + + private async _ttsClicked(): Promise { + const message = this.shadowRoot!.querySelector("ha-textarea")!.value; + this._message = message; + const item = { ...this.item }; + const query = new URLSearchParams(); + query.append("message", message); + if (this._cloudOptions) { + query.append("language", this._cloudOptions[0]); + query.append("gender", this._cloudOptions[1]); + } + item.media_content_id = `${ + item.media_content_id.split("?")[0] + }?${query.toString()}`; + item.can_play = true; + item.title = message; + fireEvent(this, "tts-picked", { item }); + } + + private async _storeDefaults() { + const oldDefaults = this._cloudDefaultOptions!; + this._cloudDefaultOptions = [...this._cloudOptions!]; + try { + await updateCloudPref(this.hass, { + tts_default_voice: this._cloudDefaultOptions, + }); + } catch (err: any) { + this._cloudDefaultOptions = oldDefaults; + showAlertDialog(this, { + text: this.hass.localize( + "ui.components.media-browser.tts.faild_to_store_defaults", + { error: err.message || err } + ), + }); + } + } + + static override styles = [ + buttonLinkStyle, + css` + :host { + margin: 16px auto; + padding: 0 8px; + display: flex; + flex-direction: column; + max-width: 400px; + } + .cloud-options { + margin-top: 16px; + display: flex; + justify-content: space-between; + } + .cloud-options ha-select { + width: 48%; + } + ha-textarea { + width: 100%; + } + button.link { + color: var(--primary-color); + } + .card-actions { + display: flex; + justify-content: space-between; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-browse-media-tts": BrowseMediaTTS; + } +} diff --git a/src/components/media-player/ha-media-manage-button.ts b/src/components/media-player/ha-media-manage-button.ts new file mode 100644 index 0000000000..b67a0903b0 --- /dev/null +++ b/src/components/media-player/ha-media-manage-button.ts @@ -0,0 +1,69 @@ +import { mdiFolderEdit } from "@mdi/js"; +import "@material/mwc-button"; +import { css, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { MediaPlayerItem } from "../../data/media-player"; +import "../ha-svg-icon"; +import { isLocalMediaSourceContentId } from "../../data/media_source"; +import type { HomeAssistant } from "../../types"; +import { showMediaManageDialog } from "./show-media-manage-dialog"; +import { fireEvent } from "../../common/dom/fire_event"; + +declare global { + interface HASSDomEvents { + "media-refresh": unknown; + } +} + +@customElement("ha-media-manage-button") +class MediaManageButton extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() currentItem?: MediaPlayerItem; + + @state() _uploading = 0; + + protected render(): TemplateResult { + if ( + !this.currentItem || + !isLocalMediaSourceContentId(this.currentItem.media_content_id || "") + ) { + return html``; + } + return html` + + + + `; + } + + private _manage() { + showMediaManageDialog(this, { + currentItem: this.currentItem!, + onClose: () => fireEvent(this, "media-refresh"), + }); + } + + static styles = css` + mwc-button { + /* We use icon + text to show disabled state */ + --mdc-button-disabled-ink-color: --mdc-theme-primary; + } + + ha-svg-icon[slot="icon"], + ha-circular-progress[slot="icon"] { + vertical-align: middle; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-media-manage-button": MediaManageButton; + } +} diff --git a/src/components/media-player/ha-media-player-browse.ts b/src/components/media-player/ha-media-player-browse.ts index 6eccdd42a9..882673175e 100644 --- a/src/components/media-player/ha-media-player-browse.ts +++ b/src/components/media-player/ha-media-player-browse.ts @@ -1,9 +1,7 @@ import "@material/mwc-button/mwc-button"; import "@material/mwc-list/mwc-list"; import "@material/mwc-list/mwc-list-item"; -import { mdiPlay, mdiPlus } from "@mdi/js"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; +import { mdiArrowUpRight, mdiPlay, mdiPlus } from "@mdi/js"; import "@polymer/paper-tooltip/paper-tooltip"; import { css, @@ -50,11 +48,21 @@ import "../ha-icon-button"; import "../ha-svg-icon"; import "../ha-fab"; import { browseLocalMediaPlayer } from "../../data/media_source"; +import { isTTSMediaSource } from "../../data/tts"; +import type { TtsMediaPickedEvent } from "./ha-browse-media-tts"; +import "./ha-browse-media-tts"; declare global { interface HASSDomEvents { "media-picked": MediaPickedEvent; - "media-browsed": { ids: MediaPlayerItemId[]; current?: MediaPlayerItem }; + "media-browsed": { + // Items of the new browse stack + ids: MediaPlayerItemId[]; + // Current fetched item for this browse stack + current?: MediaPlayerItem; + // If the new stack should replace the old stack + replace?: boolean; + }; } } @@ -115,6 +123,24 @@ export class HaMediaPlayerBrowse extends LitElement { } } + public async refresh() { + const currentId = this.navigateIds[this.navigateIds.length - 1]; + try { + this._currentItem = await this._fetchData( + this.entityId, + currentId.media_content_id, + currentId.media_content_type + ); + // Update the parent with latest item. + fireEvent(this, "media-browsed", { + ids: this.navigateIds, + current: this._currentItem, + }); + } catch (err) { + this._setError(err); + } + } + public play(): void { if (this._currentItem?.can_play) { this._runAction(this._currentItem); @@ -137,10 +163,11 @@ export class HaMediaPlayerBrowse extends LitElement { const subtitle = this.hass.localize( `ui.components.media-browser.class.${currentItem.media_class}` ); - + const children = currentItem.children || []; const mediaClass = MediaClassBrowserSettings[currentItem.media_class]; - const childrenMediaClass = - MediaClassBrowserSettings[currentItem.children_media_class]; + const childrenMediaClass = currentItem.children_media_class + ? MediaClassBrowserSettings[currentItem.children_media_class] + : MediaClassBrowserSettings.directory; return html` ${ @@ -234,159 +261,191 @@ export class HaMediaPlayerBrowse extends LitElement { ${this._renderError(this._error)}
` - : currentItem.children?.length - ? childrenMediaClass.layout === "grid" - ? html` -
- ${currentItem.children.map( - (child) => html` -
- -
- ${child.thumbnail - ? html` -
- ` - : html` -
- -
- `} - ${child.can_play - ? html` - - ` - : ""} -
-
- ${child.title} - ${child.title} -
-
+ : isTTSMediaSource(currentItem.media_content_id) + ? html` + + ` + : !children.length && !currentItem.not_shown + ? html` +
+ ${currentItem.media_content_id === + "media-source://media_source/local/." + ? html` +
+ + + + + ${this.hass.localize( + "ui.components.media-browser.file_management.highlight_button" + )} +
` - )} -
- ` - : html` - - ${currentItem.children.map( - (child) => html` + : this.hass.localize( + "ui.components.media-browser.no_items" + )} +
+ ` + : childrenMediaClass.layout === "grid" + ? html` +
+ ${children.map( + (child) => html` +
+ +
+ ${child.thumbnail + ? html` +
+ ` + : html` +
+ +
+ `} + ${child.can_play + ? html` + + ` + : ""} +
+
+ ${child.title} + ${child.title} +
+
+
+ ` + )} + ${currentItem.not_shown + ? html` +
+
+ ${this.hass.localize( + "ui.components.media-browser.not_shown", + { count: currentItem.not_shown } + )} +
+
+ ` + : ""} +
+ ` + : html` + + ${children.map( + (child) => html` + +
+ +
+ ${child.title} +
+
  • + ` + )} + ${currentItem.not_shown + ? html` -
    + ${this.hass.localize( + "ui.components.media-browser.not_shown", + { count: currentItem.not_shown } )} - slot="graphic" - > - -
    - ${child.title} +
    -
  • ` - )} -
    - ` - : html` -
    - ${this.hass.localize( - "ui.components.media-browser.no_items" - )} -
    - ${currentItem.media_content_id === - "media-source://media_source/local/." - ? html`
    ${this.hass.localize( - "ui.components.media-browser.learn_adding_local_media", - "documentation", - html`${this.hass.localize( - "ui.components.media-browser.documentation" - )}` - )} -
    - ${this.hass.localize( - "ui.components.media-browser.local_media_files" - )}` : ""} -
    + ` }
    @@ -413,8 +472,8 @@ export class HaMediaPlayerBrowse extends LitElement { if (changedProps.has("entityId")) { this._setError(undefined); - } - if (!changedProps.has("navigateIds")) { + } else if (!changedProps.has("navigateIds")) { + // Neither entity ID or navigateIDs changed, nothing to fetch return; } @@ -423,6 +482,7 @@ export class HaMediaPlayerBrowse extends LitElement { const oldNavigateIds = changedProps.get("navigateIds") as | this["navigateIds"] | undefined; + const navigateIds = this.navigateIds; // We're navigating. Reset the shizzle. this._content?.scrollTo(0, 0); @@ -431,11 +491,9 @@ export class HaMediaPlayerBrowse extends LitElement { const oldParentItem = this._parentItem; this._currentItem = undefined; this._parentItem = undefined; - const currentId = this.navigateIds[this.navigateIds.length - 1]; + const currentId = navigateIds[navigateIds.length - 1]; const parentId = - this.navigateIds.length > 1 - ? this.navigateIds[this.navigateIds.length - 2] - : undefined; + navigateIds.length > 1 ? navigateIds[navigateIds.length - 2] : undefined; let currentProm: Promise | undefined; let parentProm: Promise | undefined; @@ -444,9 +502,9 @@ export class HaMediaPlayerBrowse extends LitElement { if ( // Check if we navigated to a child oldNavigateIds && - this.navigateIds.length > oldNavigateIds.length && + navigateIds.length === oldNavigateIds.length + 1 && oldNavigateIds.every((oldVal, idx) => { - const curVal = this.navigateIds[idx]; + const curVal = navigateIds[idx]; return ( curVal.media_content_id === oldVal.media_content_id && curVal.media_content_type === oldVal.media_content_type @@ -457,8 +515,8 @@ export class HaMediaPlayerBrowse extends LitElement { } else if ( // Check if we navigated to a parent oldNavigateIds && - this.navigateIds.length < oldNavigateIds.length && - this.navigateIds.every((curVal, idx) => { + navigateIds.length === oldNavigateIds.length - 1 && + navigateIds.every((curVal, idx) => { const oldVal = oldNavigateIds[idx]; return ( curVal.media_content_id === oldVal.media_content_id && @@ -481,11 +539,33 @@ export class HaMediaPlayerBrowse extends LitElement { (item) => { this._currentItem = item; fireEvent(this, "media-browsed", { - ids: this.navigateIds, + ids: navigateIds, current: item, }); }, - (err) => this._setError(err) + (err) => { + // When we change entity ID, we will first try to see if the new entity is + // able to resolve the new path. If that results in an error, browse the root. + const isNewEntityWithSamePath = + oldNavigateIds && + changedProps.has("entityId") && + navigateIds.length === oldNavigateIds.length && + oldNavigateIds.every( + (oldItem, idx) => + navigateIds[idx].media_content_id === oldItem.media_content_id && + navigateIds[idx].media_content_type === oldItem.media_content_type + ); + if (isNewEntityWithSamePath) { + fireEvent(this, "media-browsed", { + ids: [ + { media_content_id: undefined, media_content_type: undefined }, + ], + replace: true, + }); + } else { + this._setError(err); + } + } ); // Fetch parent if (!parentProm && parentId !== undefined) { @@ -521,7 +601,17 @@ export class HaMediaPlayerBrowse extends LitElement { } private _runAction(item: MediaPlayerItem): void { - fireEvent(this, "media-picked", { item }); + fireEvent(this, "media-picked", { item, navigateIds: this.navigateIds }); + } + + private _ttsPicked(ev: CustomEvent): void { + ev.stopPropagation(); + const navigateIds = this.navigateIds.slice(0, -1); + navigateIds.push(ev.detail.item); + fireEvent(this, "media-picked", { + ...ev.detail, + navigateIds, + }); } private async _childClicked(ev: MouseEvent): Promise { @@ -724,6 +814,18 @@ export class HaMediaPlayerBrowse extends LitElement { padding-left: 32px; } + .highlight-add-button { + display: flex; + flex-direction: row-reverse; + margin-right: 48px; + } + + .highlight-add-button ha-svg-icon { + position: relative; + top: -0.5em; + margin-left: 8px; + } + .content { overflow-y: auto; box-sizing: border-box; @@ -809,6 +911,17 @@ export class HaMediaPlayerBrowse extends LitElement { transition: height 0.5s, margin 0.5s; } + .not-shown { + font-style: italic; + color: var(--secondary-text-color); + } + + .grid.not-shown { + display: flex; + align-items: center; + text-align: center; + } + /* ============= CHILDREN ============= */ mwc-list { diff --git a/src/components/media-player/ha-media-upload-button.ts b/src/components/media-player/ha-media-upload-button.ts new file mode 100644 index 0000000000..65d7b36982 --- /dev/null +++ b/src/components/media-player/ha-media-upload-button.ts @@ -0,0 +1,129 @@ +import { mdiUpload } from "@mdi/js"; +import "@material/mwc-button"; +import { css, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../common/dom/fire_event"; +import { MediaPlayerItem } from "../../data/media-player"; +import "../ha-circular-progress"; +import "../ha-svg-icon"; +import { + isLocalMediaSourceContentId, + uploadLocalMedia, +} from "../../data/media_source"; +import type { HomeAssistant } from "../../types"; +import { showAlertDialog } from "../../dialogs/generic/show-dialog-box"; + +declare global { + interface HASSDomEvents { + uploading: unknown; + "media-refresh": unknown; + } +} + +@customElement("ha-media-upload-button") +class MediaUploadButton extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() currentItem?: MediaPlayerItem; + + @state() _uploading = 0; + + protected render(): TemplateResult { + if ( + !this.currentItem || + !isLocalMediaSourceContentId(this.currentItem.media_content_id || "") + ) { + return html``; + } + return html` + 0 + ? this.hass.localize( + "ui.components.media-browser.file_management.uploading", + { + count: this._uploading, + } + ) + : this.hass.localize( + "ui.components.media-browser.file_management.add_media" + )} + .disabled=${this._uploading > 0} + @click=${this._startUpload} + > + ${this._uploading > 0 + ? html` + + ` + : html` `} + + `; + } + + private async _startUpload() { + if (this._uploading > 0) { + return; + } + const input = document.createElement("input"); + input.type = "file"; + input.accept = "audio/*,video/*,image/*"; + input.multiple = true; + input.addEventListener( + "change", + async () => { + fireEvent(this, "uploading"); + const files = input.files!; + document.body.removeChild(input); + const target = this.currentItem!.media_content_id!; + + for (let i = 0; i < files.length; i++) { + this._uploading = files.length - i; + + try { + // eslint-disable-next-line no-await-in-loop + await uploadLocalMedia(this.hass, target, files[i]); + } catch (err: any) { + showAlertDialog(this, { + text: this.hass.localize( + "ui.components.media-browser.file_management.upload_failed", + { + reason: err.message || err, + } + ), + }); + break; + } + } + this._uploading = 0; + fireEvent(this, "media-refresh"); + }, + { once: true } + ); + // https://stackoverflow.com/questions/47664777/javascript-file-input-onchange-not-working-ios-safari-only + input.style.display = "none"; + document.body.append(input); + input.click(); + } + + static styles = css` + mwc-button { + /* We use icon + text to show disabled state */ + --mdc-button-disabled-ink-color: --mdc-theme-primary; + } + + ha-svg-icon[slot="icon"], + ha-circular-progress[slot="icon"] { + vertical-align: middle; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-media-upload-button": MediaUploadButton; + } +} diff --git a/src/components/media-player/show-media-browser-dialog.ts b/src/components/media-player/show-media-browser-dialog.ts index b84fd349c7..c99e8184b6 100644 --- a/src/components/media-player/show-media-browser-dialog.ts +++ b/src/components/media-player/show-media-browser-dialog.ts @@ -3,13 +3,13 @@ import { MediaPickedEvent, MediaPlayerBrowseAction, } from "../../data/media-player"; +import { MediaPlayerItemId } from "./ha-media-player-browse"; export interface MediaPlayerBrowseDialogParams { action: MediaPlayerBrowseAction; entityId: string; mediaPickedCallback: (pickedMedia: MediaPickedEvent) => void; - mediaContentId?: string; - mediaContentType?: string; + navigateIds?: MediaPlayerItemId[]; } export const showMediaBrowserDialog = ( diff --git a/src/components/media-player/show-media-manage-dialog.ts b/src/components/media-player/show-media-manage-dialog.ts new file mode 100644 index 0000000000..efb2c08c2d --- /dev/null +++ b/src/components/media-player/show-media-manage-dialog.ts @@ -0,0 +1,18 @@ +import { fireEvent } from "../../common/dom/fire_event"; +import { MediaPlayerItem } from "../../data/media-player"; + +export interface MediaManageDialogParams { + currentItem: MediaPlayerItem; + onClose?: () => void; +} + +export const showMediaManageDialog = ( + element: HTMLElement, + dialogParams: MediaManageDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-media-manage", + dialogImport: () => import("./dialog-media-manage"), + dialogParams, + }); +}; diff --git a/src/components/paper-time-input.js b/src/components/paper-time-input.js deleted file mode 100644 index 99e9c8de8f..0000000000 --- a/src/components/paper-time-input.js +++ /dev/null @@ -1,497 +0,0 @@ -/** -Adapted from paper-time-input from -https://github.com/ryanburns23/paper-time-input -MIT Licensed. Copyright (c) 2017 Ryan Burns - -`` Polymer element to accept a time with paper-input & paper-dropdown-menu -Inspired by the time input in google forms - -### Styling - -`` provides the following custom properties and mixins for styling: - -Custom property | Description | Default -----------------|-------------|---------- -`--paper-time-input-dropdown-ripple-color` | dropdown ripple color | `--primary-color` -`--paper-time-input-cotnainer` | Mixin applied to the inputs | `{}` -`--paper-time-dropdown-input-cotnainer` | Mixin applied to the dropdown input | `{}` -*/ -import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; -import "@polymer/paper-input/paper-input"; -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"; - -export class PaperTimeInput extends PolymerElement { - static get template() { - return html` - - - -
    - - - : - - - - - : - - - - - : - - - - - - - - - - AM - PM - - -
    - `; - } - - static get properties() { - return { - /** - * Label for the input - */ - label: { - type: String, - value: "Time", - }, - /** - * auto validate time inputs - */ - autoValidate: { - type: Boolean, - value: true, - }, - /** - * hides the label - */ - hideLabel: { - type: Boolean, - value: false, - }, - /** - * float the input labels - */ - floatInputLabels: { - type: Boolean, - value: false, - }, - /** - * always float the input labels - */ - alwaysFloatInputLabels: { - type: Boolean, - value: false, - }, - /** - * 12 or 24 hr format - */ - format: { - type: Number, - value: 12, - }, - /** - * disables the inputs - */ - disabled: { - type: Boolean, - value: false, - }, - /** - * hour - */ - hour: { - type: String, - notify: true, - }, - /** - * minute - */ - min: { - type: String, - notify: true, - }, - /** - * second - */ - sec: { - type: String, - notify: true, - }, - /** - * milli second - */ - millisec: { - type: String, - notify: true, - }, - /** - * Label for the hour input - */ - hourLabel: { - type: String, - value: "", - }, - /** - * Label for the min input - */ - minLabel: { - type: String, - value: "", - }, - /** - * Label for the sec input - */ - secLabel: { - type: String, - value: "", - }, - /** - * Label for the milli sec input - */ - millisecLabel: { - type: String, - value: "", - }, - /** - * show the sec field - */ - enableSecond: { - type: Boolean, - value: false, - }, - /** - * show the milli sec field - */ - enableMillisecond: { - type: Boolean, - value: false, - }, - /** - * limit hours input - */ - noHoursLimit: { - type: Boolean, - value: false, - }, - /** - * AM or PM - */ - amPm: { - type: String, - notify: true, - value: "AM", - }, - /** - * Formatted time string - */ - value: { - type: String, - notify: true, - readOnly: true, - computed: "_computeTime(min, hour, sec, millisec, amPm)", - }, - }; - } - - /** - * Validate the inputs - * @return {boolean} - */ - validate() { - let valid = true; - // Validate hour & min fields - if (!this.$.hour.validate() || !this.$.min.validate()) { - valid = false; - } - // Validate second field - if (this.enableSecond && !this.$.sec.validate()) { - valid = false; - } - // Validate milli second field - if (this.enableMillisecond && !this.$.millisec.validate()) { - valid = false; - } - // Validate AM PM if 12 hour time - if (this.format === 12 && !this.$.dropdown.validate()) { - valid = false; - } - return valid; - } - - /** - * Create time string - */ - _computeTime(min, hour, sec, millisec, amPm) { - let str; - if ( - hour || - min || - (sec && this.enableSecond) || - (millisec && this.enableMillisecond) - ) { - hour = hour || "00"; - min = min || "00"; - sec = sec || "00"; - millisec = millisec || "000"; - str = hour + ":" + min; - // add sec field - if (this.enableSecond && sec) { - str = str + ":" + sec; - } - // add milli sec field - if (this.enableMillisecond && millisec) { - str = str + ":" + millisec; - } - // No ampm on 24 hr time - if (this.format === 12) { - str = str + " " + amPm; - } - } - - return str; - } - - _onFocus(ev) { - ev.target.inputElement.inputElement.select(); - } - - /** - * Format milli sec - */ - _formatMillisec() { - if (this.millisec.toString().length === 1) { - this.millisec = this.millisec.toString().padStart(3, "0"); - } - } - - /** - * Format sec - */ - _formatSec() { - if (this.sec.toString().length === 1) { - this.sec = this.sec.toString().padStart(2, "0"); - } - } - - /** - * Format min - */ - _formatMin() { - if (this.min.toString().length === 1) { - this.min = this.min.toString().padStart(2, "0"); - } - } - - /** - * Format hour - */ - _shouldFormatHour() { - if (this.format === 24 && this.hour.toString().length === 1) { - this.hour = this.hour.toString().padStart(2, "0"); - } - } - - /** - * 24 hour format has a max hr of 23 - */ - _computeHourMax(format) { - if (this.noHoursLimit) { - return null; - } - if (format === 12) { - return format; - } - return 23; - } - - _equal(n1, n2) { - return n1 === n2; - } - - _computeClassNames(hasSuffix) { - return hasSuffix ? " " : "no-suffix"; - } -} - -customElements.define("paper-time-input", PaperTimeInput); diff --git a/src/components/user/ha-user-picker.ts b/src/components/user/ha-user-picker.ts index b12f1f20fd..0e7ae97466 100644 --- a/src/components/user/ha-user-picker.ts +++ b/src/components/user/ha-user-picker.ts @@ -1,8 +1,4 @@ -import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-item/paper-icon-item"; -import "@polymer/paper-item/paper-item-body"; -import "@polymer/paper-listbox/paper-listbox"; +import "@material/mwc-list/mwc-list-item"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { property } from "lit/decorators"; import memoizeOne from "memoize-one"; @@ -10,6 +6,7 @@ import { fireEvent } from "../../common/dom/fire_event"; import { stringCompare } from "../../common/string/compare"; import { fetchUsers, User } from "../../data/user"; import { HomeAssistant } from "../../types"; +import "../ha-select"; import "./ha-user-badge"; class HaUserPicker extends LitElement { @@ -37,34 +34,31 @@ class HaUserPicker extends LitElement { protected render(): TemplateResult { return html` - - - - ${this.noUserLabel || - this.hass?.localize("ui.components.user-picker.no_user")} - - ${this._sortedUsers(this.users).map( - (user) => html` - - - ${user.name} - - ` - )} - - + ${this.users?.length === 0 + ? html` + ${this.noUserLabel || + this.hass?.localize("ui.components.user-picker.no_user")} + ` + : ""} + ${this._sortedUsers(this.users).map( + (user) => html` + + + ${user.name} + + ` + )} + `; } @@ -78,10 +72,10 @@ class HaUserPicker extends LitElement { } private _userChanged(ev) { - const newValue = ev.detail.item.dataset.userId; + const newValue = ev.target.value; if (newValue !== this.value) { - this.value = ev.detail.value; + this.value = newValue; setTimeout(() => { fireEvent(this, "value-changed", { value: newValue }); fireEvent(this, "change"); @@ -94,15 +88,9 @@ class HaUserPicker extends LitElement { :host { display: inline-block; } - paper-dropdown-menu-light { + mwc-list { display: block; } - paper-listbox { - min-width: 200px; - } - paper-icon-item { - cursor: pointer; - } `; } } diff --git a/src/components/user/ha-users-picker.ts b/src/components/user/ha-users-picker.ts index d9c4ff148a..ceb972d178 100644 --- a/src/components/user/ha-users-picker.ts +++ b/src/components/user/ha-users-picker.ts @@ -75,7 +75,7 @@ class HaUsersPickerLight extends LitElement { ) )} + mediaContentId.startsWith(CAMERA_MEDIA_SOURCE_PREFIX); + +export const getEntityIdFromCameraMediaSource = (mediaContentId: string) => + mediaContentId.substring(CAMERA_MEDIA_SOURCE_PREFIX.length); diff --git a/src/data/cloud.ts b/src/data/cloud.ts index 4679d97a75..e8add4f4ef 100644 --- a/src/data/cloud.ts +++ b/src/data/cloud.ts @@ -47,6 +47,7 @@ export interface CloudPreferences { export interface CloudStatusLoggedIn { logged_in: true; cloud: "disconnected" | "connecting" | "connected"; + cloud_last_disconnect_reason: { clean: boolean; reason: string } | null; email: string; google_registered: boolean; google_entities: EntityFilter; @@ -186,10 +187,3 @@ export const updateCloudAlexaEntityConfig = ( entity_id: entityId, ...values, }); - -export interface CloudTTSInfo { - languages: Array<[string, string]>; -} - -export const getCloudTTSInfo = (hass: HomeAssistant) => - hass.callWS({ type: "cloud/tts/info" }); diff --git a/src/data/cloud/tts.ts b/src/data/cloud/tts.ts new file mode 100644 index 0000000000..02fe969e21 --- /dev/null +++ b/src/data/cloud/tts.ts @@ -0,0 +1,70 @@ +import { caseInsensitiveStringCompare } from "../../common/string/compare"; +import { LocalizeFunc } from "../../common/translations/localize"; +import { translationMetadata } from "../../resources/translations-metadata"; +import { HomeAssistant } from "../../types"; + +export interface CloudTTSInfo { + languages: Array<[string, string]>; +} + +export const getCloudTTSInfo = (hass: HomeAssistant) => + hass.callWS({ type: "cloud/tts/info" }); + +export const getCloudTtsLanguages = (info?: CloudTTSInfo) => { + const languages: Array<[string, string]> = []; + + if (!info) { + return languages; + } + + const seen = new Set(); + for (const [lang] of info.languages) { + if (seen.has(lang)) { + continue; + } + seen.add(lang); + + let label = lang; + + if (lang in translationMetadata.translations) { + label = translationMetadata.translations[lang].nativeName; + } else { + const [langFamily, dialect] = lang.split("-"); + if (langFamily in translationMetadata.translations) { + label = `${translationMetadata.translations[langFamily].nativeName}`; + + if (langFamily.toLowerCase() !== dialect.toLowerCase()) { + label += ` (${dialect})`; + } + } + } + + languages.push([lang, label]); + } + return languages.sort((a, b) => caseInsensitiveStringCompare(a[1], b[1])); +}; + +export const getCloudTtsSupportedGenders = ( + language: string, + info: CloudTTSInfo | undefined, + localize: LocalizeFunc +) => { + const genders: Array<[string, string]> = []; + + if (!info) { + return genders; + } + + for (const [curLang, gender] of info.languages) { + if (curLang === language) { + genders.push([ + gender, + localize(`ui.panel.media-browser.tts.gender_${gender}`) || + localize(`ui.panel.config.cloud.account.tts.${gender}`) || + gender, + ]); + } + } + + return genders.sort((a, b) => caseInsensitiveStringCompare(a[1], b[1])); +}; diff --git a/src/data/config.ts b/src/data/config.ts new file mode 100644 index 0000000000..281e2debbf --- /dev/null +++ b/src/data/config.ts @@ -0,0 +1,19 @@ +import { HomeAssistant } from "../types"; + +interface ValidationResult { + valid: boolean; + error: string | null; +} + +type ValidKeys = "trigger" | "action" | "condition"; + +export const validateConfig = < + T extends Partial<{ [key in ValidKeys]: unknown }> +>( + hass: HomeAssistant, + config: T +): Promise> => + hass.callWS({ + type: "validate_config", + ...config, + }); diff --git a/src/data/config_entries.ts b/src/data/config_entries.ts index 78cbe6105a..f2e84ddac0 100644 --- a/src/data/config_entries.ts +++ b/src/data/config_entries.ts @@ -13,6 +13,7 @@ export interface ConfigEntry { | "not_loaded" | "failed_unload"; supports_options: boolean; + supports_remove_device: boolean; supports_unload: boolean; pref_disable_new_entities: boolean; pref_disable_polling: boolean; diff --git a/src/data/config_flow.ts b/src/data/config_flow.ts index 92352fafc9..39019393c5 100644 --- a/src/data/config_flow.ts +++ b/src/data/config_flow.ts @@ -13,6 +13,7 @@ export const DISCOVERY_SOURCES = [ "ssdp", "zeroconf", "discovery", + "integration_discovery", "mqtt", "hassio", ]; diff --git a/src/data/device_registry.ts b/src/data/device_registry.ts index 65c0bcec56..6fb72404aa 100644 --- a/src/data/device_registry.ts +++ b/src/data/device_registry.ts @@ -1,4 +1,5 @@ import { Connection, createCollection } from "home-assistant-js-websocket"; +import { Store } from "home-assistant-js-websocket/dist/store"; import { computeStateName } from "../common/entity/compute_state_name"; import { caseInsensitiveStringCompare } from "../common/string/compare"; import { debounce } from "../common/util/debounce"; @@ -77,12 +78,26 @@ export const updateDeviceRegistryEntry = ( ...updates, }); -export const fetchDeviceRegistry = (conn) => - conn.sendMessagePromise({ +export const removeConfigEntryFromDevice = ( + hass: HomeAssistant, + deviceId: string, + configEntryId: string +) => + hass.callWS({ + type: "config/device_registry/remove_config_entry", + device_id: deviceId, + config_entry_id: configEntryId, + }); + +export const fetchDeviceRegistry = (conn: Connection) => + conn.sendMessagePromise({ type: "config/device_registry/list", }); -const subscribeDeviceRegistryUpdates = (conn, store) => +const subscribeDeviceRegistryUpdates = ( + conn: Connection, + store: Store +) => conn.subscribeEvents( debounce( () => diff --git a/src/data/hassio/addon.ts b/src/data/hassio/addon.ts index ccd099edc7..0cead4ee9a 100644 --- a/src/data/hassio/addon.ts +++ b/src/data/hassio/addon.ts @@ -84,9 +84,10 @@ export interface HassioAddonDetails extends HassioAddonInfo { options: Record; privileged: any; protected: boolean; - rating: "1-6"; + rating: "1-8"; schema: HaFormSchema[] | null; services_role: string[]; + signed: boolean; slug: string; startup: AddonStartup; stdin: boolean; diff --git a/src/data/media-player.ts b/src/data/media-player.ts index 48f3351dbb..14b1a19c80 100644 --- a/src/data/media-player.ts +++ b/src/data/media-player.ts @@ -28,11 +28,13 @@ import type { HassEntityBase, } from "home-assistant-js-websocket"; import { supportsFeature } from "../common/entity/supports-feature"; +import { MediaPlayerItemId } from "../components/media-player/ha-media-player-browse"; import type { HomeAssistant } from "../types"; import { UNAVAILABLE_STATES } from "./entity"; interface MediaPlayerEntityAttributes extends HassEntityAttributeBase { - media_content_type?: any; + media_content_id?: string; + media_content_type?: string; media_artist?: string; media_playlist?: string; media_series_title?: string; @@ -147,6 +149,7 @@ export const MediaClassBrowserSettings: { export interface MediaPickedEvent { item: MediaPlayerItem; + navigateIds: MediaPlayerItemId[]; } export interface MediaPlayerThumbnail { @@ -165,11 +168,12 @@ export interface MediaPlayerItem { media_content_type: string; media_content_id: string; media_class: string; - children_media_class: string; + children_media_class?: string; can_play: boolean; can_expand: boolean; thumbnail?: string; children?: MediaPlayerItem[]; + not_shown?: number; } export const browseMediaPlayer = ( @@ -261,8 +265,10 @@ export const computeMediaControls = ( }); } + const assumedState = stateObj.attributes.assumed_state === true; + if ( - (state === "playing" || state === "paused") && + (state === "playing" || state === "paused" || assumedState) && supportsFeature(stateObj, SUPPORT_PREVIOUS_TRACK) ) { buttons.push({ @@ -272,14 +278,15 @@ export const computeMediaControls = ( } if ( - (state === "playing" && + !assumedState && + ((state === "playing" && (supportsFeature(stateObj, SUPPORT_PAUSE) || supportsFeature(stateObj, SUPPORT_STOP))) || - ((state === "paused" || state === "idle") && - supportsFeature(stateObj, SUPPORT_PLAY)) || - (state === "on" && - (supportsFeature(stateObj, SUPPORT_PLAY) || - supportsFeature(stateObj, SUPPORT_PAUSE))) + ((state === "paused" || state === "idle") && + supportsFeature(stateObj, SUPPORT_PLAY)) || + (state === "on" && + (supportsFeature(stateObj, SUPPORT_PLAY) || + supportsFeature(stateObj, SUPPORT_PAUSE)))) ) { buttons.push({ icon: @@ -299,8 +306,29 @@ export const computeMediaControls = ( }); } + if (assumedState && supportsFeature(stateObj, SUPPORT_PLAY)) { + buttons.push({ + icon: mdiPlay, + action: "media_play", + }); + } + + if (assumedState && supportsFeature(stateObj, SUPPORT_PAUSE)) { + buttons.push({ + icon: mdiPause, + action: "media_pause", + }); + } + + if (assumedState && supportsFeature(stateObj, SUPPORT_STOP)) { + buttons.push({ + icon: mdiStop, + action: "media_stop", + }); + } + if ( - (state === "playing" || state === "paused") && + (state === "playing" || state === "paused" || assumedState) && supportsFeature(stateObj, SUPPORT_NEXT_TRACK) ) { buttons.push({ @@ -313,7 +341,7 @@ export const computeMediaControls = ( }; export const formatMediaTime = (seconds: number | undefined): string => { - if (seconds === undefined) { + if (seconds === undefined || seconds === Infinity) { return ""; } @@ -333,3 +361,17 @@ export const cleanupMediaTitle = (title?: string): string | undefined => { const index = title.indexOf("?authSig="); return index > 0 ? title.slice(0, index) : title; }; + +/** + * Set volume of a media player entity. + * @param hass Home Assistant object + * @param entity_id entity ID of media player + * @param volume_level number between 0..1 + * @returns + */ +export const setMediaPlayerVolume = ( + hass: HomeAssistant, + entity_id: string, + volume_level: number +) => + hass.callService("media_player", "volume_set", { entity_id, volume_level }); diff --git a/src/data/media_source.ts b/src/data/media_source.ts index 759be2a7d3..61494ee3f8 100644 --- a/src/data/media_source.ts +++ b/src/data/media_source.ts @@ -23,3 +23,38 @@ export const browseLocalMediaPlayer = ( type: "media_source/browse_media", media_content_id: mediaContentId, }); + +export const isLocalMediaSourceContentId = (mediaId: string) => + mediaId.startsWith("media-source://media_source"); + +export const uploadLocalMedia = async ( + hass: HomeAssistant, + media_content_id: string, + file: File +) => { + const fd = new FormData(); + fd.append("media_content_id", media_content_id); + fd.append("file", file); + const resp = await hass.fetchWithAuth( + "/api/media_source/local_source/upload", + { + method: "POST", + body: fd, + } + ); + if (resp.status === 413) { + throw new Error("Uploaded image is too large"); + } else if (resp.status !== 200) { + throw new Error("Unknown error"); + } + return resp.json(); +}; + +export const removeLocalMedia = async ( + hass: HomeAssistant, + media_content_id: string +) => + hass.callWS({ + type: "media_source/local_source/remove", + media_content_id, + }); diff --git a/src/data/mqtt.ts b/src/data/mqtt.ts index 9c744085fd..2cc710fd51 100644 --- a/src/data/mqtt.ts +++ b/src/data/mqtt.ts @@ -22,6 +22,7 @@ export interface MQTTEntityDebugInfo { entity_id: string; discovery_data: MQTTDiscoveryDebugInfo; subscriptions: MQTTTopicDebugInfo[]; + transmitted: MQTTTopicDebugInfo[]; } export interface MQTTTriggerDebugInfo { @@ -43,15 +44,6 @@ export const subscribeMQTTTopic = ( topic, }); -export const removeMQTTDeviceEntry = ( - hass: HomeAssistant, - deviceId: string -): Promise => - hass.callWS({ - type: "mqtt/device/remove", - device_id: deviceId, - }); - export const fetchMQTTDebugInfo = ( hass: HomeAssistant, deviceId: string diff --git a/src/data/script.ts b/src/data/script.ts index 6b83157bde..41db0160c1 100644 --- a/src/data/script.ts +++ b/src/data/script.ts @@ -3,6 +3,17 @@ import { HassEntityBase, HassServiceTarget, } from "home-assistant-js-websocket"; +import { + object, + optional, + string, + union, + array, + assign, + literal, + is, + Describe, +} from "superstruct"; import { computeObjectId } from "../common/entity/compute_object_id"; import { navigate } from "../common/navigate"; import { HomeAssistant } from "../types"; @@ -12,6 +23,48 @@ import { BlueprintInput } from "./blueprint"; export const MODES = ["single", "restart", "queued", "parallel"] as const; export const MODES_MAX = ["queued", "parallel"]; +export const baseActionStruct = object({ + alias: optional(string()), +}); + +const targetStruct = object({ + entity_id: optional(union([string(), array(string())])), + device_id: optional(union([string(), array(string())])), + area_id: optional(union([string(), array(string())])), +}); + +export const serviceActionStruct: Describe = assign( + baseActionStruct, + object({ + service: optional(string()), + service_template: optional(string()), + entity_id: optional(string()), + target: optional(targetStruct), + data: optional(object()), + }) +); + +const playMediaActionStruct: Describe = assign( + baseActionStruct, + object({ + service: literal("media_player.play_media"), + target: optional(object({ entity_id: optional(string()) })), + entity_id: optional(string()), + data: object({ media_content_id: string(), media_content_type: string() }), + metadata: object(), + }) +); + +const activateSceneActionStruct: Describe = assign( + baseActionStruct, + object({ + service: literal("scene.turn_on"), + target: optional(object({ entity_id: optional(string()) })), + entity_id: optional(string()), + metadata: object(), + }) +); + export interface ScriptEntity extends HassEntityBase { attributes: HassEntityAttributeBase & { last_triggered: string; @@ -48,11 +101,12 @@ export interface ServiceAction { service_template?: string; entity_id?: string; target?: HassServiceTarget; - data?: Record; + data?: Record; } export interface DeviceAction { alias?: string; + type: string; device_id: string; domain: string; entity_id: string; @@ -70,10 +124,18 @@ export interface DelayAction { delay: number | Partial | string; } -export interface SceneAction { +export interface ServiceSceneAction { + alias?: string; + service: "scene.turn_on"; + target?: { entity_id?: string }; + entity_id?: string; + metadata: Record; +} +export interface LegacySceneAction { alias?: string; scene: string; } +export type SceneAction = ServiceSceneAction | LegacySceneAction; export interface WaitAction { alias?: string; @@ -89,6 +151,15 @@ export interface WaitForTriggerAction { continue_on_timeout?: boolean; } +export interface PlayMediaAction { + alias?: string; + service: "media_player.play_media"; + target?: { entity_id?: string }; + entity_id?: string; + data: { media_content_id: string; media_content_type: string }; + metadata: Record; +} + export interface RepeatAction { alias?: string; repeat: CountRepeat | WhileRepeat | UntilRepeat; @@ -145,6 +216,7 @@ export type Action = | RepeatAction | ChooseAction | VariablesAction + | PlayMediaAction | UnknownAction; export interface ActionTypes { @@ -159,6 +231,7 @@ export interface ActionTypes { wait_for_trigger: WaitForTriggerAction; variables: VariablesAction; service: ServiceAction; + play_media: PlayMediaAction; unknown: UnknownAction; } @@ -233,6 +306,14 @@ export const getActionType = (action: Action): ActionType => { return "variables"; } if ("service" in action) { + if ("metadata" in action) { + if (is(action, activateSceneActionStruct)) { + return "activate_scene"; + } + if (is(action, playMediaActionStruct)) { + return "play_media"; + } + } return "service"; } return "unknown"; diff --git a/src/data/script_i18n.ts b/src/data/script_i18n.ts index 338cb7c234..230c867ef0 100644 --- a/src/data/script_i18n.ts +++ b/src/data/script_i18n.ts @@ -9,8 +9,10 @@ import { ActionType, ActionTypes, DelayAction, + DeviceAction, EventAction, getActionType, + PlayMediaAction, SceneAction, VariablesAction, WaitForTriggerAction, @@ -104,9 +106,30 @@ export const describeAction = ( if (actionType === "activate_scene") { const config = action as SceneAction; - const sceneStateObj = hass.states[config.scene]; + let entityId: string | undefined; + if ("scene" in config) { + entityId = config.scene; + } else { + entityId = config.target?.entity_id || config.entity_id; + } + const sceneStateObj = entityId ? hass.states[entityId] : undefined; return `Activate scene ${ - sceneStateObj ? computeStateName(sceneStateObj) : config.scene + sceneStateObj + ? computeStateName(sceneStateObj) + : "scene" in config + ? config.scene + : config.target?.entity_id || config.entity_id + }`; + } + + if (actionType === "play_media") { + const config = action as PlayMediaAction; + const entityId = config.target?.entity_id || config.entity_id; + const mediaStateObj = entityId ? hass.states[entityId] : undefined; + return `Play ${config.metadata.title || config.data.media_content_id} on ${ + mediaStateObj + ? computeStateName(mediaStateObj) + : config.target?.entity_id || config.entity_id }`; } @@ -138,5 +161,13 @@ export const describeAction = ( return `Test ${describeCondition(action as Condition)}`; } + if (actionType === "device_action") { + const config = action as DeviceAction; + const stateObj = hass.states[config.entity_id as string]; + return `${config.type || "Perform action with"} ${ + stateObj ? computeStateName(stateObj) : config.entity_id + }`; + } + return actionType; }; diff --git a/src/data/selector.ts b/src/data/selector.ts index 3bb2999877..d8cb9a4e63 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -1,7 +1,9 @@ export type Selector = | AddonSelector + | AttributeSelector | EntitySelector | DeviceSelector + | DurationSelector | AreaSelector | TargetSelector | NumberSelector @@ -10,15 +12,25 @@ export type Selector = | ActionSelector | StringSelector | ObjectSelector - | SelectSelector; + | SelectSelector + | IconSelector + | MediaSelector + | ThemeSelector; + export interface EntitySelector { entity: { integration?: string; - domain?: string; + domain?: string | string[]; device_class?: string; }; } +export interface AttributeSelector { + attribute: { + entity_id: string; + }; +} + export interface DeviceSelector { device: { integration?: string; @@ -31,6 +43,11 @@ export interface DeviceSelector { }; } +export interface DurationSelector { + // eslint-disable-next-line @typescript-eslint/ban-types + duration: {}; +} + export interface AddonSelector { addon: { name?: string; @@ -70,8 +87,8 @@ export interface TargetSelector { export interface NumberSelector { number: { - min: number; - max: number; + min?: number; + max?: number; step?: number; mode?: "box" | "slider"; unit_of_measurement?: string; @@ -95,7 +112,22 @@ export interface ActionSelector { export interface StringSelector { text: { - multiline: boolean; + multiline?: boolean; + type?: + | "number" + | "text" + | "search" + | "tel" + | "url" + | "email" + | "password" + | "date" + | "month" + | "week" + | "time" + | "datetime-local" + | "color"; + suffix?: string; }; } @@ -104,8 +136,43 @@ export interface ObjectSelector { object: {}; } +export interface SelectOption { + value: string; + label: string; +} + export interface SelectSelector { select: { - options: string[]; + options: string[] | SelectOption[]; + }; +} + +export interface IconSelector { + icon: { + placeholder?: string; + fallbackPath?: string; + }; +} + +export interface ThemeSelector { + // eslint-disable-next-line @typescript-eslint/ban-types + theme: {}; +} + +export interface MediaSelector { + // eslint-disable-next-line @typescript-eslint/ban-types + media: {}; +} + +export interface MediaSelectorValue { + entity_id?: string; + media_content_id?: string; + media_content_type?: string; + metadata?: { + title?: string; + thumbnail?: string | null; + media_class?: string; + children_media_class?: string | null; + navigateIds?: { media_content_type: string; media_content_id: string }[]; }; } diff --git a/src/data/tasmota.ts b/src/data/tasmota.ts deleted file mode 100644 index df9aad3032..0000000000 --- a/src/data/tasmota.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { HomeAssistant } from "../types"; - -export const removeTasmotaDeviceEntry = ( - hass: HomeAssistant, - deviceId: string -): Promise => - hass.callWS({ - type: "tasmota/device/remove", - device_id: deviceId, - }); diff --git a/src/data/tts.ts b/src/data/tts.ts index e4469ffb24..f4d0d1346b 100644 --- a/src/data/tts.ts +++ b/src/data/tts.ts @@ -10,3 +10,11 @@ export const convertTextToSpeech = ( options?: Record; } ) => hass.callApi<{ url: string; path: string }>("POST", "tts_get_url", data); + +const TTS_MEDIA_SOURCE_PREFIX = "media-source://tts/"; + +export const isTTSMediaSource = (mediaContentId: string) => + mediaContentId.startsWith(TTS_MEDIA_SOURCE_PREFIX); + +export const getProviderFromTTSMediaSource = (mediaContentId: string) => + mediaContentId.substring(TTS_MEDIA_SOURCE_PREFIX.length); diff --git a/src/data/zwave_js.ts b/src/data/zwave_js.ts index 4940cd2d92..1cc8653752 100644 --- a/src/data/zwave_js.ts +++ b/src/data/zwave_js.ts @@ -2,6 +2,19 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { HomeAssistant } from "../types"; import { DeviceRegistryEntry } from "./device_registry"; +export enum InclusionState { + /** The controller isn't doing anything regarding inclusion. */ + Idle, + /** The controller is waiting for a node to be included. */ + Including, + /** The controller is waiting for a node to be excluded. */ + Excluding, + /** The controller is busy including or excluding a node. */ + Busy, + /** The controller listening for SmartStart nodes to announce themselves. */ + SmartStart, +} + export const enum InclusionStrategy { /** * Always uses Security S2 if supported, otherwise uses Security S0 for certain devices which don't work without encryption and uses no encryption otherwise. @@ -106,16 +119,33 @@ export interface ZWaveJSNetwork { } export interface ZWaveJSClient { - state: string; + state: "connected" | "disconnected"; ws_server_url: string; server_version: string; driver_version: string; } export interface ZWaveJSController { - home_id: string; - nodes: number[]; + home_id: number; + library_version: string; + type: number; + own_node_id: number; + is_secondary: boolean; + is_using_home_id_from_other_network: boolean; + is_sis_present: boolean; + was_real_primary: boolean; + is_static_update_controller: boolean; + is_slave: boolean; + serial_api_version: string; + manufacturer_id: number; + product_id: number; + product_type: number; + supported_function_types: number[]; + suc_node_id: number; + supports_timers: boolean; is_heal_network_active: boolean; + inclusion_state: InclusionState; + nodes: number[]; } export interface ZWaveJSNodeStatus { @@ -126,6 +156,7 @@ export interface ZWaveJSNodeStatus { is_routing: boolean | null; zwave_plus_version: number | null; highest_security_class: SecurityClass | null; + is_controller_node: boolean; } export interface ZwaveJSNodeMetadata { @@ -308,6 +339,12 @@ export const stopZwaveInclusion = (hass: HomeAssistant, entry_id: string) => entry_id, }); +export const stopZwaveExclusion = (hass: HomeAssistant, entry_id: string) => + hass.callWS({ + type: "zwave_js/stop_exclusion", + entry_id, + }); + export const zwaveGrantSecurityClasses = ( hass: HomeAssistant, entry_id: string, diff --git a/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts b/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts index f370a9b00c..aaced030d6 100644 --- a/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts +++ b/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts @@ -83,6 +83,7 @@ class DialogConfigEntrySystemOptions extends LitElement { .checked=${!this._disableNewEntities} @change=${this._disableNewEntitiesChanged} .disabled=${this._submitting} + dialogInitialFocus > ${this._allowUpdatePolling() diff --git a/src/dialogs/config-flow/step-flow-create-entry.ts b/src/dialogs/config-flow/step-flow-create-entry.ts index f2f1c36a53..6431bcc48b 100644 --- a/src/dialogs/config-flow/step-flow-create-entry.ts +++ b/src/dialogs/config-flow/step-flow-create-entry.ts @@ -1,7 +1,4 @@ import "@material/mwc-button"; -import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; @@ -122,13 +119,6 @@ class StepFlowCreateEntry extends LitElement { .buttons > *:last-child { margin-left: auto; } - paper-dropdown-menu-light { - cursor: pointer; - } - paper-item { - cursor: pointer; - white-space: nowrap; - } @media all and (max-width: 450px), all and (max-height: 500px) { .device { width: 100%; diff --git a/src/dialogs/config-flow/step-flow-form.ts b/src/dialogs/config-flow/step-flow-form.ts index a1969dc460..59e3656b1b 100644 --- a/src/dialogs/config-flow/step-flow-form.ts +++ b/src/dialogs/config-flow/step-flow-form.ts @@ -47,6 +47,7 @@ class StepFlowForm extends LitElement { ? html`${this._errorMsg}` : ""} !field.optional) === undefined + this.step.data_schema.find((field) => field.required) === undefined : // If data is filled in, make sure all required fields are stepData && this.step.data_schema.every( (field) => - field.optional || !["", undefined].includes(stepData![field.name]) + !field.required || + !["", undefined].includes(stepData![field.name]) ); if (!allRequiredInfoFilledIn) { diff --git a/src/dialogs/config-flow/step-flow-pick-handler.ts b/src/dialogs/config-flow/step-flow-pick-handler.ts index 38257e66aa..d40b8dad64 100644 --- a/src/dialogs/config-flow/step-flow-pick-handler.ts +++ b/src/dialogs/config-flow/step-flow-pick-handler.ts @@ -1,23 +1,27 @@ -import "@polymer/paper-item/paper-icon-item"; -import "@polymer/paper-item/paper-item-body"; +import "@material/mwc-list/mwc-list"; +import "@material/mwc-list/mwc-list-item"; import Fuse from "fuse.js"; import { css, CSSResultGroup, html, LitElement, - TemplateResult, PropertyValues, + TemplateResult, } from "lit"; import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; +import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { fireEvent } from "../../common/dom/fire_event"; +import { navigate } from "../../common/navigate"; import "../../common/search/search-input"; import { caseInsensitiveStringCompare } from "../../common/string/compare"; import { LocalizeFunc } from "../../common/translations/localize"; import "../../components/ha-icon-next"; +import { getConfigEntries } from "../../data/config_entries"; import { domainToName } from "../../data/integration"; +import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node"; import { HomeAssistant } from "../../types"; import { brandsUrl } from "../../util/brands-url"; import { documentationUrl } from "../../util/documentation-url"; @@ -26,6 +30,7 @@ import { configFlowContentStyles } from "./styles"; interface HandlerObj { name: string; slug: string; + is_add?: boolean; } declare global { @@ -77,6 +82,17 @@ class StepFlowPickHandler extends LitElement { protected render(): TemplateResult { const handlers = this._getHandlers(); + const addDeviceRows: HandlerObj[] = ["zha", "zwave_js"] + .filter((domain) => isComponentLoaded(this.hass, domain)) + .map((domain) => ({ + name: this.hass.localize( + `ui.panel.config.integrations.add_${domain}_device` + ), + slug: domain, + is_add: true, + })) + .sort((a, b) => caseInsensitiveStringCompare(a.name, b.name)); + return html`

    ${this.hass.localize("ui.panel.config.integrations.new")}

    -
    + ${addDeviceRows.length + ? html` + ${addDeviceRows.map((handler) => this._renderRow(handler))} + + ` + : ""} ${handlers.length - ? handlers.map( - (handler: HandlerObj) => - html` - - - - ${handler.name} - - - ` - ) + ? handlers.map((handler) => this._renderRow(handler)) : html`

    ${this.hass.localize( @@ -140,15 +139,56 @@ class StepFlowPickHandler extends LitElement { >.

    `} -
    + + `; + } + + private _renderRow(handler: HandlerObj) { + return html` + + + ${handler.name} + ${handler.is_add ? "" : html``} + `; } public willUpdate(changedProps: PropertyValues): void { + super.willUpdate(changedProps); if (this._filter === undefined && this.initialFilter !== undefined) { this._filter = this.initialFilter; } - super.willUpdate(changedProps); + if (this.initialFilter !== undefined && this._filter === "") { + this.initialFilter = undefined; + this._filter = ""; + this._width = undefined; + this._height = undefined; + } else if ( + this.hasUpdated && + changedProps.has("_filter") && + (!this._width || !this._height) + ) { + // Store the width and height so that when we search, box doesn't jump + const boundingRect = + this.shadowRoot!.querySelector("mwc-list")!.getBoundingClientRect(); + this._width = boundingRect.width; + this._height = boundingRect.height; + } } protected firstUpdated(changedProps) { @@ -159,24 +199,6 @@ class StepFlowPickHandler extends LitElement { ); } - protected updated(changedProps) { - super.updated(changedProps); - // Store the width and height so that when we search, box doesn't jump - const div = this.shadowRoot!.querySelector("div")!; - if (!this._width) { - const width = div.clientWidth; - if (width) { - this._width = width; - } - } - if (!this._height) { - const height = div.clientHeight; - if (height) { - this._height = height; - } - } - } - private _getHandlers() { return this._filterHandlers( this.handlers, @@ -190,8 +212,31 @@ class StepFlowPickHandler extends LitElement { } private async _handlerPicked(ev) { + const handler: HandlerObj = ev.currentTarget.handler; + + if (handler.is_add) { + if (handler.slug === "zwave_js") { + const entries = await getConfigEntries(this.hass); + const entry = entries.find((ent) => ent.domain === "zwave_js"); + + if (!entry) { + return; + } + + showZWaveJSAddNodeDialog(this, { + entry_id: entry.entry_id, + }); + } else if (handler.slug === "zha") { + navigate("/config/zha/add"); + } + + // This closes dialog. + fireEvent(this, "flow-update"); + return; + } + fireEvent(this, "handler-picked", { - handler: ev.currentTarget.handler.slug, + handler: handler.slug, }); } @@ -219,27 +264,26 @@ class StepFlowPickHandler extends LitElement { } search-input { display: block; - margin: -12px 16px 0; + margin: 16px 16px 0; } ha-icon-next { margin-right: 8px; } - div { + mwc-list { overflow: auto; max-height: 600px; } + .divider { + border-bottom-color: var(--divider-color); + } h2 { padding-right: 66px; } @media all and (max-height: 900px) { - div { + mwc-list { max-height: calc(100vh - 134px); } } - paper-icon-item { - cursor: pointer; - margin-bottom: 4px; - } p { text-align: center; padding: 16px; diff --git a/src/dialogs/generic/dialog-box.ts b/src/dialogs/generic/dialog-box.ts index 4fc1c09cde..d57508e3cc 100644 --- a/src/dialogs/generic/dialog-box.ts +++ b/src/dialogs/generic/dialog-box.ts @@ -1,12 +1,12 @@ import "@material/mwc-button/mwc-button"; -import "@polymer/paper-input/paper-input"; +import { mdiAlertOutline } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { classMap } from "lit/directives/class-map"; import { fireEvent } from "../../common/dom/fire_event"; import "../../components/ha-dialog"; +import "../../components/ha-svg-icon"; import "../../components/ha-switch"; -import { PolymerChangedEvent } from "../../polymer-types"; +import "../../components/ha-textfield"; import { haStyleDialog } from "../../resources/styles"; import { HomeAssistant } from "../../types"; import { DialogBoxParams } from "./show-dialog-box"; @@ -51,38 +51,40 @@ class DialogBox extends LitElement { ?escapeKeyAction=${confirmPrompt} @closed=${this._dialogClosed} defaultAction="ignore" - .heading=${this._params.title + .heading=${html`${this._params.warning + ? html` ` + : ""}${this._params.title ? this._params.title : this._params.confirmation && - this.hass.localize("ui.dialogs.generic.default_confirmation_title")} + this.hass.localize( + "ui.dialogs.generic.default_confirmation_title" + )}`} >
    ${this._params.text ? html` -

    +

    ${this._params.text}

    ` : ""} ${this._params.prompt ? html` - + > ` : ""}
    @@ -107,8 +109,8 @@ class DialogBox extends LitElement { `; } - private _valueChanged(ev: PolymerChangedEvent) { - this._value = ev.detail.value; + private _valueChanged(ev) { + this._value = ev.target.value; } private _dismiss(): void { @@ -173,9 +175,6 @@ class DialogBox extends LitElement { /* Place above other dialogs */ --dialog-z-index: 104; } - .warning { - color: var(--warning-color); - } `, ]; } diff --git a/src/dialogs/more-info/controls/more-info-alarm_control_panel.js b/src/dialogs/more-info/controls/more-info-alarm_control_panel.js deleted file mode 100644 index 99be49908f..0000000000 --- a/src/dialogs/more-info/controls/more-info-alarm_control_panel.js +++ /dev/null @@ -1,281 +0,0 @@ -import "@material/mwc-button"; -import "@polymer/iron-flex-layout/iron-flex-layout-classes"; -import "@polymer/paper-input/paper-input"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { fireEvent } from "../../../common/dom/fire_event"; -import { FORMAT_NUMBER } from "../../../data/alarm_control_panel"; -import LocalizeMixin from "../../../mixins/localize-mixin"; - -class MoreInfoAlarmControlPanel extends LocalizeMixin(PolymerElement) { - static get template() { - return html` - - - - - -
    - - -
    - `; - } - - static get properties() { - return { - hass: Object, - stateObj: { - type: Object, - observer: "_stateObjChanged", - }, - _enteredCode: { - type: String, - value: "", - }, - _codeFormat: { - type: String, - value: "", - }, - _codeValid: { - type: Boolean, - computed: - "_validateCode(_enteredCode, _codeFormat, _armVisible, _codeArmRequired)", - }, - _disarmVisible: { - type: Boolean, - value: false, - }, - _armVisible: { - type: Boolean, - value: false, - }, - _inputEnabled: { - type: Boolean, - value: false, - }, - _inputMode: { - type: String, - computed: "_getInputMode(_codeFormat)", - }, - }; - } - - constructor() { - super(); - this._armedStates = [ - "armed_home", - "armed_away", - "armed_night", - "armed_custom_bypass", - ]; - } - - _stateObjChanged(newVal, oldVal) { - if (newVal) { - const state = newVal.state; - const props = { - _codeFormat: newVal.attributes.code_format, - _armVisible: state === "disarmed", - _codeArmRequired: newVal.attributes.code_arm_required, - _disarmVisible: - this._armedStates.includes(state) || - state === "pending" || - state === "triggered" || - state === "arming", - }; - props._inputEnabled = props._disarmVisible || props._armVisible; - this.setProperties(props); - } - if (oldVal) { - setTimeout(() => { - fireEvent(this, "iron-resize"); - }, 500); - } - } - - _getInputMode(format) { - return this._isNumber(format) ? "numeric" : "text"; - } - - _isNumber(format) { - return format === FORMAT_NUMBER; - } - - _validateCode(code, format, armVisible, codeArmRequired) { - return !format || code.length > 0 || (armVisible && !codeArmRequired); - } - - _digitClicked(ev) { - this._enteredCode += ev.target.getAttribute("data-digit"); - } - - _clearEnteredCode() { - this._enteredCode = ""; - } - - _callService(ev) { - const service = ev.target.getAttribute("data-service"); - const data = { - entity_id: this.stateObj.entity_id, - code: this._enteredCode, - }; - this.hass.callService("alarm_control_panel", service, data).then(() => { - this._enteredCode = ""; - }); - } -} -customElements.define( - "more-info-alarm_control_panel", - MoreInfoAlarmControlPanel -); diff --git a/src/dialogs/more-info/controls/more-info-alarm_control_panel.ts b/src/dialogs/more-info/controls/more-info-alarm_control_panel.ts new file mode 100644 index 0000000000..1efb0094f7 --- /dev/null +++ b/src/dialogs/more-info/controls/more-info-alarm_control_panel.ts @@ -0,0 +1,165 @@ +import "@material/mwc-button"; +import type { HassEntity } from "home-assistant-js-websocket"; +import { css, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, query } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; +import "../../../components/ha-textfield"; +import type { HaTextField } from "../../../components/ha-textfield"; +import { + callAlarmAction, + FORMAT_NUMBER, +} from "../../../data/alarm_control_panel"; +import type { HomeAssistant } from "../../../types"; + +const BUTTONS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "", "0", "clear"]; +const ARM_ACTIONS = ["arm_away", "arm_home"]; +const DISARM_ACTIONS = ["disarm"]; + +@customElement("more-info-alarm_control_panel") +export class MoreInfoAlarmControlPanel extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj?: HassEntity; + + @query("#alarmCode") private _input?: HaTextField; + + protected render(): TemplateResult { + if (!this.hass || !this.stateObj) { + return html``; + } + + return html` + ${!this.stateObj.attributes.code_format + ? "" + : html` +
    + +
    + `} + ${this.stateObj.attributes.code_format !== FORMAT_NUMBER + ? "" + : html` +
    + ${BUTTONS.map((value) => + value === "" + ? html`` + : html` + + ${value === "clear" + ? this.hass!.localize( + `ui.card.alarm_control_panel.clear_code` + ) + : value} + + ` + )} +
    + `} +
    + ${(this.stateObj.state === "disarmed" + ? ARM_ACTIONS + : DISARM_ACTIONS + ).map( + (stateAction) => html` + + ${this.hass!.localize( + `ui.card.alarm_control_panel.${stateAction}` + )} + + ` + )} +
    + `; + } + + private _handlePadClick(e: MouseEvent): void { + const val = (e.currentTarget! as any).value; + this._input!.value = val === "clear" ? "" : this._input!.value + val; + } + + private _handleActionClick(e: MouseEvent): void { + const input = this._input; + callAlarmAction( + this.hass!, + this.stateObj!.entity_id, + (e.currentTarget! as any).action, + input?.value || undefined + ); + if (input) { + input.value = ""; + } + } + + static styles = css` + ha-textfield { + display: block; + margin: 8px; + max-width: 150px; + text-align: center; + } + + #keypad { + display: flex; + justify-content: center; + flex-wrap: wrap; + margin: auto; + width: 100%; + max-width: 300px; + } + + #keypad mwc-button { + padding: 8px; + width: 30%; + box-sizing: border-box; + } + + .actions { + margin: 0; + display: flex; + flex-wrap: wrap; + justify-content: center; + } + + .actions mwc-button { + margin: 0 4px 4px; + } + + mwc-button#disarm { + color: var(--error-color); + } + + mwc-button.numberkey { + --mdc-typography-button-font-size: var(--keypad-font-size, 0.875rem); + } + + .center { + display: flex; + justify-content: center; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "more-info-alarm_control_panel": MoreInfoAlarmControlPanel; + } +} diff --git a/src/dialogs/more-info/controls/more-info-climate.ts b/src/dialogs/more-info/controls/more-info-climate.ts index 6074e2be62..2a66080055 100644 --- a/src/dialogs/more-info/controls/more-info-climate.ts +++ b/src/dialogs/more-info/controls/more-info-climate.ts @@ -1,6 +1,4 @@ -import "@polymer/iron-flex-layout/iron-flex-layout-classes"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; +import "@material/mwc-list/mwc-list-item"; import { css, CSSResultGroup, @@ -12,10 +10,11 @@ import { import { property } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { fireEvent } from "../../../common/dom/fire_event"; +import { stopPropagation } from "../../../common/dom/stop_propagation"; import { supportsFeature } from "../../../common/entity/supports-feature"; import { computeRTLDirection } from "../../../common/util/compute_rtl"; import "../../../components/ha-climate-control"; -import "../../../components/ha-paper-dropdown-menu"; +import "../../../components/ha-select"; import "../../../components/ha-slider"; import "../../../components/ha-switch"; import { @@ -169,109 +168,93 @@ class MoreInfoClimate extends LitElement {
    - - - ${stateObj.attributes.hvac_modes - .concat() - .sort(compareClimateHvacModes) - .map( - (mode) => html` - - ${hass.localize(`component.climate.state._.${mode}`)} - - ` - )} - - + ${stateObj.attributes.hvac_modes + .concat() + .sort(compareClimateHvacModes) + .map( + (mode) => html` + + ${hass.localize(`component.climate.state._.${mode}`)} + + ` + )} +
    ${supportPresetMode && stateObj.attributes.preset_modes ? html`
    - - - ${stateObj.attributes.preset_modes!.map( - (mode) => html` - - ${hass.localize( - `state_attributes.climate.preset_mode.${mode}` - ) || mode} - - ` - )} - - + ${stateObj.attributes.preset_modes!.map( + (mode) => html` + + ${hass.localize( + `state_attributes.climate.preset_mode.${mode}` + ) || mode} + + ` + )} +
    ` : ""} ${supportFanMode && stateObj.attributes.fan_modes ? html`
    - - - ${stateObj.attributes.fan_modes!.map( - (mode) => html` - - ${hass.localize( - `state_attributes.climate.fan_mode.${mode}` - ) || mode} - - ` - )} - - + ${stateObj.attributes.fan_modes!.map( + (mode) => html` + + ${hass.localize( + `state_attributes.climate.fan_mode.${mode}` + ) || mode} + + ` + )} +
    ` : ""} ${supportSwingMode && stateObj.attributes.swing_modes ? html`
    - - - ${stateObj.attributes.swing_modes!.map( - (mode) => html` - ${mode} - ` - )} - - + ${stateObj.attributes.swing_modes!.map( + (mode) => html` + ${mode} + ` + )} +
    ` : ""} @@ -366,7 +349,7 @@ class MoreInfoClimate extends LitElement { } private _handleFanmodeChanged(ev) { - const newVal = ev.detail.value; + const newVal = ev.target.value; this._callServiceHelper( this.stateObj!.attributes.fan_mode, newVal, @@ -376,14 +359,14 @@ class MoreInfoClimate extends LitElement { } private _handleOperationmodeChanged(ev) { - const newVal = ev.detail.value; + const newVal = ev.target.value; this._callServiceHelper(this.stateObj!.state, newVal, "set_hvac_mode", { hvac_mode: newVal, }); } private _handleSwingmodeChanged(ev) { - const newVal = ev.detail.value; + const newVal = ev.target.value; this._callServiceHelper( this.stateObj!.attributes.swing_mode, newVal, @@ -393,7 +376,7 @@ class MoreInfoClimate extends LitElement { } private _handlePresetmodeChanged(ev) { - const newVal = ev.detail.value || null; + const newVal = ev.target.value || null; this._callServiceHelper( this.stateObj!.attributes.preset_mode, newVal, @@ -444,12 +427,9 @@ class MoreInfoClimate extends LitElement { color: var(--primary-text-color); } - ha-paper-dropdown-menu { + ha-select { width: 100%; - } - - paper-item { - cursor: pointer; + margin-top: 8px; } ha-slider { @@ -488,3 +468,9 @@ class MoreInfoClimate extends LitElement { } customElements.define("more-info-climate", MoreInfoClimate); + +declare global { + interface HTMLElementTagNameMap { + "more-info-climate": MoreInfoClimate; + } +} diff --git a/src/dialogs/more-info/controls/more-info-configurator.js b/src/dialogs/more-info/controls/more-info-configurator.js deleted file mode 100644 index 52f7ce6406..0000000000 --- a/src/dialogs/more-info/controls/more-info-configurator.js +++ /dev/null @@ -1,148 +0,0 @@ -import "@material/mwc-button"; -import "@polymer/iron-flex-layout/iron-flex-layout-classes"; -import "@polymer/paper-input/paper-input"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "../../../components/ha-circular-progress"; -import "../../../components/ha-markdown"; - -class MoreInfoConfigurator extends PolymerElement { - static get template() { - return html` - - - -
    - -
    - `; - } - - static get properties() { - return { - stateObj: { - type: Object, - }, - - action: { - type: String, - value: "display", - }, - - isConfigurable: { - type: Boolean, - computed: "computeIsConfigurable(stateObj)", - }, - - isConfiguring: { - type: Boolean, - value: false, - }, - - fieldInput: { - type: Object, - value: function () { - return {}; - }, - }, - }; - } - - computeIsConfigurable(stateObj) { - return stateObj.state === "configure"; - } - - fieldChanged(ev) { - const el = ev.target; - this.fieldInput[el.name] = el.value; - } - - submitClicked() { - const data = { - configure_id: this.stateObj.attributes.configure_id, - fields: this.fieldInput, - }; - - this.isConfiguring = true; - - this.hass.callService("configurator", "configure", data).then( - () => { - this.isConfiguring = false; - }, - () => { - this.isConfiguring = false; - } - ); - } -} - -customElements.define("more-info-configurator", MoreInfoConfigurator); diff --git a/src/dialogs/more-info/controls/more-info-configurator.ts b/src/dialogs/more-info/controls/more-info-configurator.ts new file mode 100644 index 0000000000..f6f77f451b --- /dev/null +++ b/src/dialogs/more-info/controls/more-info-configurator.ts @@ -0,0 +1,128 @@ +import "@material/mwc-button"; +import type { HassEntity } from "home-assistant-js-websocket"; +import { css, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import "../../../components/ha-alert"; +import "../../../components/ha-circular-progress"; +import "../../../components/ha-markdown"; +import "../../../components/ha-textfield"; +import type { HomeAssistant } from "../../../types"; + +@customElement("more-info-configurator") +export class MoreInfoConfigurator extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj?: HassEntity; + + @state() private _isConfiguring = false; + + private _fieldInput = {}; + + protected render(): TemplateResult { + if (this.stateObj?.state !== "configure") { + return html``; + } + + return html` +
    + + + ${this.stateObj.attributes.errors + ? html` + ${this.stateObj.attributes.errors} + ` + : ""} + ${this.stateObj.attributes.fields.map( + (field) => html`` + )} + ${this.stateObj.attributes.submit_caption + ? html`

    + + ${this._isConfiguring + ? html`` + : ""} + ${this.stateObj.attributes.submit_caption} + +

    ` + : ""} +
    + `; + } + + private _fieldChanged(ev) { + const el = ev.target; + this._fieldInput[el.name] = el.value; + } + + private _submitClicked() { + const data = { + configure_id: this.stateObj!.attributes.configure_id, + fields: this._fieldInput, + }; + + this._isConfiguring = true; + + this.hass.callService("configurator", "configure", data).then( + () => { + this._isConfiguring = false; + }, + () => { + this._isConfiguring = false; + } + ); + } + + static styles = css` + .container { + display: flex; + flex-direction: column; + } + p { + margin: 8px 0; + } + + a { + color: var(--primary-color); + } + + p > img { + max-width: 100%; + } + + p.center { + text-align: center; + } + + p.submit { + text-align: center; + height: 41px; + } + + ha-circular-progress { + width: 14px; + height: 14px; + margin-right: 20px; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "more-info-configurator": MoreInfoConfigurator; + } +} diff --git a/src/dialogs/more-info/controls/more-info-fan.js b/src/dialogs/more-info/controls/more-info-fan.js index 3bfa8ec2aa..5a2f042448 100644 --- a/src/dialogs/more-info/controls/more-info-fan.js +++ b/src/dialogs/more-info/controls/more-info-fan.js @@ -1,6 +1,5 @@ +import "@material/mwc-list/mwc-list-item"; import "@polymer/iron-flex-layout/iron-flex-layout-classes"; -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"; @@ -10,7 +9,7 @@ import "../../../components/ha-attributes"; import "../../../components/ha-icon"; import "../../../components/ha-icon-button"; import "../../../components/ha-labeled-slider"; -import "../../../components/ha-paper-dropdown-menu"; +import "../../../components/ha-select"; import "../../../components/ha-switch"; import { SUPPORT_SET_SPEED } from "../../../data/fan"; import { EventsMixin } from "../../../mixins/events-mixin"; @@ -38,13 +37,9 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) { display: block; } - ha-paper-dropdown-menu { + ha-select { width: 100%; } - - paper-item { - cursor: pointer; - }
    @@ -62,25 +57,21 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
    - - - - - + [[item]] + +
    @@ -180,7 +171,7 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) { presetModeChanged(ev) { const oldVal = this.stateObj.attributes.preset_mode; - const newVal = ev.detail.value; + const newVal = ev.target.value; if (!newVal || oldVal === newVal) return; @@ -190,6 +181,10 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) { }); } + stopPropagation(ev) { + ev.stopPropagation(); + } + percentageChanged(ev) { const oldVal = parseInt(this.stateObj.attributes.percentage, 10); const newVal = ev.target.value; diff --git a/src/dialogs/more-info/controls/more-info-humidifier.ts b/src/dialogs/more-info/controls/more-info-humidifier.ts index 8271b86703..bce976b45b 100644 --- a/src/dialogs/more-info/controls/more-info-humidifier.ts +++ b/src/dialogs/more-info/controls/more-info-humidifier.ts @@ -1,6 +1,3 @@ -import "@polymer/iron-flex-layout/iron-flex-layout-classes"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; import { css, CSSResultGroup, @@ -12,9 +9,9 @@ import { import { property } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { fireEvent } from "../../../common/dom/fire_event"; +import { stopPropagation } from "../../../common/dom/stop_propagation"; import { supportsFeature } from "../../../common/entity/supports-feature"; import { computeRTLDirection } from "../../../common/util/compute_rtl"; -import "../../../components/ha-paper-dropdown-menu"; import "../../../components/ha-slider"; import "../../../components/ha-switch"; import { @@ -22,6 +19,8 @@ import { HUMIDIFIER_SUPPORT_MODES, } from "../../../data/humidifier"; import { HomeAssistant } from "../../../types"; +import "@material/mwc-list/mwc-list"; +import "@material/mwc-list/mwc-list-item"; class MoreInfoHumidifier extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -69,28 +68,24 @@ class MoreInfoHumidifier extends LitElement { ${supportModes ? html`
    - - - ${stateObj.attributes.available_modes!.map( - (mode) => html` - - ${hass.localize( - `state_attributes.humidifier.mode.${mode}` - ) || mode} - - ` - )} - - + ${stateObj.attributes.available_modes!.map( + (mode) => html` + + ${hass.localize( + `state_attributes.humidifier.mode.${mode}` + ) || mode} + + ` + )} +
    ` : ""} @@ -124,7 +119,7 @@ class MoreInfoHumidifier extends LitElement { } private _handleModeChanged(ev) { - const newVal = ev.detail.value || null; + const newVal = ev.target.value || null; this._callServiceHelper( this.stateObj!.attributes.mode, newVal, @@ -175,15 +170,7 @@ class MoreInfoHumidifier extends LitElement { color: var(--primary-text-color); } - ha-paper-dropdown-menu { - width: 100%; - } - - paper-item { - cursor: pointer; - } - - ha-slider { + ha-select { width: 100%; } @@ -207,3 +194,9 @@ class MoreInfoHumidifier extends LitElement { } customElements.define("more-info-humidifier", MoreInfoHumidifier); + +declare global { + interface HTMLElementTagNameMap { + "more-info-humidifier": MoreInfoHumidifier; + } +} diff --git a/src/dialogs/more-info/controls/more-info-input_datetime.ts b/src/dialogs/more-info/controls/more-info-input_datetime.ts index af1e9aa6a1..a0f11eb726 100644 --- a/src/dialogs/more-info/controls/more-info-input_datetime.ts +++ b/src/dialogs/more-info/controls/more-info-input_datetime.ts @@ -23,6 +23,7 @@ class MoreInfoInputDatetime extends LitElement { this.stateObj.attributes.has_date ? html` diff --git a/src/dialogs/more-info/controls/more-info-light.ts b/src/dialogs/more-info/controls/more-info-light.ts index cc656c8e60..5a2a07c21b 100644 --- a/src/dialogs/more-info/controls/more-info-light.ts +++ b/src/dialogs/more-info/controls/more-info-light.ts @@ -1,6 +1,5 @@ +import "@material/mwc-list/mwc-list-item"; import { mdiPalette } from "@mdi/js"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; import { css, CSSResultGroup, @@ -11,13 +10,14 @@ import { } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; +import { stopPropagation } from "../../../common/dom/stop_propagation"; import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-attributes"; import "../../../components/ha-button-toggle-group"; import "../../../components/ha-color-picker"; import "../../../components/ha-icon-button"; import "../../../components/ha-labeled-slider"; -import "../../../components/ha-paper-dropdown-menu"; +import "../../../components/ha-select"; import { getLightCurrentModeRgbColor, LightColorModes, @@ -208,24 +208,22 @@ class MoreInfoLight extends LitElement { this.stateObj!.attributes.effect_list?.length ? html`
    - - ${this.stateObj.attributes.effect_list.map( - (effect: string) => html` - ${effect} - ` - )} - - + ${this.stateObj.attributes.effect_list.map( + (effect: string) => html` + + ${effect} + + ` + )} + ` : ""} ` @@ -322,8 +320,8 @@ class MoreInfoLight extends LitElement { this._mode = ev.detail.value; } - private _effectChanged(ev: CustomEvent) { - const newVal = ev.detail.item.itemName; + private _effectChanged(ev) { + const newVal = ev.target.value; if (!newVal || this.stateObj!.attributes.effect === newVal) { return; @@ -617,10 +615,6 @@ class MoreInfoLight extends LitElement { color: var(--secondary-text-color); } - paper-item { - cursor: pointer; - } - hr { border-color: var(--divider-color); border-bottom: none; diff --git a/src/dialogs/more-info/controls/more-info-lock.js b/src/dialogs/more-info/controls/more-info-lock.js deleted file mode 100644 index 346e1a8625..0000000000 --- a/src/dialogs/more-info/controls/more-info-lock.js +++ /dev/null @@ -1,80 +0,0 @@ -import "@material/mwc-button"; -import "@polymer/paper-input/paper-input"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "../../../components/ha-attributes"; -import LocalizeMixin from "../../../mixins/localize-mixin"; - -/* - * @appliesMixin LocalizeMixin - */ -class MoreInfoLock extends LocalizeMixin(PolymerElement) { - static get template() { - return html` - - - - - `; - } - - static get properties() { - return { - hass: Object, - stateObj: { - type: Object, - observer: "stateObjChanged", - }, - enteredCode: { - type: String, - value: "", - }, - isLocked: Boolean, - }; - } - - stateObjChanged(newVal) { - if (newVal) { - this.isLocked = newVal.state === "locked"; - } - } - - callService(ev) { - const service = ev.target.getAttribute("data-service"); - const data = { - entity_id: this.stateObj.entity_id, - code: this.enteredCode, - }; - this.hass.callService("lock", service, data); - } -} - -customElements.define("more-info-lock", MoreInfoLock); diff --git a/src/dialogs/more-info/controls/more-info-lock.ts b/src/dialogs/more-info/controls/more-info-lock.ts new file mode 100644 index 0000000000..0c380d267f --- /dev/null +++ b/src/dialogs/more-info/controls/more-info-lock.ts @@ -0,0 +1,70 @@ +import "@material/mwc-button"; +import type { HassEntity } from "home-assistant-js-websocket"; +import { css, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, query } from "lit/decorators"; +import "../../../components/ha-attributes"; +import "../../../components/ha-textfield"; +import type { HaTextField } from "../../../components/ha-textfield"; +import type { HomeAssistant } from "../../../types"; + +@customElement("more-info-lock") +class MoreInfoLock extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj?: HassEntity; + + @query("ha-textfield") private _textfield?: HaTextField; + + protected render(): TemplateResult { + if (!this.hass || !this.stateObj) { + return html``; + } + return html` + ${this.stateObj.attributes.code_format + ? html` + + ${this.stateObj.state === "locked" + ? html`${this.hass.localize("ui.card.lock.unlock")}` + : html`${this.hass.localize("ui.card.lock.lock")}`} + ` + : ""} + + `; + } + + private _callService(ev) { + const service = ev.target.getAttribute("data-service"); + const data = { + entity_id: this.stateObj!.entity_id, + code: this._textfield?.value, + }; + this.hass.callService("lock", service, data); + } + + static styles = css` + :host { + display: flex; + align-items: center; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "more-info-lock": MoreInfoLock; + } +} diff --git a/src/dialogs/more-info/controls/more-info-media_player.ts b/src/dialogs/more-info/controls/more-info-media_player.ts index e836e3dd28..efb7bf3617 100644 --- a/src/dialogs/more-info/controls/more-info-media_player.ts +++ b/src/dialogs/more-info/controls/more-info-media_player.ts @@ -1,28 +1,26 @@ import "@material/mwc-button/mwc-button"; +import "@material/mwc-list/mwc-list-item"; import { mdiLoginVariant, mdiMusicNote, mdiPlayBoxMultiple, - mdiSend, mdiVolumeHigh, mdiVolumeMinus, mdiVolumeOff, mdiVolumePlus, } from "@mdi/js"; -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, query } from "lit/decorators"; +import { customElement, property } from "lit/decorators"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; +import { stopPropagation } from "../../../common/dom/stop_propagation"; import { supportsFeature } from "../../../common/entity/supports-feature"; import { computeRTLDirection } from "../../../common/util/compute_rtl"; import "../../../components/ha-icon-button"; -import "../../../components/ha-svg-icon"; -import "../../../components/ha-paper-dropdown-menu"; +import "../../../components/ha-select"; import "../../../components/ha-slider"; +import "../../../components/ha-svg-icon"; import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog"; -import { UNAVAILABLE, UNAVAILABLE_STATES, UNKNOWN } from "../../../data/entity"; +import { UNAVAILABLE, UNKNOWN } from "../../../data/entity"; import { computeMediaControls, MediaPickedEvent, @@ -43,8 +41,6 @@ class MoreInfoMediaPlayer extends LitElement { @property({ attribute: false }) public stateObj?: MediaPlayerEntity; - @query("#ttsInput") private _ttsInput?: HTMLInputElement; - protected render(): TemplateResult { if (!this.stateObj) { return html``; @@ -75,13 +71,17 @@ class MoreInfoMediaPlayer extends LitElement {
    ${supportsFeature(stateObj, SUPPORT_BROWSE_MEDIA) ? html` - + > + + ` : ""}
    @@ -135,27 +135,23 @@ class MoreInfoMediaPlayer extends LitElement { stateObj.attributes.source_list?.length ? html`
    - - - - ${stateObj.attributes.source_list!.map( - (source) => - html` - ${source} - ` - )} - - + ${stateObj.attributes.source_list!.map( + (source) => + html` + ${source} + ` + )} + +
    ` : ""} @@ -163,25 +159,22 @@ class MoreInfoMediaPlayer extends LitElement { stateObj.attributes.sound_mode_list?.length ? html`
    - - - - ${stateObj.attributes.sound_mode_list.map( - (mode) => html` - ${mode} - ` - )} - - + ${stateObj.attributes.sound_mode_list.map( + (mode) => html` + ${mode} + ` + )} + +
    ` : ""} @@ -189,21 +182,8 @@ class MoreInfoMediaPlayer extends LitElement { supportsFeature(stateObj, SUPPORT_PLAY_MEDIA) ? html`
    - - + Text to speech has moved to the media browser.
    -
    ` : ""} `; @@ -213,14 +193,14 @@ class MoreInfoMediaPlayer extends LitElement { return css` ha-icon-button[action="turn_off"], ha-icon-button[action="turn_on"], - ha-slider, - #ttsInput { + ha-slider { flex-grow: 1; } .controls { display: flex; align-items: center; + --mdc-theme-primary: currentColor; } .basic-controls { @@ -229,27 +209,25 @@ class MoreInfoMediaPlayer extends LitElement { .volume, .source-input, - .sound-input, - .tts { + .sound-input { display: flex; align-items: center; justify-content: space-between; } - .source-input ha-svg-icon, - .sound-input ha-svg-icon { - padding: 7px; - margin-top: 24px; - } - - .source-input ha-paper-dropdown-menu, - .sound-input ha-paper-dropdown-menu { + .source-input ha-select, + .sound-input ha-select { margin-left: 10px; flex-grow: 1; } - paper-item { - cursor: pointer; + .tts { + margin-top: 16px; + font-style: italic; + } + + mwc-button > ha-svg-icon { + vertical-align: text-bottom; } `; } @@ -279,8 +257,8 @@ class MoreInfoMediaPlayer extends LitElement { }); } - private _handleSourceChanged(e: CustomEvent) { - const newVal = e.detail.item.itemName; + private _handleSourceChanged(e) { + const newVal = e.target.value; if (!newVal || this.stateObj!.attributes.source === newVal) { return; @@ -292,8 +270,8 @@ class MoreInfoMediaPlayer extends LitElement { }); } - private _handleSoundModeChanged(e: CustomEvent) { - const newVal = e.detail.item.itemName; + private _handleSoundModeChanged(e) { + const newVal = e.target.value; if (!newVal || this.stateObj?.attributes.sound_mode === newVal) { return; @@ -305,32 +283,6 @@ class MoreInfoMediaPlayer extends LitElement { }); } - private _ttsCheckForEnter(e: KeyboardEvent) { - if (e.keyCode === 13) this._sendTTS(); - } - - private _sendTTS() { - const ttsInput = this._ttsInput; - if (!ttsInput) { - return; - } - - const services = this.hass.services.tts; - const serviceKeys = Object.keys(services).sort(); - - const service = serviceKeys.find((key) => key.indexOf("_say") !== -1); - - if (!service) { - return; - } - - this.hass.callService("tts", service, { - entity_id: this.stateObj!.entity_id, - message: ttsInput.value, - }); - ttsInput.value = ""; - } - private _showBrowseMedia(): void { showMediaBrowserDialog(this, { action: "play", diff --git a/src/dialogs/more-info/controls/more-info-remote.ts b/src/dialogs/more-info/controls/more-info-remote.ts index 421537bf67..dbdc2fd0c9 100644 --- a/src/dialogs/more-info/controls/more-info-remote.ts +++ b/src/dialogs/more-info/controls/more-info-remote.ts @@ -1,12 +1,12 @@ -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-attributes"; -import "../../../components/ha-paper-dropdown-menu"; import { RemoteEntity, REMOTE_SUPPORT_ACTIVITY } from "../../../data/remote"; import { HomeAssistant } from "../../../types"; +import "@material/mwc-list/mwc-list-item"; +import "@material/mwc-list/mwc-list"; +import { stopPropagation } from "../../../common/dom/stop_propagation"; const filterExtraAttributes = "activity_list,current_activity"; @@ -26,24 +26,22 @@ class MoreInfoRemote extends LitElement { return html` ${supportsFeature(stateObj, REMOTE_SUPPORT_ACTIVITY) ? html` - - - ${stateObj.attributes.activity_list!.map( - (activity) => html` - ${activity} - ` - )} - - + ${stateObj.attributes.activity_list!.map( + (activity) => html` + ${activity} + ` + )} + ` : ""} @@ -55,9 +53,9 @@ class MoreInfoRemote extends LitElement { `; } - private handleActivityChanged(ev: CustomEvent) { + private handleActivityChanged(ev) { const oldVal = this.stateObj!.attributes.current_activity; - const newVal = ev.detail.item.itemName; + const newVal = ev.target.value; if (!newVal || oldVal === newVal) { return; @@ -68,14 +66,6 @@ class MoreInfoRemote extends LitElement { activity: newVal, }); } - - static get styles(): CSSResultGroup { - return css` - paper-item { - cursor: pointer; - } - `; - } } declare global { diff --git a/src/dialogs/more-info/controls/more-info-vacuum.ts b/src/dialogs/more-info/controls/more-info-vacuum.ts index 73a3c12eb0..852a41b98a 100644 --- a/src/dialogs/more-info/controls/more-info-vacuum.ts +++ b/src/dialogs/more-info/controls/more-info-vacuum.ts @@ -1,3 +1,4 @@ +import "@material/mwc-list/mwc-list-item"; import { mdiFan, mdiHomeMapMarker, @@ -8,15 +9,14 @@ import { mdiStop, mdiTargetVariant, } from "@mdi/js"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; +import { stopPropagation } from "../../../common/dom/stop_propagation"; import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-attributes"; import "../../../components/ha-icon"; import "../../../components/ha-icon-button"; -import "../../../components/ha-paper-dropdown-menu"; +import "../../../components/ha-select"; import { UNAVAILABLE } from "../../../data/entity"; import { VacuumEntity, @@ -173,25 +173,23 @@ class MoreInfoVacuum extends LitElement { ? html`
    - - - ${stateObj.attributes.fan_speed_list!.map( - (mode) => html` - ${mode} - ` - )} - - + ${stateObj.attributes.fan_speed_list!.map( + (mode) => html` + ${mode} + ` + )} +
    @@ -221,9 +219,9 @@ class MoreInfoVacuum extends LitElement { }); } - private handleFanSpeedChanged(ev: CustomEvent) { + private handleFanSpeedChanged(ev) { const oldVal = this.stateObj!.attributes.fan_speed; - const newVal = ev.detail.item.itemName; + const newVal = ev.target.value; if (!newVal || oldVal === newVal) { return; @@ -243,9 +241,6 @@ class MoreInfoVacuum extends LitElement { .status-subtitle { color: var(--secondary-text-color); } - paper-item { - cursor: pointer; - } .flex-horizontal { display: flex; flex-direction: row; diff --git a/src/dialogs/more-info/controls/more-info-water_heater.js b/src/dialogs/more-info/controls/more-info-water_heater.js index 0a8c006006..a48c3b4118 100644 --- a/src/dialogs/more-info/controls/more-info-water_heater.js +++ b/src/dialogs/more-info/controls/more-info-water_heater.js @@ -1,6 +1,5 @@ +import "@material/mwc-list/mwc-list-item"; import "@polymer/iron-flex-layout/iron-flex-layout-classes"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; import { timeOut } from "@polymer/polymer/lib/utils/async"; import { Debouncer } from "@polymer/polymer/lib/utils/debounce"; import { html } from "@polymer/polymer/lib/utils/html-tag"; @@ -8,7 +7,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; import { featureClassNames } from "../../../common/entity/feature_class_names"; import { supportsFeature } from "../../../common/entity/supports-feature"; -import "../../../components/ha-paper-dropdown-menu"; +import "../../../components/ha-select"; import "../../../components/ha-switch"; import "../../../components/ha-water_heater-control"; import { EventsMixin } from "../../../mixins/events-mixin"; @@ -27,14 +26,10 @@ class MoreInfoWaterHeater extends LocalizeMixin(EventsMixin(PolymerElement)) { color: var(--primary-text-color); } - ha-paper-dropdown-menu { + ha-select { width: 100%; } - paper-item { - cursor: pointer; - } - ha-water_heater-control.range-control-left, ha-water_heater-control.range-control-right { float: left; @@ -75,27 +70,23 @@ class MoreInfoWaterHeater extends LocalizeMixin(EventsMixin(PolymerElement)) { +
    @@ -209,13 +200,17 @@ class MoreInfoWaterHeater extends LocalizeMixin(EventsMixin(PolymerElement)) { handleOperationmodeChanged(ev) { const oldVal = this.stateObj.attributes.operation_mode; - const newVal = ev.detail.value; + const newVal = ev.target.value; if (!newVal || oldVal === newVal) return; this.callServiceHelper("set_operation_mode", { operation_mode: newVal, }); } + stopPropagation(ev) { + ev.stopPropagation(); + } + callServiceHelper(service, data) { // We call stateChanged after a successful call to re-sync the inputs // with the state. It will be out of sync if our service call did not diff --git a/src/dialogs/more-info/ha-more-info-dialog.ts b/src/dialogs/more-info/ha-more-info-dialog.ts index 64082db266..2198a69a9e 100644 --- a/src/dialogs/more-info/ha-more-info-dialog.ts +++ b/src/dialogs/more-info/ha-more-info-dialog.ts @@ -164,6 +164,7 @@ export class MoreInfoDialog extends LitElement { .label=${this.hass.localize( "ui.dialogs.more_info_control.details" )} + dialogInitialFocus > -
    +
    ${cache( this._currTabIndex === 0 ? html` @@ -337,7 +338,9 @@ export class MoreInfoDialog extends LitElement { flex-shrink: 0; display: block; } - + .content { + outline: none; + } @media all and (max-width: 450px), all and (max-height: 500px) { ha-header-bar { --mdc-theme-primary: var(--app-header-background-color); diff --git a/src/dialogs/quick-bar/ha-quick-bar.ts b/src/dialogs/quick-bar/ha-quick-bar.ts index 77e1ccd8df..337855d71b 100644 --- a/src/dialogs/quick-bar/ha-quick-bar.ts +++ b/src/dialogs/quick-bar/ha-quick-bar.ts @@ -1,8 +1,5 @@ -import "../../components/ha-textfield"; -import { Layout1d, scroll } from "@lit-labs/virtualizer"; +import "@lit-labs/virtualizer"; import "@material/mwc-list/mwc-list"; -import type { List } from "@material/mwc-list/mwc-list"; -import { SingleSelectedEvent } from "@material/mwc-list/mwc-list-foundation"; import "@material/mwc-list/mwc-list-item"; import type { ListItem } from "@material/mwc-list/mwc-list-item"; import { @@ -13,7 +10,7 @@ import { mdiReload, mdiServerNetwork, } from "@mdi/js"; -import { css, html, LitElement } from "lit"; +import { css, html, LitElement, TemplateResult } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; import { styleMap } from "lit/directives/style-map"; @@ -25,7 +22,6 @@ import { computeDomain } from "../../common/entity/compute_domain"; import { computeStateName } from "../../common/entity/compute_state_name"; import { domainIcon } from "../../common/entity/domain_icon"; import { navigate } from "../../common/navigate"; -import "../../common/search/search-input"; import { caseInsensitiveStringCompare } from "../../common/string/compare"; import { fuzzyFilterSort, @@ -36,11 +32,12 @@ import "../../components/ha-chip"; import "../../components/ha-circular-progress"; import "../../components/ha-header-bar"; import "../../components/ha-icon-button"; +import "../../components/ha-textfield"; import { domainToName } from "../../data/integration"; import { getPanelNameTranslationKey } from "../../data/panel"; import { PageNavigation } from "../../layouts/hass-tabs-subpage"; import { configSections } from "../../panels/config/ha-panel-config"; -import { haStyleDialog } from "../../resources/styles"; +import { haStyleDialog, haStyleScrollbar } from "../../resources/styles"; import { HomeAssistant } from "../../types"; import { ConfirmationDialogParams, @@ -124,18 +121,28 @@ export class QuickBar extends LitElement { fireEvent(this, "dialog-closed", { dialog: this.localName }); } + private _getItems = memoizeOne( + (commandMode: boolean, commandItems, entityItems, filter: string) => { + const items = commandMode ? commandItems : entityItems; + + if (items && filter && filter !== " ") { + return this._filterItems(items, filter); + } + return items; + } + ); + protected render() { if (!this._opened) { return html``; } - let items: QuickBarItem[] | undefined = this._commandMode - ? this._commandItems - : this._entityItems; - - if (items && this._filter && this._filter !== " ") { - items = this._filterItems(items, this._filter); - } + const items: QuickBarItem[] | undefined = this._getItems( + this._commandMode, + this._commandItems, + this._entityItems, + this._filter + ); return html` ` : html` - - ${scroll({ - items, - layout: Layout1d, - renderItem: (item: QuickBarItem, index) => - this._renderItem(item, index), - })} + + + `} ${this._hint ? html`
    ${this._hint}
    ` : ""} @@ -261,14 +268,14 @@ export class QuickBar extends LitElement { } } - private _renderItem(item: QuickBarItem, index?: number) { + private _renderItem = (item: QuickBarItem, index: number): TemplateResult => { if (!item) { return html``; } return isCommandItem(item) ? this._renderCommandItem(item, index) : this._renderEntityItem(item as EntityItem, index); - } + }; private _renderEntityItem(item: EntityItem, index?: number) { return html` @@ -337,16 +344,6 @@ export class QuickBar extends LitElement { this.closeDialog(); } - private _handleSelected(ev: SingleSelectedEvent) { - const index = ev.detail.index; - if (index < 0) { - return; - } - - const item = ((ev.target as List).items[index] as any).item; - this.processItemAndCloseDialog(item, index); - } - private _handleInputKeyDown(ev: KeyboardEvent) { if (ev.code === "Enter") { const firstItem = this._getItemAtIndex(0); @@ -431,20 +428,38 @@ export class QuickBar extends LitElement { private _handleListItemKeyDown(ev: KeyboardEvent) { const isSingleCharacter = ev.key.length === 1; - const isFirstListItem = - (ev.target as HTMLElement).getAttribute("index") === "0"; + const index = (ev.target as HTMLElement).getAttribute("index"); + const isFirstListItem = index === "0"; this._focusListElement = ev.target as ListItem; + if (ev.key === "ArrowDown") { + this._getItemAtIndex(Number(index) + 1)?.focus(); + } if (ev.key === "ArrowUp") { if (isFirstListItem) { this._filterInputField?.focus(); + } else { + this._getItemAtIndex(Number(index) - 1)?.focus(); } } + if (ev.key === "Enter" || ev.key === " ") { + this.processItemAndCloseDialog( + (ev.target as any).item, + Number((ev.target as HTMLElement).getAttribute("index")) + ); + } if (ev.key === "Backspace" || isSingleCharacter) { - (ev.currentTarget as List).scrollTop = 0; + (ev.currentTarget as HTMLElement).scrollTop = 0; this._filterInputField?.focus(); } } + private _handleItemClick(ev) { + this.processItemAndCloseDialog( + (ev.target as any).item, + Number((ev.target as HTMLElement).getAttribute("index")) + ); + } + private _generateEntityItems(): EntityItem[] { return Object.keys(this.hass.states) .map((entityId) => { @@ -683,6 +698,7 @@ export class QuickBar extends LitElement { static get styles() { return [ + haStyleScrollbar, haStyleDialog, css` .heading { @@ -780,6 +796,10 @@ export class QuickBar extends LitElement { display: flex; align-items: center; } + + lit-virtualizer { + contain: size layout !important; + } `, ]; } diff --git a/src/entrypoints/app.ts b/src/entrypoints/app.ts index d4d1852c20..f7f607720b 100644 --- a/src/entrypoints/app.ts +++ b/src/entrypoints/app.ts @@ -1,7 +1,12 @@ -import { setPassiveTouchGestures } from "@polymer/polymer/lib/utils/settings"; +import { + setPassiveTouchGestures, + setCancelSyntheticClickEvents, +} from "@polymer/polymer/lib/utils/settings"; +import "@webcomponents/scoped-custom-element-registry/scoped-custom-element-registry.min"; import "../layouts/home-assistant"; import "../resources/ha-style"; import "../resources/roboto"; import "../util/legacy-support"; setPassiveTouchGestures(true); +setCancelSyntheticClickEvents(false); diff --git a/src/entrypoints/authorize.ts b/src/entrypoints/authorize.ts index 36954a3fcf..0bbfdf65eb 100644 --- a/src/entrypoints/authorize.ts +++ b/src/entrypoints/authorize.ts @@ -1,13 +1,10 @@ // Compat needs to be first import import "../resources/compatibility"; +import { setCancelSyntheticClickEvents } from "@polymer/polymer/lib/utils/settings"; import "../auth/ha-authorize"; import "../resources/ha-style"; import "../resources/roboto"; import "../resources/safari-14-attachshadow-patch"; import "../resources/array.flat.polyfill"; -/* polyfill for paper-dropdown */ -setTimeout( - () => import("web-animations-js/web-animations-next-lite.min"), - 2000 -); +setCancelSyntheticClickEvents(false); diff --git a/src/entrypoints/custom-panel.ts b/src/entrypoints/custom-panel.ts index e2da175ea8..4a40fcab04 100644 --- a/src/entrypoints/custom-panel.ts +++ b/src/entrypoints/custom-panel.ts @@ -1,5 +1,6 @@ // Compat needs to be first import import "../resources/compatibility"; +import { setCancelSyntheticClickEvents } from "@polymer/polymer/lib/utils/settings"; import "../resources/safari-14-attachshadow-patch"; import { PolymerElement } from "@polymer/polymer"; @@ -15,6 +16,8 @@ import { createCustomPanelElement } from "../util/custom-panel/create-custom-pan import { loadCustomPanel } from "../util/custom-panel/load-custom-panel"; import { setCustomPanelProperties } from "../util/custom-panel/set-custom-panel-properties"; +setCancelSyntheticClickEvents(false); + declare global { interface Window { loadES5Adapter: () => Promise; @@ -47,7 +50,8 @@ function initialize( ) { const style = document.createElement("style"); - style.innerHTML = `body { margin:0; } + style.innerHTML = ` + body { margin:0; } @media (prefers-color-scheme: dark) { body { background-color: #111111; diff --git a/src/entrypoints/onboarding.ts b/src/entrypoints/onboarding.ts index 44a67890ab..78696169ca 100644 --- a/src/entrypoints/onboarding.ts +++ b/src/entrypoints/onboarding.ts @@ -1,11 +1,14 @@ // Compat needs to be first import import "../resources/compatibility"; +import { setCancelSyntheticClickEvents } from "@polymer/polymer/lib/utils/settings"; import "../onboarding/ha-onboarding"; import "../resources/ha-style"; import "../resources/roboto"; import "../resources/safari-14-attachshadow-patch"; import "../resources/array.flat.polyfill"; +setCancelSyntheticClickEvents(false); + declare global { interface Window { stepsPromise: Promise; diff --git a/src/fake_data/provide_hass.ts b/src/fake_data/provide_hass.ts index 8ca7b46de0..dc9e875b32 100644 --- a/src/fake_data/provide_hass.ts +++ b/src/fake_data/provide_hass.ts @@ -3,6 +3,7 @@ import { applyThemesOnElement, invalidateThemeCache, } from "../common/dom/apply_themes_on_element"; +import { fireEvent } from "../common/dom/fire_event"; import { computeLocalize } from "../common/translations/localize"; import { DEFAULT_PANEL } from "../data/panel"; import { NumberFormat, TimeFormat } from "../data/translation"; @@ -85,6 +86,7 @@ export const provideHass = ( hass().updateHass({ localize: await computeLocalize(elements[0], lang, hass().resources), }); + fireEvent(window, "translations-updated"); } function updateStates(newStates: HassEntities) { diff --git a/src/html/_js_base.html.template b/src/html/_js_base.html.template index 0eea886de2..ed3408e16e 100644 --- a/src/html/_js_base.html.template +++ b/src/html/_js_base.html.template @@ -22,4 +22,4 @@ document.write(" + \ No newline at end of file diff --git a/src/layouts/hass-tabs-subpage-data-table.ts b/src/layouts/hass-tabs-subpage-data-table.ts index b92f35e2e9..1e44c0fafd 100644 --- a/src/layouts/hass-tabs-subpage-data-table.ts +++ b/src/layouts/hass-tabs-subpage-data-table.ts @@ -1,5 +1,4 @@ import "@material/mwc-button/mwc-button"; -import { mdiFilterVariant } from "@mdi/js"; import "@polymer/paper-tooltip/paper-tooltip"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, query } from "lit/decorators"; @@ -157,30 +156,31 @@ export class HaTabsSubpageDataTable extends LitElement { : hiddenLabel; const headerToolbar = html` - ${filterInfo - ? html`
    - ${this.narrow - ? html`
    - - - ${filterInfo} - + .hass=${this.hass} + .filter=${this.filter} + .suffix=${!this.narrow} + @value-changed=${this._handleSearchChange} + .label=${this.searchLabel || + this.hass.localize("ui.components.data-table.search")} + > + ${!this.narrow + ? html`
    + ${filterInfo + ? html`
    + ${filterInfo} + + ${this.hass.localize("ui.components.data-table.clear")} +
    ` - : filterInfo} - - ${this.hass.localize("ui.components.data-table.clear")} - + : ""} +
    ` - : ""}`; + : ""} + `; return html` -
    +
    + ${this.narrow + ? html`
    + ${this.numHidden || this.activeFilters + ? html`${this.numHidden || "!"}` + : ""} + +
    ` + : ""} +
    ${this.narrow ? html`
    @@ -233,7 +242,14 @@ export class HaTabsSubpageDataTable extends LitElement { `; } + private _preventDefault(ev) { + ev.preventDefault(); + } + private _handleSearchChange(ev: CustomEvent) { + if (this.filter === ev.detail.value) { + return; + } this.filter = ev.detail.value; fireEvent(this, "search-changed", { value: this.filter }); } @@ -254,10 +270,10 @@ export class HaTabsSubpageDataTable extends LitElement { display: block; } .table-header { - border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12); - padding: 0 16px; display: flex; align-items: center; + --mdc-shape-small: 0; + height: 56px; } .search-toolbar { display: flex; @@ -265,12 +281,33 @@ export class HaTabsSubpageDataTable extends LitElement { color: var(--secondary-text-color); } search-input { - position: relative; - top: 2px; - flex-grow: 1; + --mdc-text-field-fill-color: var(--sidebar-background-color); + --mdc-text-field-idle-line-color: var(--divider-color); + --text-field-overflow: visible; + z-index: 5; } - search-input.header { - left: -8px; + .table-header search-input { + display: block; + position: absolute; + top: 0; + right: 0; + left: 0; + } + .search-toolbar search-input { + display: block; + width: 100%; + color: var(--secondary-text-color); + --mdc-ripple-color: transparant; + } + .filters { + --mdc-text-field-fill-color: var(--input-fill-color); + --mdc-text-field-idle-line-color: var(--input-idle-line-color); + --mdc-shape-small: 4px; + --text-field-overflow: initial; + display: flex; + justify-content: flex-end; + margin-right: 8px; + color: var(--primary-text-color); } .active-filters { color: var(--primary-text-color); @@ -280,6 +317,8 @@ export class HaTabsSubpageDataTable extends LitElement { padding: 2px 2px 2px 8px; margin-left: 4px; font-size: 14px; + width: max-content; + cursor: initial; } .active-filters ha-svg-icon { color: var(--primary-color); @@ -298,6 +337,24 @@ export class HaTabsSubpageDataTable extends LitElement { left: 0; content: ""; } + .badge { + min-width: 20px; + box-sizing: border-box; + border-radius: 50%; + font-weight: 400; + background-color: var(--primary-color); + line-height: 20px; + text-align: center; + padding: 0px 4px; + color: var(--text-primary-color); + position: absolute; + right: 0; + top: 4px; + font-size: 0.65em; + } + .filter-menu { + position: relative; + } `; } } diff --git a/src/layouts/hass-tabs-subpage.ts b/src/layouts/hass-tabs-subpage.ts index 0054e2dbbf..1c749ad090 100644 --- a/src/layouts/hass-tabs-subpage.ts +++ b/src/layouts/hass-tabs-subpage.ts @@ -272,6 +272,7 @@ class HassTabsSubpage extends LitElement { ha-menu-button, ha-icon-button-arrow-prev, ::slotted([slot="toolbar-icon"]) { + display: flex; flex-shrink: 0; pointer-events: auto; color: var(--sidebar-icon-color); diff --git a/src/layouts/home-assistant.ts b/src/layouts/home-assistant.ts index 7142419b8f..f75efdb624 100644 --- a/src/layouts/home-assistant.ts +++ b/src/layouts/home-assistant.ts @@ -78,8 +78,7 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) { super.firstUpdated(changedProps); this._initializeHass(); setTimeout(() => registerServiceWorker(this), 1000); - /* polyfill for paper-dropdown */ - import("web-animations-js/web-animations-next-lite.min"); + this.addEventListener("hass-suspend-when-hidden", (ev) => { this._updateHass({ suspendWhenHidden: ev.detail.suspend }); storeState(this.hass!); diff --git a/src/onboarding/onboarding-create-user.ts b/src/onboarding/onboarding-create-user.ts index c511d34a46..1855141a60 100644 --- a/src/onboarding/onboarding-create-user.ts +++ b/src/onboarding/onboarding-create-user.ts @@ -18,10 +18,18 @@ import { onboardUserStep } from "../data/onboarding"; import { PolymerChangedEvent } from "../polymer-types"; const CREATE_USER_SCHEMA: HaFormSchema[] = [ - { type: "string", name: "name", required: true }, - { type: "string", name: "username", required: true }, - { type: "string", name: "password", required: true }, - { type: "string", name: "password_confirm", required: true }, + { name: "name", required: true, selector: { text: {} } }, + { name: "username", required: true, selector: { text: {} } }, + { + name: "password", + required: true, + selector: { text: { type: "password" } }, + }, + { + name: "password_confirm", + required: true, + selector: { text: { type: "password" } }, + }, ]; @customElement("onboarding-create-user") diff --git a/src/onboarding/onboarding-integrations.ts b/src/onboarding/onboarding-integrations.ts index d6874a6ae8..6ac3504e5e 100644 --- a/src/onboarding/onboarding-integrations.ts +++ b/src/onboarding/onboarding-integrations.ts @@ -30,7 +30,7 @@ import { HomeAssistant } from "../types"; import "./action-badge"; import "./integration-badge"; -const HIDDEN_DOMAINS = new Set(["met", "rpi_power", "hassio"]); +const HIDDEN_DOMAINS = new Set(["hassio", "met", "radio_browser", "rpi_power"]); @customElement("onboarding-integrations") class OnboardingIntegrations extends LitElement { @@ -140,8 +140,6 @@ class OnboardingIntegrations extends LitElement { this._scanUSBDevices(); loadConfigFlowDialog(); this._loadConfigEntries(); - /* polyfill for paper-dropdown */ - import("web-animations-js/web-animations-next-lite.min"); } private _createFlow() { diff --git a/src/panels/config/areas/dialog-area-registry-detail.ts b/src/panels/config/areas/dialog-area-registry-detail.ts index 9bbb35de18..fcf95fc6c1 100644 --- a/src/panels/config/areas/dialog-area-registry-detail.ts +++ b/src/panels/config/areas/dialog-area-registry-detail.ts @@ -1,10 +1,10 @@ import "@material/mwc-button"; -import "@polymer/paper-input/paper-input"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { property, state } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; import { createCloseHeading } from "../../../components/ha-dialog"; import "../../../components/ha-alert"; +import "../../../components/ha-textfield"; import "../../../components/ha-picture-upload"; import type { HaPictureUpload } from "../../../components/ha-picture-upload"; import { AreaRegistryEntryMutableParams } from "../../../data/area_registry"; @@ -69,7 +69,7 @@ class DialogAreaDetail extends LitElement { >
    ${this._error - ? html` ${this._error} ` + ? html`${this._error}` : ""}
    ${entry @@ -83,16 +83,16 @@ class DialogAreaDetail extends LitElement { ` : ""} - + dialogInitialFocus + > ) { + private _nameChanged(ev) { this._error = undefined; - this._name = ev.detail.value; + this._name = ev.target.value; } private _pictureChanged(ev: PolymerChangedEvent) { @@ -187,6 +181,10 @@ class DialogAreaDetail extends LitElement { .form { padding-bottom: 24px; } + ha-textfield { + display: block; + margin-bottom: 16px; + } `, ]; } diff --git a/src/panels/config/automation/action/ha-automation-action-row.ts b/src/panels/config/automation/action/ha-automation-action-row.ts index 31cbd74e7c..1844341245 100644 --- a/src/panels/config/automation/action/ha-automation-action-row.ts +++ b/src/panels/config/automation/action/ha-automation-action-row.ts @@ -1,8 +1,6 @@ import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import "@material/mwc-list/mwc-list-item"; import { mdiArrowDown, mdiArrowUp, mdiDotsVertical } from "@mdi/js"; -import "@material/mwc-select"; -import type { Select } from "@material/mwc-select"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import memoizeOne from "memoize-one"; @@ -11,22 +9,25 @@ import { fireEvent } from "../../../../common/dom/fire_event"; import { stringCompare } from "../../../../common/string/compare"; import { handleStructError } from "../../../../common/structs/handle-errors"; import { LocalizeFunc } from "../../../../common/translations/localize"; +import "../../../../components/ha-alert"; import "../../../../components/ha-button-menu"; import "../../../../components/ha-card"; -import "../../../../components/ha-alert"; import "../../../../components/ha-icon-button"; +import "../../../../components/ha-select"; +import type { HaSelect } from "../../../../components/ha-select"; import type { HaYamlEditor } from "../../../../components/ha-yaml-editor"; -import type { Action } from "../../../../data/script"; +import { Action, getActionType } from "../../../../data/script"; import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; import { haStyle } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; +import "./types/ha-automation-action-activate_scene"; import "./types/ha-automation-action-choose"; import "./types/ha-automation-action-condition"; import "./types/ha-automation-action-delay"; import "./types/ha-automation-action-device_id"; import "./types/ha-automation-action-event"; +import "./types/ha-automation-action-play_media"; import "./types/ha-automation-action-repeat"; -import "./types/ha-automation-action-scene"; import "./types/ha-automation-action-service"; import "./types/ha-automation-action-wait_for_trigger"; import "./types/ha-automation-action-wait_template"; @@ -35,7 +36,8 @@ const OPTIONS = [ "condition", "delay", "event", - "scene", + "play_media", + "activate_scene", "service", "wait_template", "wait_for_trigger", @@ -44,8 +46,15 @@ const OPTIONS = [ "device_id", ]; -const getType = (action: Action | undefined) => - action ? OPTIONS.find((option) => option in action) : undefined; +const getType = (action: Action | undefined) => { + if (!action) { + return undefined; + } + if ("service" in action || "scene" in action) { + return getActionType(action); + } + return OPTIONS.find((option) => option in action); +}; declare global { // for fire event @@ -64,7 +73,7 @@ export const handleChangeEvent = (element: ActionElement, ev: CustomEvent) => { if (!name) { return; } - const newVal = ev.detail.value; + const newVal = ev.detail?.value || (ev.target as any).value; if ((element.action[name] || "") === newVal) { return; @@ -113,24 +122,30 @@ export default class HaAutomationActionRow extends LitElement { ).sort((a, b) => stringCompare(a[1], b[1])) ); + protected willUpdate(changedProperties: PropertyValues) { + if (!changedProperties.has("action")) { + return; + } + this._uiModeAvailable = getType(this.action) !== undefined; + if (!this._uiModeAvailable && !this._yamlMode) { + this._yamlMode = true; + } + } + protected updated(changedProperties: PropertyValues) { if (!changedProperties.has("action")) { return; } - this._uiModeAvailable = Boolean(getType(this.action)); - if (!this._uiModeAvailable && !this._yamlMode) { - this._yamlMode = true; - } - - const yamlEditor = this._yamlEditor; - if (this._yamlMode && yamlEditor && yamlEditor.value !== this.action) { - yamlEditor.setValue(this.action); + if (this._yamlMode) { + const yamlEditor = this._yamlEditor; + if (yamlEditor && yamlEditor.value !== this.action) { + yamlEditor.setValue(this.action); + } } } protected render() { const type = getType(this.action); - const selected = type ? OPTIONS.indexOf(type) : -1; const yamlMode = this._yamlMode; return html` @@ -205,7 +220,7 @@ export default class HaAutomationActionRow extends LitElement { : ""} ${yamlMode ? html` - ${selected === -1 + ${type === undefined ? html` ${this.hass.localize( "ui.panel.config.automation.editor.actions.unsupported_action", @@ -220,12 +235,13 @@ export default class HaAutomationActionRow extends LitElement { )} ` : html` - ${label} ` )} - +
    ${dynamicElement(`ha-automation-action-${type}`, { @@ -299,7 +315,7 @@ export default class HaAutomationActionRow extends LitElement { } private _typeChanged(ev: CustomEvent) { - const type = (ev.target as Select).value; + const type = (ev.target as HaSelect).value; if (!type) { return; @@ -341,12 +357,14 @@ export default class HaAutomationActionRow extends LitElement { haStyle, css` .card-menu { - float: right; + position: absolute; + right: 16px; z-index: 3; --mdc-theme-text-primary-on-background: var(--primary-text-color); } .rtl .card-menu { - float: left; + right: initial; + left: 16px; } mwc-list-item[disabled] { --mdc-theme-text-primary-on-background: var(--disabled-text-color); @@ -357,6 +375,9 @@ export default class HaAutomationActionRow extends LitElement { .warning ul { margin: 4px 0; } + ha-select { + margin-bottom: 24px; + } `, ]; } diff --git a/src/panels/config/automation/action/types/ha-automation-action-scene.ts b/src/panels/config/automation/action/types/ha-automation-action-activate_scene.ts similarity index 69% rename from src/panels/config/automation/action/types/ha-automation-action-scene.ts rename to src/panels/config/automation/action/types/ha-automation-action-activate_scene.ts index 6af3f06c40..0ce146589a 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-scene.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-activate_scene.ts @@ -9,18 +9,30 @@ import { ActionElement } from "../ha-automation-action-row"; const includeDomains = ["scene"]; -@customElement("ha-automation-action-scene") +@customElement("ha-automation-action-activate_scene") export class HaSceneAction extends LitElement implements ActionElement { @property({ attribute: false }) public hass!: HomeAssistant; @property() public action!: SceneAction; public static get defaultConfig(): SceneAction { - return { scene: "" }; + return { + service: "scene.turn_on", + target: { + entity_id: "", + }, + metadata: {}, + }; } protected render() { - const { scene } = this.action; + let scene; + + if ("scene" in this.action) { + scene = this.action.scene; + } else { + scene = this.action.target?.entity_id; + } return html` ) { ev.stopPropagation(); fireEvent(this, "value-changed", { - value: { ...this.action, scene: ev.detail.value }, + value: { + service: "scene.turn_on", + target: { + entity_id: ev.detail.value, + }, + metadata: {}, + }, }); } } declare global { interface HTMLElementTagNameMap { - "ha-automation-action-scene": HaSceneAction; + "ha-automation-action-activate_scene": HaSceneAction; } } diff --git a/src/panels/config/automation/action/types/ha-automation-action-choose.ts b/src/panels/config/automation/action/types/ha-automation-action-choose.ts index f342512745..43e360c459 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-choose.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-choose.ts @@ -1,6 +1,4 @@ import { mdiDelete } from "@mdi/js"; -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-listbox/paper-listbox"; import { css, CSSResultGroup, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../../../common/dom/fire_event"; @@ -10,8 +8,8 @@ import { Condition } from "../../../../../data/automation"; import { Action, ChooseAction } from "../../../../../data/script"; import { haStyle } from "../../../../../resources/styles"; import { HomeAssistant } from "../../../../../types"; -import "../ha-automation-action"; import { ActionElement } from "../ha-automation-action-row"; +import "../../../../../components/ha-form/ha-form"; @customElement("ha-automation-action-choose") export class HaChooseAction extends LitElement implements ActionElement { @@ -61,12 +59,13 @@ export class HaChooseAction extends LitElement implements ActionElement { "ui.panel.config.automation.editor.actions.type.choose.sequence" )}: - + >
    ` )} @@ -107,7 +106,7 @@ export class HaChooseAction extends LitElement implements ActionElement { private _actionChanged(ev: CustomEvent) { ev.stopPropagation(); - const value = ev.detail.value as Action[]; + const value = ev.detail.value.sequence as Action[]; const index = (ev.target as any).idx; const choose = this.action.choose ? [...ensureArray(this.action.choose)] diff --git a/src/panels/config/automation/action/types/ha-automation-action-delay.ts b/src/panels/config/automation/action/types/ha-automation-action-delay.ts index 488494e65e..2d71065f8b 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-delay.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-delay.ts @@ -15,7 +15,7 @@ export class HaDelayAction extends LitElement implements ActionElement { @property() public action!: DelayAction; - @property() public _timeData!: HaDurationData; + @property() public _timeData?: HaDurationData; public static get defaultConfig() { return { delay: "" }; diff --git a/src/panels/config/automation/action/types/ha-automation-action-device_id.ts b/src/panels/config/automation/action/types/ha-automation-action-device_id.ts index 1a4565f870..c837304ea9 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-device_id.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-device_id.ts @@ -1,4 +1,4 @@ -import { html, LitElement } from "lit"; +import { css, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../../common/dom/fire_event"; @@ -69,6 +69,7 @@ export class HaDeviceAction extends LitElement { ${this._capabilities?.extra_fields ? html` + @change=${this._eventChanged} + > ({ + entity_id: action.target?.entity_id || action.entity_id, + media_content_id: action.data?.media_content_id, + media_content_type: action.data?.media_content_type, + metadata: action.metadata, + }) + ); + + protected render() { + return html` + + `; + } + + private _valueChanged(ev: CustomEvent<{ value: MediaSelectorValue }>) { + ev.stopPropagation(); + fireEvent(this, "value-changed", { + value: { + service: "media_player.play_media", + target: { entity_id: ev.detail.value.entity_id }, + data: { + media_content_id: ev.detail.value.media_content_id, + media_content_type: ev.detail.value.media_content_type, + }, + metadata: ev.detail.value.metadata || {}, + } as PlayMediaAction, + }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-action-play_media": HaPlayMediaAction; + } +} diff --git a/src/panels/config/automation/action/types/ha-automation-action-repeat.ts b/src/panels/config/automation/action/types/ha-automation-action-repeat.ts index 07249aeaf8..13942a211a 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-repeat.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-repeat.ts @@ -1,7 +1,4 @@ -import "@polymer/paper-input/paper-input"; -import type { PaperListboxElement } from "@polymer/paper-listbox"; -import "@polymer/paper-listbox/paper-listbox"; -import { CSSResultGroup, html, LitElement } from "lit"; +import { css, CSSResultGroup, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../../../common/dom/fire_event"; import { @@ -12,10 +9,11 @@ import { WhileRepeat, } from "../../../../../data/script"; import { haStyle } from "../../../../../resources/styles"; -import { HomeAssistant } from "../../../../../types"; -import { Condition } from "../../../../lovelace/common/validate-condition"; +import type { HomeAssistant } from "../../../../../types"; +import type { Condition } from "../../../../lovelace/common/validate-condition"; import "../ha-automation-action"; -import { ActionElement } from "../ha-automation-action-row"; +import "../../../../../components/ha-textfield"; +import type { ActionElement } from "../ha-automation-action-row"; const OPTIONS = ["count", "while", "until"]; @@ -35,40 +33,36 @@ export class HaRepeatAction extends LitElement implements ActionElement { const action = this.action.repeat; const type = getType(action); - const selected = type ? OPTIONS.indexOf(type) : -1; return html` - - - ${OPTIONS.map( - (opt) => html` - - ${this.hass.localize( - `ui.panel.config.automation.editor.actions.type.repeat.type.${opt}.label` - )} - - ` - )} - - + ${OPTIONS.map( + (opt) => html` + + ${this.hass.localize( + `ui.panel.config.automation.editor.actions.type.repeat.type.${opt}.label` + )} + + ` + )} + ${type === "count" - ? html`` + ? html` + + ` : ""} ${type === "while" ? html`

    @@ -107,9 +101,8 @@ export class HaRepeatAction extends LitElement implements ActionElement { `; } - private _typeChanged(ev: CustomEvent) { - const type = ((ev.target as PaperListboxElement)?.selectedItem as any) - ?.action; + private _typeChanged(ev) { + const type = ev.target.value; if (!type || type === getType(this.action.repeat)) { return; @@ -151,7 +144,7 @@ export class HaRepeatAction extends LitElement implements ActionElement { } private _countChanged(ev: CustomEvent): void { - const newVal = ev.detail.value; + const newVal = (ev.target as any).value; if ((this.action.repeat as CountRepeat).count === newVal) { return; } @@ -166,7 +159,14 @@ export class HaRepeatAction extends LitElement implements ActionElement { } static get styles(): CSSResultGroup { - return haStyle; + return [ + haStyle, + css` + ha-select { + margin-top: 8px; + } + `, + ]; } } diff --git a/src/panels/config/automation/action/types/ha-automation-action-service.ts b/src/panels/config/automation/action/types/ha-automation-action-service.ts index c67e94195f..fdac1e1824 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-service.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-service.ts @@ -1,4 +1,3 @@ -import "@polymer/paper-input/paper-input"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { customElement, property, state } from "lit/decorators"; import { any, assert, object, optional, string } from "superstruct"; @@ -31,7 +30,7 @@ export class HaServiceAction extends LitElement implements ActionElement { return { service: "", data: {} }; } - protected updated(changedProperties: PropertyValues) { + protected willUpdate(changedProperties: PropertyValues) { if (!changedProperties.has("action")) { return; } diff --git a/src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger.ts b/src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger.ts index 997ab3e752..2b76b0918c 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger.ts @@ -1,6 +1,5 @@ -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-input/paper-textarea"; -import { html, LitElement } from "lit"; +import "../../../../../components/ha-textfield"; +import { css, CSSResultGroup, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../../../common/dom/fire_event"; import "../../../../../components/ha-formfield"; @@ -26,14 +25,14 @@ export class HaWaitForTriggerAction const { wait_for_trigger, continue_on_timeout, timeout } = this.action; return html` - + .value=${timeout || ""} + @change=${this._valueChanged} + >
    - -
    - - - + `; } - private _continueChanged(ev) { - fireEvent(this, "value-changed", { - value: { ...this.action, continue_on_timeout: ev.target.checked }, - }); - } - - private _valueChanged(ev: CustomEvent): void { - handleChangeEvent(this, ev); - } + private _computeLabelCallback = (schema: HaFormSchema): string => + this.hass.localize( + `ui.panel.config.automation.editor.actions.type.wait_template.${ + schema.name === "continue_on_timeout" ? "continue_timeout" : schema.name + }` + ); } declare global { diff --git a/src/panels/config/automation/blueprint-automation-editor.ts b/src/panels/config/automation/blueprint-automation-editor.ts index f0179d2fbd..61db1916fb 100644 --- a/src/panels/config/automation/blueprint-automation-editor.ts +++ b/src/panels/config/automation/blueprint-automation-editor.ts @@ -1,5 +1,4 @@ import "@material/mwc-button/mwc-button"; -import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; import "@polymer/paper-input/paper-textarea"; import { HassEntity } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement } from "lit"; diff --git a/src/panels/config/automation/condition/ha-automation-condition-editor.ts b/src/panels/config/automation/condition/ha-automation-condition-editor.ts index e7977694f3..80d2f58ac3 100644 --- a/src/panels/config/automation/condition/ha-automation-condition-editor.ts +++ b/src/panels/config/automation/condition/ha-automation-condition-editor.ts @@ -1,13 +1,13 @@ -import { CSSResultGroup, html, LitElement } from "lit"; +import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; -import "@material/mwc-select"; -import type { Select } from "@material/mwc-select"; import memoizeOne from "memoize-one"; import { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; import { fireEvent } from "../../../../common/dom/fire_event"; import { stringCompare } from "../../../../common/string/compare"; -import { LocalizeFunc } from "../../../../common/translations/localize"; +import type { LocalizeFunc } from "../../../../common/translations/localize"; import "../../../../components/ha-card"; +import "../../../../components/ha-select"; +import type { HaSelect } from "../../../../components/ha-select"; import "../../../../components/ha-yaml-editor"; import type { Condition } from "../../../../data/automation"; import { haStyle } from "../../../../resources/styles"; @@ -80,12 +80,13 @@ export default class HaAutomationConditionEditor extends LitElement { )}

    ` : html` - ${label} ` )} - +
    ${dynamicElement( @@ -111,7 +112,7 @@ export default class HaAutomationConditionEditor extends LitElement { } private _typeChanged(ev: CustomEvent) { - const type = (ev.target as Select).value; + const type = (ev.target as HaSelect).value; if (!type) { return; @@ -142,9 +143,14 @@ export default class HaAutomationConditionEditor extends LitElement { fireEvent(this, "value-changed", { value: ev.detail.value, yaml: true }); } - static get styles(): CSSResultGroup { - return haStyle; - } + static styles = [ + haStyle, + css` + ha-select { + margin-bottom: 24px; + } + `, + ]; } declare global { diff --git a/src/panels/config/automation/condition/ha-automation-condition-row.ts b/src/panels/config/automation/condition/ha-automation-condition-row.ts index 3e792abd18..4bce08a441 100644 --- a/src/panels/config/automation/condition/ha-automation-condition-row.ts +++ b/src/panels/config/automation/condition/ha-automation-condition-row.ts @@ -1,7 +1,6 @@ import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import "@material/mwc-list/mwc-list-item"; import { mdiDotsVertical } from "@mdi/js"; -import "@polymer/paper-item/paper-item"; import { css, CSSResultGroup, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; @@ -24,11 +23,11 @@ export const handleChangeEvent = ( ev: CustomEvent ) => { ev.stopPropagation(); - const name = (ev.target as any)?.name; + const name = (ev.currentTarget as any)?.name; if (!name) { return; } - const newVal = ev.detail.value; + const newVal = ev.detail?.value || (ev.currentTarget as any)?.value; if ((element.condition[name] || "") === newVal) { return; diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-device.ts b/src/panels/config/automation/condition/types/ha-automation-condition-device.ts index 74277a4680..83c543d489 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-device.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-device.ts @@ -1,4 +1,4 @@ -import { html, LitElement } from "lit"; +import { css, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../../common/dom/fire_event"; @@ -11,7 +11,7 @@ import { DeviceCondition, fetchDeviceConditionCapabilities, } from "../../../../../data/device_automation"; -import { HomeAssistant } from "../../../../../types"; +import type { HomeAssistant } from "../../../../../types"; @customElement("ha-automation-condition-device") export class HaDeviceCondition extends LitElement { @@ -69,6 +69,7 @@ export class HaDeviceCondition extends LitElement { ${this._capabilities?.extra_fields ? html` [ + { name: "entity_id", required: true, selector: { entity: {} } }, + { + name: "attribute", + selector: { attribute: { entity_id: entityId } }, + }, + { name: "above", selector: { text: {} } }, + { name: "below", selector: { text: {} } }, + { + name: "value_template", + selector: { text: { multiline: true } }, + }, + ]); + public render() { - const { value_template, entity_id, attribute, below, above } = - this.condition; + const schema = this._schema(this.condition.entity_id); return html` - - - - - + .computeLabel=${this._computeLabelCallback} + > `; } private _valueChanged(ev: CustomEvent): void { - handleChangeEvent(this, ev); + ev.stopPropagation(); + const newTrigger = ev.detail.value; + fireEvent(this, "value-changed", { value: newTrigger }); } + + private _computeLabelCallback = (schema: HaFormSchema): string => { + switch (schema.name) { + case "entity_id": + return this.hass.localize("ui.components.entity.entity-picker.entity"); + case "attribute": + return this.hass.localize( + "ui.components.entity.entity-attribute-picker.attribute" + ); + default: + return this.hass.localize( + `ui.panel.config.automation.editor.triggers.type.numeric_state.${schema.name}` + ); + } + }; } declare global { diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-state.ts b/src/panels/config/automation/condition/types/ha-automation-condition-state.ts index 4a6e5a86ef..61e064ff4c 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-state.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-state.ts @@ -1,19 +1,15 @@ -import "@polymer/paper-input/paper-input"; import { html, LitElement, PropertyValues } from "lit"; import { customElement, property } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { assert, literal, object, optional, string, union } from "superstruct"; import { createDurationData } from "../../../../../common/datetime/create_duration_data"; import { fireEvent } from "../../../../../common/dom/fire_event"; -import "../../../../../components/entity/ha-entity-attribute-picker"; -import "../../../../../components/entity/ha-entity-picker"; -import "../../../../../components/ha-duration-input"; -import { StateCondition } from "../../../../../data/automation"; -import { HomeAssistant } from "../../../../../types"; +import type { HaFormSchema } from "../../../../../components/ha-form/types"; +import type { StateCondition } from "../../../../../data/automation"; +import type { HomeAssistant } from "../../../../../types"; import { forDictStruct } from "../../structs"; -import { - ConditionElement, - handleChangeEvent, -} from "../ha-automation-condition-row"; +import type { ConditionElement } from "../ha-automation-condition-row"; +import "../../../../../components/ha-form/ha-form"; const stateConditionStruct = object({ condition: literal("state"), @@ -27,12 +23,22 @@ const stateConditionStruct = object({ export class HaStateCondition extends LitElement implements ConditionElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public condition!: StateCondition; + @property({ attribute: false }) public condition!: StateCondition; public static get defaultConfig() { return { entity_id: "", state: "" }; } + private _schema = memoizeOne((entityId) => [ + { name: "entity_id", required: true, selector: { entity: {} } }, + { + name: "attribute", + selector: { attribute: { entity_id: entityId } }, + }, + { name: "state", selector: { text: {} } }, + { name: "for", selector: { duration: {} } }, + ]); + public shouldUpdate(changedProperties: PropertyValues) { if (changedProperties.has("condition")) { try { @@ -46,50 +52,52 @@ export class HaStateCondition extends LitElement implements ConditionElement { } protected render() { - const { entity_id, attribute, state } = this.condition; - const forTime = createDurationData(this.condition.for); + const trgFor = createDurationData(this.condition.for); + const data = { ...this.condition, for: trgFor }; + const schema = this._schema(this.condition.entity_id); return html` - - - - + .computeLabel=${this._computeLabelCallback} + > `; } private _valueChanged(ev: CustomEvent): void { - handleChangeEvent(this, ev); + ev.stopPropagation(); + const newTrigger = ev.detail.value; + + Object.keys(newTrigger).forEach((key) => + newTrigger[key] === undefined || newTrigger[key] === "" + ? delete newTrigger[key] + : {} + ); + + fireEvent(this, "value-changed", { value: newTrigger }); } + + private _computeLabelCallback = (schema: HaFormSchema): string => { + switch (schema.name) { + case "entity_id": + return this.hass.localize("ui.components.entity.entity-picker.entity"); + case "attribute": + return this.hass.localize( + "ui.components.entity.entity-attribute-picker.attribute" + ); + case "for": + return this.hass.localize( + `ui.panel.config.automation.editor.triggers.type.state.for` + ); + default: + return this.hass.localize( + `ui.panel.config.automation.editor.conditions.type.state.${schema.name}` + ); + } + }; } declare global { diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-sun.ts b/src/panels/config/automation/condition/types/ha-automation-condition-sun.ts index 0cbf5a339a..e8966b6969 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-sun.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-sun.ts @@ -1,16 +1,13 @@ -import "@polymer/paper-input/paper-input"; -import { css, html, LitElement } from "lit"; +import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../../common/dom/fire_event"; import type { SunCondition } from "../../../../../data/automation"; import type { HomeAssistant } from "../../../../../types"; -import { - ConditionElement, - handleChangeEvent, -} from "../ha-automation-condition-row"; -import "../../../../../components/ha-radio"; -import "../../../../../components/ha-formfield"; -import type { HaRadio } from "../../../../../components/ha-radio"; +import type { ConditionElement } from "../ha-automation-condition-row"; +import type { LocalizeFunc } from "../../../../../common/translations/localize"; +import type { HaFormSchema } from "../../../../../components/ha-form/types"; +import "../../../../../components/ha-form/ha-form"; @customElement("ha-automation-condition-sun") export class HaSunCondition extends LitElement implements ConditionElement { @@ -22,111 +19,72 @@ export class HaSunCondition extends LitElement implements ConditionElement { return {}; } + private _schema = memoizeOne((localize: LocalizeFunc) => [ + { + name: "before", + type: "select", + required: true, + options: [ + [ + "sunrise", + localize( + "ui.panel.config.automation.editor.conditions.type.sun.sunrise" + ), + ], + [ + "sunset", + localize( + "ui.panel.config.automation.editor.conditions.type.sun.sunset" + ), + ], + ], + }, + { name: "before_offset", selector: { text: {} } }, + { + name: "after", + type: "select", + required: true, + options: [ + [ + "sunrise", + localize( + "ui.panel.config.automation.editor.conditions.type.sun.sunrise" + ), + ], + [ + "sunset", + localize( + "ui.panel.config.automation.editor.conditions.type.sun.sunset" + ), + ], + ], + }, + { name: "after_offset", selector: { text: {} } }, + ]); + protected render() { - const { after, after_offset, before, before_offset } = this.condition; + const schema = this._schema(this.hass.localize); return html` - - - - - - - + > `; } private _valueChanged(ev: CustomEvent): void { - handleChangeEvent(this, ev); - } - - private _radioGroupPicked(ev: CustomEvent) { - const key = (ev.target as HaRadio).name; ev.stopPropagation(); - fireEvent(this, "value-changed", { - value: { - ...this.condition, - [key]: (ev.target as HaRadio).value, - }, - }); + const newTrigger = ev.detail.value; + fireEvent(this, "value-changed", { value: newTrigger }); } - static styles = css` - label { - display: flex; - align-items: center; - } - `; + private _computeLabelCallback = (schema: HaFormSchema): string => + this.hass.localize( + `ui.panel.config.automation.editor.conditions.type.sun.${schema.name}` + ); } declare global { diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-template.ts b/src/panels/config/automation/condition/types/ha-automation-condition-template.ts index d83eea64e2..6ada82f152 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-template.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-template.ts @@ -1,15 +1,15 @@ -import "@polymer/paper-input/paper-textarea"; -import { html, LitElement } from "lit"; +import "../../../../../components/ha-textarea"; +import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; -import { TemplateCondition } from "../../../../../data/automation"; -import { HomeAssistant } from "../../../../../types"; +import type { TemplateCondition } from "../../../../../data/automation"; +import type { HomeAssistant } from "../../../../../types"; import { handleChangeEvent } from "../ha-automation-condition-row"; @customElement("ha-automation-condition-template") export class HaTemplateCondition extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public condition!: TemplateCondition; + @property({ attribute: false }) public condition!: TemplateCondition; public static get defaultConfig() { return { value_template: "" }; @@ -18,19 +18,32 @@ export class HaTemplateCondition extends LitElement { protected render() { const { value_template } = this.condition; return html` - + autogrow + > `; } private _valueChanged(ev: CustomEvent): void { handleChangeEvent(this, ev); } + + static styles = css` + ha-textarea { + display: block; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-condition-template": HaTemplateCondition; + } } diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-time.ts b/src/panels/config/automation/condition/types/ha-automation-condition-time.ts index f227847a3f..fca4616a7e 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-time.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-time.ts @@ -1,20 +1,13 @@ -import { Radio } from "@material/mwc-radio"; -import { css, CSSResultGroup, html, LitElement } from "lit"; +import { html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../../common/dom/fire_event"; -import { computeRTLDirection } from "../../../../../common/util/compute_rtl"; -import "../../../../../components/ha-formfield"; -import "../../../../../components/ha-radio"; -import { HaSwitch } from "../../../../../components/ha-switch"; -import { TimeCondition } from "../../../../../data/automation"; -import { HomeAssistant } from "../../../../../types"; -import { - ConditionElement, - handleChangeEvent, -} from "../ha-automation-condition-row"; -import "../../../../../components/ha-time-input"; - -const includeDomains = ["input_datetime"]; +import type { TimeCondition } from "../../../../../data/automation"; +import type { HomeAssistant } from "../../../../../types"; +import type { ConditionElement } from "../ha-automation-condition-row"; +import type { LocalizeFunc } from "../../../../../common/translations/localize"; +import type { HaFormSchema } from "../../../../../components/ha-form/types"; +import "../../../../../components/ha-form/ha-form"; const DAYS = { mon: 1, @@ -26,10 +19,6 @@ const DAYS = { sun: 7, }; -interface WeekdayHaSwitch extends HaSwitch { - day: string; -} - @customElement("ha-automation-condition-time") export class HaTimeCondition extends LitElement implements ConditionElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -44,176 +33,136 @@ export class HaTimeCondition extends LitElement implements ConditionElement { return {}; } - protected render() { - const { after, before, weekday } = this.condition; + private _schema = memoizeOne( + ( + localize: LocalizeFunc, + inputModeAfter?: boolean, + inputModeBefore?: boolean + ): HaFormSchema[] => { + const modeAfterSchema = inputModeAfter + ? { name: "after", selector: { entity: { domain: "input_datetime" } } } + : { name: "after", selector: { time: {} } }; + const modeBeforeSchema = inputModeBefore + ? { name: "before", selector: { entity: { domain: "input_datetime" } } } + : { name: "before", selector: { time: {} } }; + + return [ + { + name: "mode_after", + type: "select", + required: true, + options: [ + [ + "value", + localize( + "ui.panel.config.automation.editor.conditions.type.time.type_value" + ), + ], + [ + "input", + localize( + "ui.panel.config.automation.editor.conditions.type.time.type_input" + ), + ], + ], + }, + modeAfterSchema, + { + name: "mode_before", + type: "select", + required: true, + options: [ + [ + "value", + localize( + "ui.panel.config.automation.editor.conditions.type.time.type_value" + ), + ], + [ + "input", + localize( + "ui.panel.config.automation.editor.conditions.type.time.type_input" + ), + ], + ], + }, + modeBeforeSchema, + { + type: "multi_select", + name: "weekday", + options: Object.keys(DAYS).map((day) => [ + day, + localize( + `ui.panel.config.automation.editor.conditions.type.time.weekdays.${day}` + ), + ]), + }, + ]; + } + ); + + protected render() { const inputModeBefore = - this._inputModeBefore ?? before?.startsWith("input_datetime."); + this._inputModeBefore ?? + this.condition.before?.startsWith("input_datetime."); const inputModeAfter = - this._inputModeAfter ?? after?.startsWith("input_datetime."); + this._inputModeAfter ?? + this.condition.after?.startsWith("input_datetime."); + + const schema: HaFormSchema[] = this._schema( + this.hass.localize, + inputModeAfter, + inputModeBefore + ); + + const data = { + mode_before: "value", + mode_after: "value", + ...this.condition, + }; return html` - - - - - - - ${inputModeAfter - ? html`` - : html``} - - - - - - - - ${inputModeBefore - ? html`` - : html``} - ${Object.keys(DAYS).map( - (day) => html` - - - - - ` - )} + `; } - private _handleModeChanged(ev: Event) { - const target = ev.target as Radio; - if (target.getAttribute("name") === "mode_after") { - this._inputModeAfter = target.value === "input"; - } else { - this._inputModeBefore = target.value === "input"; - } - } - private _valueChanged(ev: CustomEvent): void { - handleChangeEvent(this, ev); - } + ev.stopPropagation(); + const newValue = ev.detail.value; - private _dayValueChanged(ev: CustomEvent): void { - const daySwitch = ev.currentTarget as WeekdayHaSwitch; + const newModeAfter = newValue.mode_after === "input"; + const newModeBefore = newValue.mode_before === "input"; - let days: string[]; - - if (!this.condition.weekday) { - days = Object.keys(DAYS); - } else { - days = !Array.isArray(this.condition.weekday) - ? [this.condition.weekday] - : this.condition.weekday; + if (newModeAfter !== this._inputModeAfter) { + this._inputModeAfter = newModeAfter; + newValue.after = undefined; } - if (daySwitch.checked) { - days.push(daySwitch.day); - } else { - days = days.filter((d) => d !== daySwitch.day); + if (newModeBefore !== this._inputModeBefore) { + this._inputModeBefore = newModeBefore; + newValue.before = undefined; } - days.sort((a: string, b: string) => DAYS[a] - DAYS[b]); + Object.keys(newValue).forEach((key) => + newValue[key] === undefined || newValue[key] === "" + ? delete newValue[key] + : {} + ); - fireEvent(this, "value-changed", { - value: { ...this.condition, weekday: days }, - }); + fireEvent(this, "value-changed", { value: newValue }); } - static get styles(): CSSResultGroup { - return css` - .weekday-toggle { - display: flex; - height: 40px; - } - `; - } + private _computeLabelCallback = (schema: HaFormSchema): string => + this.hass.localize( + `ui.panel.config.automation.editor.conditions.type.time.${schema.name}` + ); } declare global { diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-trigger.ts b/src/panels/config/automation/condition/types/ha-automation-condition-trigger.ts index 14adc8d117..cb80c3769b 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-trigger.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-trigger.ts @@ -1,15 +1,16 @@ -import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; +import "@material/mwc-list/mwc-list-item"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../../../common/dom/fire_event"; import { ensureArray } from "../../../../../common/ensure-array"; -import { +import "../../../../../components/ha-select"; +import type { AutomationConfig, Trigger, TriggerCondition, } from "../../../../../data/automation"; -import { HomeAssistant } from "../../../../../types"; +import type { HomeAssistant } from "../../../../../types"; @customElement("ha-automation-condition-trigger") export class HaTriggerCondition extends LitElement { @@ -17,7 +18,7 @@ export class HaTriggerCondition extends LitElement { @property({ attribute: false }) public condition!: TriggerCondition; - @state() private _triggers?: Trigger | Trigger[]; + @state() private _triggers: Trigger[] = []; private _unsub?: UnsubscribeFunc; @@ -43,46 +44,40 @@ export class HaTriggerCondition extends LitElement { protected render() { const { id } = this.condition; - if (!this._triggers) { + + if (!this._triggers.length) { return this.hass.localize( "ui.panel.config.automation.editor.conditions.type.trigger.no_triggers" ); } - return html` - - ${ensureArray(this._triggers).map((trigger) => - trigger.id - ? html` - - ${trigger.id} - - ` - : "" - )} - - `; + ${this._triggers.map( + (trigger) => + html` + ${trigger.id} + ` + )} + `; } private _automationUpdated(config?: AutomationConfig) { - this._triggers = config?.trigger; + this._triggers = config?.trigger + ? ensureArray(config.trigger).filter((t) => t.id) + : []; } - private _triggerPicked(ev: CustomEvent) { + private _triggerPicked(ev) { ev.stopPropagation(); - if (!ev.detail.value) { + if (!ev.target.value) { return; } - const newTrigger = ev.detail.value.dataset.triggerId; + const newTrigger = ev.target.value; if (this.condition.id === newTrigger) { return; } diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-zone.ts b/src/panels/config/automation/condition/types/ha-automation-condition-zone.ts index cf79928402..6c748351a9 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-zone.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-zone.ts @@ -1,4 +1,4 @@ -import { html, LitElement } from "lit"; +import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../../../common/dom/fire_event"; import { computeStateDomain } from "../../../../../common/entity/compute_state_domain"; @@ -71,6 +71,13 @@ export class HaZoneCondition extends LitElement { value: { ...this.condition, zone: ev.detail.value }, }); } + + static styles = css` + ha-entity-picker { + display: block; + margin-bottom: 24px; + } + `; } declare global { diff --git a/src/panels/config/automation/dialog-new-automation.ts b/src/panels/config/automation/dialog-new-automation.ts index 4c3b6723eb..daaa4a8c2a 100644 --- a/src/panels/config/automation/dialog-new-automation.ts +++ b/src/panels/config/automation/dialog-new-automation.ts @@ -94,10 +94,10 @@ class DialogNewAutomation extends LitElement implements HassDialog { haStyleDialog, css` mwc-list-item.blueprint { - height: 92px; + height: 110px; } ha-blueprint-picker { - margin-top: -16px; + margin-top: 8px; } ha-dialog { --dialog-content-padding: 0; diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index 97f7cc3999..9a6293175c 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -9,8 +9,6 @@ import { } from "@mdi/js"; import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-toolbar/app-toolbar"; -import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; -import "@polymer/paper-input/paper-textarea"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, @@ -202,7 +200,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { ${this._config ? html` ${this.narrow - ? html` ${this._config?.alias} ` + ? html`${this._config?.alias}` : ""}
    ${this._errors}
    ` + ? html`
    ${this._errors}
    ` : ""} ${this._mode === "gui" ? html` ${"use_blueprint" in this._config - ? html`` - : html``} + ? html` + + ` + : html` + + `} ` : this._mode === "yaml" ? html` @@ -269,6 +271,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { ` : ``} diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index cf06bc92ea..7c637b4dc7 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -7,7 +7,6 @@ import { mdiPlayCircleOutline, mdiPlus, } from "@mdi/js"; -import "@polymer/paper-tooltip/paper-tooltip"; import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; @@ -81,6 +80,9 @@ class HaAutomationPicker extends LitElement { const columns: DataTableColumnContainer = { toggle: { title: "", + label: this.hass.localize( + "ui.panel.config.automation.picker.headers.toggle" + ), type: "icon", template: (_toggle, automation: any) => html` @@ -127,6 +129,9 @@ class HaAutomationPicker extends LitElement { `, }; columns.trigger = { + label: this.hass.localize( + "ui.panel.config.automation.picker.headers.trigger" + ), title: html` ${this.hass.localize("ui.card.automation.trigger")} @@ -146,6 +151,9 @@ class HaAutomationPicker extends LitElement { } columns.actions = { title: "", + label: this.hass.localize( + "ui.panel.config.automation.picker.headers.actions" + ), type: "overflow-menu", template: (_info, automation: any) => html` ${!this.narrow - ? html` ${this.config.alias} ` + ? html`${this.config.alias}` : ""} ${this.hass.localize( @@ -47,26 +48,39 @@ export class HaManualAutomationEditor extends LitElement {
    - - - + + ${this._showDescription + ? html` + + ` + : html` + + `}

    ${this.hass.localize( "ui.panel.config.automation.editor.modes.description", @@ -81,41 +95,38 @@ export class HaManualAutomationEditor extends LitElement { >` )}

    - - - ${MODES.map( - (mode) => html` - - ${this.hass.localize( - `ui.panel.config.automation.editor.modes.${mode}` - ) || mode} - - ` - )} - - + ${MODES.map( + (mode) => html` + + ${this.hass.localize( + `ui.panel.config.automation.editor.modes.${mode}` + ) || mode} + + ` + )} + ${this.config.mode && MODES_MAX.includes(this.config.mode) - ? html` - ` + ? html` +
    + + ` : html``}
    ${this.stateObj @@ -240,6 +251,17 @@ export class HaManualAutomationEditor extends LitElement { `; } + protected willUpdate(changedProps: PropertyValues): void { + super.willUpdate(changedProps); + if ( + !this._showDescription && + changedProps.has("config") && + this.config.description + ) { + this._showDescription = true; + } + } + private _runActions(ev: Event) { triggerAutomationActions(this.hass, (ev.target as any).stateObj.entity_id); } @@ -251,7 +273,7 @@ export class HaManualAutomationEditor extends LitElement { if (!name) { return; } - let newVal = ev.detail.value; + let newVal = target.value; if (target.type === "number") { newVal = Number(newVal); } @@ -263,9 +285,8 @@ export class HaManualAutomationEditor extends LitElement { }); } - private _modeChanged(ev: CustomEvent) { - const mode = ((ev.target as PaperListboxElement)?.selectedItem as any) - ?.mode; + private _modeChanged(ev) { + const mode = ev.target.value; if ( mode === this.config!.mode || @@ -311,6 +332,10 @@ export class HaManualAutomationEditor extends LitElement { }); } + private _addDescription() { + this._showDescription = true; + } + static get styles(): CSSResultGroup { return [ haStyle, @@ -318,6 +343,12 @@ export class HaManualAutomationEditor extends LitElement { ha-card { overflow: hidden; } + .link-button-row { + padding: 14px; + } + ha-textarea { + display: block; + } span[slot="introduction"] a { color: var(--primary-color); } @@ -327,6 +358,11 @@ export class HaManualAutomationEditor extends LitElement { ha-entity-toggle { margin-right: 8px; } + ha-select, + .max { + margin-top: 16px; + width: 200px; + } `, ]; } diff --git a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts index b8f69ee4c2..7844e7ba09 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts @@ -1,21 +1,26 @@ import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import "@material/mwc-list/mwc-list-item"; import { mdiDotsVertical } from "@mdi/js"; -import "@material/mwc-select"; -import type { Select } from "@material/mwc-select"; -import { css, CSSResultGroup, html, LitElement } from "lit"; +import type { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; import { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; import { fireEvent } from "../../../../common/dom/fire_event"; import { stringCompare } from "../../../../common/string/compare"; import { handleStructError } from "../../../../common/structs/handle-errors"; import { LocalizeFunc } from "../../../../common/translations/localize"; +import { debounce } from "../../../../common/util/debounce"; +import "../../../../components/ha-alert"; import "../../../../components/ha-button-menu"; import "../../../../components/ha-card"; -import "../../../../components/ha-alert"; import "../../../../components/ha-icon-button"; -import type { Trigger } from "../../../../data/automation"; +import "../../../../components/ha-select"; +import type { HaSelect } from "../../../../components/ha-select"; +import "../../../../components/ha-textfield"; +import { subscribeTrigger, Trigger } from "../../../../data/automation"; +import { validateConfig } from "../../../../data/config"; import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; import { haStyle } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; @@ -57,11 +62,11 @@ export interface TriggerElement extends LitElement { export const handleChangeEvent = (element: TriggerElement, ev: CustomEvent) => { ev.stopPropagation(); - const name = (ev.target as any)?.name; + const name = (ev.currentTarget as any)?.name; if (!name) { return; } - const newVal = ev.detail.value; + const newVal = (ev.target as any)?.value; if ((element.trigger[name] || "") === newVal) { return; @@ -89,6 +94,12 @@ export default class HaAutomationTriggerRow extends LitElement { @state() private _requestShowId = false; + @state() private _triggered = false; + + @state() private _triggerColor = false; + + private _triggerUnsub?: Promise; + private _processedTypes = memoizeOne( (localize: LocalizeFunc): [string, string][] => OPTIONS.map( @@ -177,12 +188,13 @@ export default class HaAutomationTriggerRow extends LitElement { )} ` : html` - ${label} ` )} - + ${showId ? html` - - + ` : ""}
    @@ -217,10 +229,98 @@ export default class HaAutomationTriggerRow extends LitElement {
    `}
    +
    + ${this.hass.localize( + "ui.panel.config.automation.editor.triggers.triggered" + )} +
    `; } + protected override updated(changedProps: PropertyValues): void { + super.updated(changedProps); + if (changedProps.has("trigger")) { + this._subscribeTrigger(); + } + } + + public connectedCallback(): void { + super.connectedCallback(); + if (this.hasUpdated && this.trigger) { + this._subscribeTrigger(); + } + } + + public disconnectedCallback(): void { + super.disconnectedCallback(); + if (this._triggerUnsub) { + this._triggerUnsub.then((unsub) => unsub()); + this._triggerUnsub = undefined; + } + this._doSubscribeTrigger.cancel(); + } + + private _subscribeTrigger() { + // Clean up old trigger subscription. + if (this._triggerUnsub) { + this._triggerUnsub.then((unsub) => unsub()); + this._triggerUnsub = undefined; + } + + this._doSubscribeTrigger(); + } + + private _doSubscribeTrigger = debounce(async () => { + let untriggerTimeout: number | undefined; + const showTriggeredTime = 5000; + const trigger = this.trigger; + + // Clean up old trigger subscription. + if (this._triggerUnsub) { + this._triggerUnsub.then((unsub) => unsub()); + this._triggerUnsub = undefined; + } + + const validateResult = await validateConfig(this.hass, { + trigger: this.trigger, + }); + + // Don't do anything if trigger not valid or if trigger changed. + if (!validateResult.trigger.valid || this.trigger !== trigger) { + return; + } + + const triggerUnsub = subscribeTrigger( + this.hass, + () => { + if (untriggerTimeout !== undefined) { + clearTimeout(untriggerTimeout); + this._triggerColor = !this._triggerColor; + } else { + this._triggerColor = false; + } + this._triggered = true; + untriggerTimeout = window.setTimeout(() => { + this._triggered = false; + untriggerTimeout = undefined; + }, showTriggeredTime); + }, + trigger + ); + triggerUnsub.catch(() => { + if (this._triggerUnsub === triggerUnsub) { + this._triggerUnsub = undefined; + } + }); + this._triggerUnsub = triggerUnsub; + }, 5000); + private _handleUiModeNotAvailable(ev: CustomEvent) { this._warnings = handleStructError(this.hass, ev.detail).warnings; if (!this._yamlMode) { @@ -259,7 +359,7 @@ export default class HaAutomationTriggerRow extends LitElement { } private _typeChanged(ev: CustomEvent) { - const type = (ev.target as Select).value; + const type = (ev.target as HaSelect).value; if (!type) { return; @@ -286,7 +386,7 @@ export default class HaAutomationTriggerRow extends LitElement { } private _idChanged(ev: CustomEvent) { - const newId = ev.detail.value; + const newId = (ev.target as any).value; if (newId === (this.trigger.id ?? "")) { return; } @@ -325,12 +425,44 @@ export default class HaAutomationTriggerRow extends LitElement { z-index: 3; --mdc-theme-text-primary-on-background: var(--primary-text-color); } + .triggered { + position: absolute; + top: 0px; + right: 0px; + left: 0px; + text-transform: uppercase; + pointer-events: none; + font-weight: bold; + font-size: 14px; + background-color: var(--primary-color); + color: var(--text-primary-color); + max-height: 0px; + overflow: hidden; + transition: max-height 0.3s; + text-align: center; + border-top-right-radius: var(--ha-card-border-radius, 4px); + border-top-left-radius: var(--ha-card-border-radius, 4px); + } + .triggered.active { + max-height: 100px; + } + .triggered.accent { + background-color: var(--accent-color); + color: var(--text-accent-color, var(--text-primary-color)); + } .rtl .card-menu { float: left; } mwc-list-item[disabled] { --mdc-theme-text-primary-on-background: var(--disabled-text-color); } + ha-select { + margin-bottom: 24px; + } + ha-textfield { + display: block; + margin-bottom: 24px; + } `, ]; } diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-device.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-device.ts index 6dc84a01f2..bf5f1523bb 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-device.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-device.ts @@ -1,4 +1,4 @@ -import { html, LitElement } from "lit"; +import { css, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../../common/dom/fire_event"; @@ -69,6 +69,7 @@ export class HaDeviceTrigger extends LitElement { ${this._capabilities?.extra_fields ? html` + @change=${this._valueChanged} + > [ + { name: "source", selector: { text: {} } }, + { name: "zone", selector: { entity: { domain: "zone" } } }, + { + name: "event", + type: "select", + required: true, + options: [ + [ + "enter", + localize( + "ui.panel.config.automation.editor.triggers.type.geo_location.enter" + ), + ], + [ + "leave", + localize( + "ui.panel.config.automation.editor.triggers.type.geo_location.leave" + ), + ], + ], + }, + ]); + public static get defaultConfig() { return { source: "", @@ -24,86 +47,27 @@ export class HaGeolocationTrigger extends LitElement { } protected render() { - const { source, zone, event } = this.trigger; - return html` - - - + .computeLabel=${this._computeLabelCallback} + @value-changed=${this._valueChanged} + > `; } private _valueChanged(ev: CustomEvent): void { - handleChangeEvent(this, ev); - } - - private _zonePicked(ev: CustomEvent) { ev.stopPropagation(); - fireEvent(this, "value-changed", { - value: { ...this.trigger, zone: ev.detail.value }, - }); + const newTrigger = ev.detail.value; + fireEvent(this, "value-changed", { value: newTrigger }); } - private _radioGroupPicked(ev: CustomEvent) { - ev.stopPropagation(); - fireEvent(this, "value-changed", { - value: { - ...this.trigger, - event: (ev.target as HaRadio).value, - }, - }); - } - - static styles = css` - label { - display: flex; - align-items: center; - } - `; + private _computeLabelCallback = (schema: HaFormSchema): string => + this.hass.localize( + `ui.panel.config.automation.editor.triggers.type.geo_location.${schema.name}` + ); } declare global { diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-homeassistant.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-homeassistant.ts index c660b8f132..fceb9bd515 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-homeassistant.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-homeassistant.ts @@ -1,11 +1,12 @@ +import "../../../../../components/ha-form/ha-form"; import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../../common/dom/fire_event"; -import type { HaRadio } from "../../../../../components/ha-radio"; import type { HassTrigger } from "../../../../../data/automation"; import type { HomeAssistant } from "../../../../../types"; -import "../../../../../components/ha-formfield"; -import "../../../../../components/ha-radio"; +import type { HaFormSchema } from "../../../../../components/ha-form/types"; +import type { LocalizeFunc } from "../../../../../common/translations/localize"; @customElement("ha-automation-trigger-homeassistant") export class HaHassTrigger extends LitElement { @@ -13,6 +14,28 @@ export class HaHassTrigger extends LitElement { @property({ attribute: false }) public trigger!: HassTrigger; + private _schema = memoizeOne((localize: LocalizeFunc) => [ + { + name: "event", + type: "select", + required: true, + options: [ + [ + "start", + localize( + "ui.panel.config.automation.editor.triggers.type.homeassistant.start" + ), + ], + [ + "shutdown", + localize( + "ui.panel.config.automation.editor.triggers.type.homeassistant.shutdown" + ), + ], + ], + }, + ]); + public static get defaultConfig() { return { event: "start" as HassTrigger["event"], @@ -20,50 +43,28 @@ export class HaHassTrigger extends LitElement { } protected render() { - const { event } = this.trigger; return html` - + `; } - private _radioGroupPicked(ev) { + private _valueChanged(ev: CustomEvent): void { ev.stopPropagation(); - fireEvent(this, "value-changed", { - value: { - ...this.trigger, - event: (ev.target as HaRadio).value, - }, - }); + const newTrigger = ev.detail.value; + fireEvent(this, "value-changed", { value: newTrigger }); } + private _computeLabelCallback = (schema: HaFormSchema): string => + this.hass.localize( + `ui.panel.config.automation.editor.triggers.type.geo_location.${schema.name}` + ); + static styles = css` label { display: flex; diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt.ts index e3e997771f..95941c85b2 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt.ts @@ -1,12 +1,15 @@ -import "@polymer/paper-input/paper-input"; import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import type { HaFormSchema } from "../../../../../components/ha-form/types"; import { MqttTrigger } from "../../../../../data/automation"; import { HomeAssistant } from "../../../../../types"; -import { - handleChangeEvent, - TriggerElement, -} from "../ha-automation-trigger-row"; +import type { TriggerElement } from "../ha-automation-trigger-row"; + +const SCHEMA: HaFormSchema[] = [ + { name: "topic", required: true, selector: { text: {} } }, + { name: "payload", selector: { text: {} } }, +]; @customElement("ha-automation-trigger-mqtt") export class HaMQTTTrigger extends LitElement implements TriggerElement { @@ -19,30 +22,27 @@ export class HaMQTTTrigger extends LitElement implements TriggerElement { } protected render() { - const { topic, payload } = this.trigger; return html` - - + >
    `; } private _valueChanged(ev: CustomEvent): void { - handleChangeEvent(this, ev); + ev.stopPropagation(); + const newTrigger = ev.detail.value; + fireEvent(this, "value-changed", { value: newTrigger }); } + + private _computeLabelCallback = (schema: HaFormSchema): string => + this.hass.localize( + `ui.panel.config.automation.editor.triggers.type.mqtt.${schema.name}` + ); } declare global { diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state.ts index 218bb8273b..c156d2b9aa 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state.ts @@ -1,15 +1,13 @@ -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-input/paper-textarea"; +import "../../../../../components/ha-form/ha-form"; import { html, LitElement, PropertyValues } from "lit"; import { customElement, property } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import type { HaFormSchema } from "../../../../../components/ha-form/types"; import { createDurationData } from "../../../../../common/datetime/create_duration_data"; import { fireEvent } from "../../../../../common/dom/fire_event"; import { hasTemplate } from "../../../../../common/string/has-template"; -import "../../../../../components/entity/ha-entity-picker"; -import { NumericStateTrigger } from "../../../../../data/automation"; -import { HomeAssistant } from "../../../../../types"; -import { handleChangeEvent } from "../ha-automation-trigger-row"; -import "../../../../../components/ha-duration-input"; +import type { NumericStateTrigger } from "../../../../../data/automation"; +import type { HomeAssistant } from "../../../../../types"; @customElement("ha-automation-trigger-numeric_state") export class HaNumericStateTrigger extends LitElement { @@ -17,6 +15,21 @@ export class HaNumericStateTrigger extends LitElement { @property() public trigger!: NumericStateTrigger; + private _schema = memoizeOne((entityId): HaFormSchema[] => [ + { name: "entity_id", required: true, selector: { entity: {} } }, + { + name: "attribute", + selector: { attribute: { entity_id: entityId } }, + }, + { name: "above", selector: { text: {} } }, + { name: "below", selector: { text: {} } }, + { + name: "value_template", + selector: { text: { multiline: true } }, + }, + { name: "for", selector: { duration: {} } }, + ]); + public willUpdate(changedProperties: PropertyValues) { if (!changedProperties.has("trigger")) { return; @@ -38,67 +51,46 @@ export class HaNumericStateTrigger extends LitElement { } public render() { - const { value_template, entity_id, attribute, below, above } = this.trigger; const trgFor = createDurationData(this.trigger.for); + const data = { ...this.trigger, for: trgFor }; + const schema = this._schema(this.trigger.entity_id); + return html` - - - - - - + .computeLabel=${this._computeLabelCallback} + > `; } private _valueChanged(ev: CustomEvent): void { - handleChangeEvent(this, ev); + ev.stopPropagation(); + const newTrigger = ev.detail.value; + fireEvent(this, "value-changed", { value: newTrigger }); } + + private _computeLabelCallback = (schema: HaFormSchema): string => { + switch (schema.name) { + case "entity_id": + return this.hass.localize("ui.components.entity.entity-picker.entity"); + case "attribute": + return this.hass.localize( + "ui.components.entity.entity-attribute-picker.attribute" + ); + case "for": + return this.hass.localize( + `ui.panel.config.automation.editor.triggers.type.state.for` + ); + default: + return this.hass.localize( + `ui.panel.config.automation.editor.triggers.type.numeric_state.${schema.name}` + ); + } + }; } declare global { diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-state.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-state.ts index c38e78d6a4..44d3183e0d 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-state.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-state.ts @@ -1,4 +1,3 @@ -import "@polymer/paper-input/paper-input"; import { html, LitElement, PropertyValues } from "lit"; import { customElement, property } from "lit/decorators"; import { @@ -10,19 +9,16 @@ import { string, union, } from "superstruct"; -import { createDurationData } from "../../../../../common/datetime/create_duration_data"; +import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../../common/dom/fire_event"; import { hasTemplate } from "../../../../../common/string/has-template"; -import "../../../../../components/entity/ha-entity-attribute-picker"; -import "../../../../../components/entity/ha-entity-picker"; -import "../../../../../components/ha-duration-input"; import { StateTrigger } from "../../../../../data/automation"; import { HomeAssistant } from "../../../../../types"; import { baseTriggerStruct, forDictStruct } from "../../structs"; -import { - handleChangeEvent, - TriggerElement, -} from "../ha-automation-trigger-row"; +import { TriggerElement } from "../ha-automation-trigger-row"; +import "../../../../../components/ha-form/ha-form"; +import { createDurationData } from "../../../../../common/datetime/create_duration_data"; +import { HaFormSchema } from "../../../../../components/ha-form/types"; const stateTriggerStruct = assign( baseTriggerStruct, @@ -46,6 +42,17 @@ export class HaStateTrigger extends LitElement implements TriggerElement { return { entity_id: "" }; } + private _schema = memoizeOne((entityId) => [ + { name: "entity_id", required: true, selector: { entity: {} } }, + { + name: "attribute", + selector: { attribute: { entity_id: entityId } }, + }, + { name: "from", selector: { text: {} } }, + { name: "to", selector: { text: {} } }, + { name: "for", selector: { duration: {} } }, + ]); + public shouldUpdate(changedProperties: PropertyValues) { if (!changedProperties.has("trigger")) { return true; @@ -76,58 +83,41 @@ export class HaStateTrigger extends LitElement implements TriggerElement { } protected render() { - const { entity_id, attribute, to, from } = this.trigger; const trgFor = createDurationData(this.trigger.for); + const data = { ...this.trigger, ...{ for: trgFor } }; + const schema = this._schema(this.trigger.entity_id); + return html` - - - - - + .computeLabel=${this._computeLabelCallback} + > `; } private _valueChanged(ev: CustomEvent): void { - handleChangeEvent(this, ev); + ev.stopPropagation(); + const newTrigger = ev.detail.value; + + Object.keys(newTrigger).forEach((key) => + newTrigger[key] === undefined || newTrigger[key] === "" + ? delete newTrigger[key] + : {} + ); + + fireEvent(this, "value-changed", { value: newTrigger }); } + + private _computeLabelCallback = (schema: HaFormSchema): string => + this.hass.localize( + schema.name === "entity_id" + ? "ui.components.entity.entity-picker.entity" + : `ui.panel.config.automation.editor.triggers.type.state.${schema.name}` + ); } declare global { diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-sun.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-sun.ts index b996cbdcbf..0be06923b8 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-sun.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-sun.ts @@ -1,16 +1,12 @@ -import "@polymer/paper-input/paper-input"; -import { css, html, LitElement } from "lit"; +import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../../common/dom/fire_event"; -import "../../../../../components/ha-radio"; -import "../../../../../components/ha-formfield"; -import type { HaRadio } from "../../../../../components/ha-radio"; import type { SunTrigger } from "../../../../../data/automation"; import type { HomeAssistant } from "../../../../../types"; -import { - handleChangeEvent, - TriggerElement, -} from "../ha-automation-trigger-row"; +import type { TriggerElement } from "../ha-automation-trigger-row"; +import type { HaFormSchema } from "../../../../../components/ha-form/types"; +import type { LocalizeFunc } from "../../../../../common/translations/localize"; @customElement("ha-automation-trigger-sun") export class HaSunTrigger extends LitElement implements TriggerElement { @@ -18,6 +14,29 @@ export class HaSunTrigger extends LitElement implements TriggerElement { @property({ attribute: false }) public trigger!: SunTrigger; + private _schema = memoizeOne((localize: LocalizeFunc) => [ + { + name: "event", + type: "select", + required: true, + options: [ + [ + "sunrise", + localize( + "ui.panel.config.automation.editor.triggers.type.sun.sunrise" + ), + ], + [ + "sunset", + localize( + "ui.panel.config.automation.editor.triggers.type.sun.sunset" + ), + ], + ], + }, + { name: "offset", selector: { text: {} } }, + ]); + public static get defaultConfig() { return { event: "sunrise" as SunTrigger["event"], @@ -26,69 +45,28 @@ export class HaSunTrigger extends LitElement implements TriggerElement { } protected render() { - const { offset, event } = this.trigger; + const schema = this._schema(this.hass.localize); return html` - - - + > `; } private _valueChanged(ev: CustomEvent): void { - handleChangeEvent(this, ev); - } - - private _radioGroupPicked(ev) { ev.stopPropagation(); - fireEvent(this, "value-changed", { - value: { - ...this.trigger, - event: (ev.target as HaRadio).value, - }, - }); + const newTrigger = ev.detail.value; + fireEvent(this, "value-changed", { value: newTrigger }); } - static styles = css` - label { - display: flex; - align-items: center; - } - `; + private _computeLabelCallback = (schema: HaFormSchema): string => + this.hass.localize( + `ui.panel.config.automation.editor.triggers.type.sun.${schema.name}` + ); } declare global { diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-tag.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-tag.ts index ce891e3a57..9565c87347 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-tag.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-tag.ts @@ -1,13 +1,13 @@ -import "@polymer/paper-input/paper-input"; +import "@material/mwc-list/mwc-list-item"; import { html, LitElement, PropertyValues } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../../../common/dom/fire_event"; +import { caseInsensitiveStringCompare } from "../../../../../common/string/compare"; +import "../../../../../components/ha-select"; import { TagTrigger } from "../../../../../data/automation"; import { fetchTags, Tag } from "../../../../../data/tag"; import { HomeAssistant } from "../../../../../types"; import { TriggerElement } from "../ha-automation-trigger-row"; -import "../../../../../components/ha-paper-dropdown-menu"; -import { caseInsensitiveStringCompare } from "../../../../../common/string/compare"; @customElement("ha-automation-trigger-tag") export class HaTagTrigger extends LitElement implements TriggerElement { @@ -29,27 +29,22 @@ export class HaTagTrigger extends LitElement implements TriggerElement { protected render() { const { tag_id } = this.trigger; return html` - - - ${this._tags.map( - (tag) => html` - - ${tag.name || tag.id} - - ` - )} - - + ${this._tags.map( + (tag) => html` + + ${tag.name || tag.id} + + ` + )} + `; } @@ -64,8 +59,14 @@ export class HaTagTrigger extends LitElement implements TriggerElement { fireEvent(this, "value-changed", { value: { ...this.trigger, - tag_id: ev.detail.item.tag.id, + tag_id: ev.target.value, }, }); } } + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-trigger-tag": HaTagTrigger; + } +} diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-template.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-template.ts index 2a40c2fc9d..1bc192a231 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-template.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-template.ts @@ -1,15 +1,15 @@ -import "@polymer/paper-input/paper-textarea"; -import { html, LitElement } from "lit"; +import "../../../../../components/ha-textarea"; +import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; -import { TemplateTrigger } from "../../../../../data/automation"; -import { HomeAssistant } from "../../../../../types"; +import type { TemplateTrigger } from "../../../../../data/automation"; +import type { HomeAssistant } from "../../../../../types"; import { handleChangeEvent } from "../ha-automation-trigger-row"; @customElement("ha-automation-trigger-template") export class HaTemplateTrigger extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public trigger!: TemplateTrigger; + @property({ attribute: false }) public trigger!: TemplateTrigger; public static get defaultConfig() { return { value_template: "" }; @@ -18,19 +18,32 @@ export class HaTemplateTrigger extends LitElement { protected render() { const { value_template } = this.trigger; return html` - + autogrow + > `; } private _valueChanged(ev: CustomEvent): void { handleChangeEvent(this, ev); } + + static styles = css` + ha-textarea { + display: block; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-trigger-template": HaTemplateTrigger; + } } diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-time.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-time.ts index a72c5a9749..bdac66a6a1 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-time.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-time.ts @@ -1,23 +1,19 @@ +import memoizeOne from "memoize-one"; import { html, LitElement, PropertyValues } from "lit"; import { customElement, property, state } from "lit/decorators"; -import "../../../../../components/entity/ha-entity-picker"; -import "../../../../../components/ha-formfield"; -import "../../../../../components/ha-radio"; -import { TimeTrigger } from "../../../../../data/automation"; -import { HomeAssistant } from "../../../../../types"; -import { - handleChangeEvent, - TriggerElement, -} from "../ha-automation-trigger-row"; -import "../../../../../components/ha-time-input"; +import type { TimeTrigger } from "../../../../../data/automation"; +import type { HomeAssistant } from "../../../../../types"; +import type { TriggerElement } from "../ha-automation-trigger-row"; +import type { LocalizeFunc } from "../../../../../common/translations/localize"; +import type { HaFormSchema } from "../../../../../components/ha-form/types"; import { fireEvent } from "../../../../../common/dom/fire_event"; +import "../../../../../components/ha-form/ha-form"; -const includeDomains = ["input_datetime"]; @customElement("ha-automation-trigger-time") export class HaTimeTrigger extends LitElement implements TriggerElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public trigger!: TimeTrigger; + @property({ attribute: false }) public trigger!: TimeTrigger; @state() private _inputMode?: boolean; @@ -25,6 +21,37 @@ export class HaTimeTrigger extends LitElement implements TriggerElement { return { at: "" }; } + private _schema = memoizeOne( + (localize: LocalizeFunc, inputMode?: boolean): HaFormSchema[] => { + const modeSchema = inputMode + ? { name: "at", selector: { entity: { domain: "input_datetime" } } } + : { name: "at", selector: { time: {} } }; + + return [ + { + name: "mode", + type: "select", + required: true, + options: [ + [ + "value", + localize( + "ui.panel.config.automation.editor.triggers.type.time.type_value" + ), + ], + [ + "input", + localize( + "ui.panel.config.automation.editor.triggers.type.time.type_input" + ), + ], + ], + }, + modeSchema, + ]; + } + ); + public willUpdate(changedProperties: PropertyValues) { if (!changedProperties.has("trigger")) { return; @@ -50,67 +77,43 @@ export class HaTimeTrigger extends LitElement implements TriggerElement { this._inputMode ?? (at?.startsWith("input_datetime.") || at?.startsWith("sensor.")); - return html` - - - - - + const schema: HaFormSchema[] = this._schema(this.hass.localize, inputMode); - ${inputMode - ? html`` - : html``} `; - } + const data = { + mode: "value", + ...this.trigger, + }; - private _handleModeChanged(ev: Event) { - this._inputMode = (ev.target as any).value === "input"; + return html` + + `; } private _valueChanged(ev: CustomEvent): void { - handleChangeEvent(this, ev); + ev.stopPropagation(); + const newValue = ev.detail.value; + + this._inputMode = newValue.mode.value === "input"; + + Object.keys(newValue).forEach((key) => + newValue[key] === undefined || newValue[key] === "" + ? delete newValue[key] + : {} + ); + + fireEvent(this, "value-changed", { value: newValue }); } + + private _computeLabelCallback = (schema: HaFormSchema): string => + this.hass.localize( + `ui.panel.config.automation.editor.triggers.type.time.${schema.name}` + ); } declare global { diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-time_pattern.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-time_pattern.ts index b1b0a74f4c..dbb91a0567 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-time_pattern.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-time_pattern.ts @@ -1,12 +1,16 @@ -import "@polymer/paper-input/paper-input"; import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; -import { TimePatternTrigger } from "../../../../../data/automation"; -import { HomeAssistant } from "../../../../../types"; -import { - handleChangeEvent, - TriggerElement, -} from "../ha-automation-trigger-row"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import type { HaFormSchema } from "../../../../../components/ha-form/types"; +import type { TimePatternTrigger } from "../../../../../data/automation"; +import type { HomeAssistant } from "../../../../../types"; +import type { TriggerElement } from "../ha-automation-trigger-row"; + +const SCHEMA: HaFormSchema[] = [ + { name: "hours", selector: { text: {} } }, + { name: "minutes", selector: { text: {} } }, + { name: "seconds", selector: { text: {} } }, +]; @customElement("ha-automation-trigger-time_pattern") export class HaTimePatternTrigger extends LitElement implements TriggerElement { @@ -19,38 +23,27 @@ export class HaTimePatternTrigger extends LitElement implements TriggerElement { } protected render() { - const { hours, minutes, seconds } = this.trigger; return html` - - - + > `; } private _valueChanged(ev: CustomEvent): void { - handleChangeEvent(this, ev); + ev.stopPropagation(); + const newTrigger = ev.detail.value; + fireEvent(this, "value-changed", { value: newTrigger }); } + + private _computeLabelCallback = (schema: HaFormSchema): string => + this.hass.localize( + `ui.panel.config.automation.editor.triggers.type.time_pattern.${schema.name}` + ); } declare global { diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-webhook.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-webhook.ts index 4d90ec0219..fecd09535d 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-webhook.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-webhook.ts @@ -1,39 +1,133 @@ -import "@polymer/paper-input/paper-input"; -import { html, LitElement } from "lit"; -import { customElement, property } from "lit/decorators"; -import { WebhookTrigger } from "../../../../../data/automation"; +import "../../../../../components/ha-icon-button"; +import "../../../../../components/ha-textfield"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { mdiContentCopy } from "@mdi/js"; +import { css, html, LitElement, PropertyValues } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import { slugify } from "../../../../../common/string/slugify"; +import { copyToClipboard } from "../../../../../common/util/copy-clipboard"; +import type { HaTextField } from "../../../../../components/ha-textfield"; +import { showToast } from "../../../../../util/toast"; +import { + WebhookTrigger, + AutomationConfig, +} from "../../../../../data/automation"; import { HomeAssistant } from "../../../../../types"; import { handleChangeEvent } from "../ha-automation-trigger-row"; +const DEFAULT_WEBHOOK_ID = ""; + @customElement("ha-automation-trigger-webhook") export class HaWebhookTrigger extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property() public trigger!: WebhookTrigger; + @state() private _config?: AutomationConfig; + + private _unsub?: UnsubscribeFunc; + public static get defaultConfig() { return { - webhook_id: "", + webhook_id: DEFAULT_WEBHOOK_ID, }; } + connectedCallback() { + super.connectedCallback(); + const details = { + callback: (config) => { + this._config = config; + }, + }; + fireEvent(this, "subscribe-automation-config", details); + this._unsub = (details as any).unsub; + } + + disconnectedCallback() { + super.disconnectedCallback(); + if (this._unsub) { + this._unsub(); + } + } + + private _generateWebhookId(): string { + // The webhook_id should be treated like a password. Generate a default + // value that would be hard for someone to guess. This generates a + // 144-bit random value. The output is a 24 character url-safe string. + const randomBytes = crypto.getRandomValues(new Uint8Array(18)); + const base64Str = btoa(String.fromCharCode(...randomBytes)); + const urlSafeId = base64Str.replace(/\+/g, "-").replace(/\//g, "_"); + + // Include the automation name to give the user context about what the + // webhook_id is used for. + const urlSafeAlias = slugify(this._config?.alias || "", "-"); + + return `${urlSafeAlias}-${urlSafeId}`; + } + + public willUpdate(changedProperties: PropertyValues) { + super.willUpdate(changedProperties); + if (changedProperties.has("trigger")) { + if (this.trigger.webhook_id === DEFAULT_WEBHOOK_ID) { + this.trigger.webhook_id = this._generateWebhookId(); + } + } + } + protected render() { const { webhook_id: webhookId } = this.trigger; + return html` - + .helper=${this.hass.localize( + "ui.panel.config.automation.editor.triggers.type.webhook.webhook_id_helper" + )} + .iconTrailing=${true} + .value=${webhookId || ""} + @input=${this._valueChanged} + > + + `; } private _valueChanged(ev: CustomEvent): void { handleChangeEvent(this, ev); } + + private async _copyUrl(ev): Promise { + const inputElement = ev.target.parentElement as HaTextField; + const url = this.hass.hassUrl(`/api/webhook/${inputElement.value}`); + + await copyToClipboard(url); + showToast(this, { + message: this.hass.localize("ui.common.copied_clipboard"), + }); + } + + static styles = css` + ha-textfield { + display: block; + } + + ha-textfield > ha-icon-button { + --mdc-icon-button-size: 24px; + --mdc-icon-size: 18px; + } + `; } declare global { diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-zone.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-zone.ts index 333f3dc529..7b97d9a5eb 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-zone.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-zone.ts @@ -1,14 +1,13 @@ +import "../../../../../components/entity/ha-entity-picker"; +import "../../../../../components/ha-formfield"; import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../../../common/dom/fire_event"; import { computeStateDomain } from "../../../../../common/entity/compute_state_domain"; import { hasLocation } from "../../../../../common/entity/has_location"; -import "../../../../../components/entity/ha-entity-picker"; import type { ZoneTrigger } from "../../../../../data/automation"; import type { PolymerChangedEvent } from "../../../../../polymer-types"; import type { HomeAssistant } from "../../../../../types"; -import "../../../../../components/ha-radio"; -import "../../../../../components/ha-formfield"; import type { HaRadio } from "../../../../../components/ha-radio"; function zoneAndLocationFilter(stateObj) { @@ -116,6 +115,10 @@ export class HaZoneTrigger extends LitElement { display: flex; align-items: center; } + ha-entity-picker { + display: block; + margin-bottom: 24px; + } `; } diff --git a/src/panels/config/blueprint/dialog-import-blueprint.ts b/src/panels/config/blueprint/dialog-import-blueprint.ts index ad2cb1eb6c..ef43378c16 100644 --- a/src/panels/config/blueprint/dialog-import-blueprint.ts +++ b/src/panels/config/blueprint/dialog-import-blueprint.ts @@ -1,13 +1,13 @@ import "@material/mwc-button"; -import "@polymer/paper-input/paper-input"; -import type { PaperInputElement } from "@polymer/paper-input/paper-input"; -import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, state, query } from "lit/decorators"; +import { css, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-circular-progress"; import { createCloseHeading } from "../../../components/ha-dialog"; import "../../../components/ha-expansion-panel"; import "../../../components/ha-markdown"; +import "../../../components/ha-textfield"; +import type { HaTextField } from "../../../components/ha-textfield"; import { BlueprintImportResult, importBlueprint, @@ -32,7 +32,7 @@ class DialogImportBlueprint extends LitElement { @state() private _url?: string; - @query("#input") private _input?: PaperInputElement; + @query("#input") private _input?: HaTextField; public showDialog(params): void { this._params = params; @@ -90,13 +90,13 @@ class DialogImportBlueprint extends LitElement { ` : html` - + > `} ` - )}`} + >`}
    ${!this._result ? html`
    + ${this.cloudStatus.cloud === "connecting" && + this.cloudStatus.cloud_last_disconnect_reason + ? html` + + ` + : ""} + `}
    @@ -229,7 +229,7 @@ export class CloudGooglePref extends LitElement { } private async _pinChanged(ev) { - const input = ev.target as TextField; + const input = ev.target as HaTextField; try { await updateCloudPref(this.hass, { [input.id]: input.value || null, @@ -260,7 +260,7 @@ export class CloudGooglePref extends LitElement { right: auto; left: 24px; } - mwc-textfield { + ha-textfield { width: 250px; display: block; margin-top: 8px; diff --git a/src/panels/config/cloud/account/cloud-remote-pref.ts b/src/panels/config/cloud/account/cloud-remote-pref.ts index aeadcf80ed..2f1eb91417 100644 --- a/src/panels/config/cloud/account/cloud-remote-pref.ts +++ b/src/panels/config/cloud/account/cloud-remote-pref.ts @@ -1,5 +1,4 @@ import "@material/mwc-button"; -import "@polymer/paper-item/paper-item-body"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; diff --git a/src/panels/config/cloud/account/cloud-tts-pref.ts b/src/panels/config/cloud/account/cloud-tts-pref.ts index ed27eab975..e7f36d8edf 100644 --- a/src/panels/config/cloud/account/cloud-tts-pref.ts +++ b/src/panels/config/cloud/account/cloud-tts-pref.ts @@ -1,23 +1,21 @@ import "@material/mwc-button"; -import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; +import "@material/mwc-list/mwc-list-item"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../common/dom/fire_event"; -import { caseInsensitiveStringCompare } from "../../../../common/string/compare"; import "../../../../components/ha-card"; +import "../../../../components/ha-select"; import "../../../../components/ha-svg-icon"; import "../../../../components/ha-switch"; +import { CloudStatusLoggedIn, updateCloudPref } from "../../../../data/cloud"; import { - CloudStatusLoggedIn, CloudTTSInfo, getCloudTTSInfo, - updateCloudPref, -} from "../../../../data/cloud"; + getCloudTtsLanguages, + getCloudTtsSupportedGenders, +} from "../../../../data/cloud/tts"; import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box"; -import { translationMetadata } from "../../../../resources/translations-metadata"; import type { HomeAssistant } from "../../../../types"; import { showTryTtsDialog } from "./show-dialog-cloud-tts-try"; @@ -32,13 +30,17 @@ export class CloudTTSPref extends LitElement { @state() private ttsInfo?: CloudTTSInfo; protected render(): TemplateResult { - if (!this.cloudStatus) { + if (!this.cloudStatus || !this.ttsInfo) { return html``; } const languages = this.getLanguages(this.ttsInfo); const defaultVoice = this.cloudStatus.prefs.tts_default_voice; - const genders = this.getSupportedGenders(defaultVoice[0], this.ttsInfo); + const genders = this.getSupportedGenders( + defaultVoice[0], + this.ttsInfo, + this.hass.localize + ); return html`
    - - - ${languages.map( - ([key, label]) => - html`${label}` - )} - - + ${languages.map( + ([key, label]) => + html`${label}` + )} + - - - ${genders.map( - ([key, label]) => - html`${label}` - )} - - + + ${genders.map( + ([key, label]) => + html`${label}` + )} +
    @@ -94,75 +91,21 @@ export class CloudTTSPref extends LitElement { `; } - protected firstUpdated(changedProps) { - super.firstUpdated(changedProps); - getCloudTTSInfo(this.hass).then((info) => { - this.ttsInfo = info; - }); - } - - protected updated(changedProps) { - super.updated(changedProps); + protected willUpdate(changedProps) { + super.willUpdate(changedProps); + if (!this.hasUpdated) { + getCloudTTSInfo(this.hass).then((info) => { + this.ttsInfo = info; + }); + } if (changedProps.has("cloudStatus")) { this.savingPreferences = false; } } - private getLanguages = memoizeOne((info?: CloudTTSInfo) => { - const languages: Array<[string, string]> = []; + private getLanguages = memoizeOne(getCloudTtsLanguages); - if (!info) { - return languages; - } - - const seen = new Set(); - for (const [lang] of info.languages) { - if (seen.has(lang)) { - continue; - } - seen.add(lang); - - let label = lang; - - if (lang in translationMetadata.translations) { - label = translationMetadata.translations[lang].nativeName; - } else { - const [langFamily, dialect] = lang.split("-"); - if (langFamily in translationMetadata.translations) { - label = `${translationMetadata.translations[langFamily].nativeName}`; - - if (langFamily.toLowerCase() !== dialect.toLowerCase()) { - label += ` (${dialect})`; - } - } - } - - languages.push([lang, label]); - } - return languages.sort((a, b) => caseInsensitiveStringCompare(a[1], b[1])); - }); - - private getSupportedGenders = memoizeOne( - (language: string, info?: CloudTTSInfo) => { - const genders: Array<[string, string]> = []; - - if (!info) { - return genders; - } - - for (const [curLang, gender] of info.languages) { - if (curLang === language) { - genders.push([ - gender, - this.hass.localize(`ui.panel.config.cloud.account.tts.${gender}`) || - gender, - ]); - } - } - - return genders.sort((a, b) => caseInsensitiveStringCompare(a[1], b[1])); - } - ); + private getSupportedGenders = memoizeOne(getCloudTtsSupportedGenders); private _openTryDialog() { showTryTtsDialog(this, { @@ -171,14 +114,18 @@ export class CloudTTSPref extends LitElement { } async _handleLanguageChange(ev) { - if (ev.detail.item.value === this.cloudStatus!.prefs.tts_default_voice[0]) { + if (ev.target.value === this.cloudStatus!.prefs.tts_default_voice[0]) { return; } this.savingPreferences = true; - const language = ev.detail.item.value; + const language = ev.target.value; const curGender = this.cloudStatus!.prefs.tts_default_voice[1]; - const genders = this.getSupportedGenders(language, this.ttsInfo); + const genders = this.getSupportedGenders( + language, + this.ttsInfo, + this.hass.localize + ); const newGender = genders.find((item) => item[0] === curGender) ? curGender : genders[0][0]; @@ -200,12 +147,12 @@ export class CloudTTSPref extends LitElement { } async _handleGenderChange(ev) { - if (ev.detail.item.value === this.cloudStatus!.prefs.tts_default_voice[1]) { + if (ev.target.value === this.cloudStatus!.prefs.tts_default_voice[1]) { return; } this.savingPreferences = true; const language = this.cloudStatus!.prefs.tts_default_voice[0]; - const gender = ev.detail.item.value; + const gender = ev.target.value; try { await updateCloudPref(this.hass, { diff --git a/src/panels/config/cloud/account/cloud-webhooks.ts b/src/panels/config/cloud/account/cloud-webhooks.ts index 6f31ea5b52..a18e9e038f 100644 --- a/src/panels/config/cloud/account/cloud-webhooks.ts +++ b/src/panels/config/cloud/account/cloud-webhooks.ts @@ -1,5 +1,3 @@ -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-item/paper-item-body"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { customElement, property, state } from "lit/decorators"; import { isComponentLoaded } from "../../../../common/config/is_component_loaded"; diff --git a/src/panels/config/cloud/account/dialog-cloud-tts-try.ts b/src/panels/config/cloud/account/dialog-cloud-tts-try.ts index a65c544e40..b93cbeac21 100644 --- a/src/panels/config/cloud/account/dialog-cloud-tts-try.ts +++ b/src/panels/config/cloud/account/dialog-cloud-tts-try.ts @@ -1,18 +1,18 @@ import "@material/mwc-button"; +import "@material/mwc-list/mwc-list-item"; import { mdiPlayCircleOutline, mdiRobot } from "@mdi/js"; -import "@polymer/paper-input/paper-textarea"; -import type { PaperTextareaElement } from "@polymer/paper-input/paper-textarea"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, state, query } from "lit/decorators"; +import { customElement, property, query, state } from "lit/decorators"; import { LocalStorage } from "../../../../common/decorators/local-storage"; import { fireEvent } from "../../../../common/dom/fire_event"; +import { stopPropagation } from "../../../../common/dom/stop_propagation"; import { computeStateDomain } from "../../../../common/entity/compute_state_domain"; import { computeStateName } from "../../../../common/entity/compute_state_name"; import { supportsFeature } from "../../../../common/entity/supports-feature"; import { createCloseHeading } from "../../../../components/ha-dialog"; -import "../../../../components/ha-paper-dropdown-menu"; +import "../../../../components/ha-select"; +import "../../../../components/ha-textarea"; +import type { HaTextArea } from "../../../../components/ha-textarea"; import { showAutomationEditor } from "../../../../data/automation"; import { SUPPORT_PLAY_MEDIA } from "../../../../data/media-player"; import { convertTextToSpeech } from "../../../../data/tts"; @@ -29,7 +29,7 @@ export class DialogTryTts extends LitElement { @state() private _params?: TryTtsDialogParams; - @query("#message") private _messageInput?: PaperTextareaElement; + @query("#message") private _messageInput?: HaTextArea; @LocalStorage("cloudTtsTryMessage", false, false) private _message!: string; @@ -61,7 +61,8 @@ export class DialogTryTts extends LitElement { )} >
    - - + - - - - ${this.hass.localize( - "ui.panel.config.cloud.account.tts.dialog.target_browser" - )} - - ${Object.values(this.hass.states) - .filter( - (entity) => - computeStateDomain(entity) === "media_player" && - supportsFeature(entity, SUPPORT_PLAY_MEDIA) - ) - .map( - (entity) => html` - - ${computeStateName(entity)} - - ` - )} - - + + ${this.hass.localize( + "ui.panel.config.cloud.account.tts.dialog.target_browser" + )} + + ${Object.values(this.hass.states) + .filter( + (entity) => + computeStateDomain(entity) === "media_player" && + supportsFeature(entity, SUPPORT_PLAY_MEDIA) + ) + .map( + (entity) => html` + + ${computeStateName(entity)} + + ` + )} +
    ${this._error}` : ""} - + >
    ${this._error}` : ""} - - + + >
    ` : ""} - + @changed=${this._handleChange} + >
    @@ -62,9 +60,9 @@ class ConfigNameForm extends LitElement { : this.hass.config.location_name; } - private _handleChange(ev: PolymerChangedEvent) { - const target = ev.currentTarget as PaperInputElement; - this[`_${target.name}`] = target.value; + private _handleChange(ev) { + const target = ev.currentTarget as HaTextField; + this._name = target.value; } private async _save() { @@ -85,6 +83,9 @@ class ConfigNameForm extends LitElement { .card-actions { text-align: right; } + ha-textfield { + display: block; + } `; } } diff --git a/src/panels/config/core/ha-config-section-core.js b/src/panels/config/core/ha-config-section-core.js index 6c74b6f539..8a807fa346 100644 --- a/src/panels/config/core/ha-config-section-core.js +++ b/src/panels/config/core/ha-config-section-core.js @@ -1,5 +1,4 @@ import "@material/mwc-button"; -import "@polymer/paper-input/paper-input"; import { html } from "@polymer/polymer/lib/utils/html-tag"; /* eslint-plugin-disable lit */ import { PolymerElement } from "@polymer/polymer/polymer-element"; diff --git a/src/panels/config/dashboard/ha-config-dashboard.ts b/src/panels/config/dashboard/ha-config-dashboard.ts index ad05c9d55f..707b707232 100644 --- a/src/panels/config/dashboard/ha-config-dashboard.ts +++ b/src/panels/config/dashboard/ha-config-dashboard.ts @@ -1,4 +1,10 @@ -import { mdiCloudLock, mdiDotsVertical, mdiMagnify } from "@mdi/js"; +import { + mdiCloudLock, + mdiDotsVertical, + mdiLightbulbOutline, + mdiMagnify, + mdiNewBox, +} from "@mdi/js"; import "@material/mwc-list/mwc-list-item"; import type { ActionDetail } from "@material/mwc-list"; import "@polymer/app-layout/app-header/app-header"; @@ -18,6 +24,7 @@ import "../../../components/ha-icon-next"; import "../../../components/ha-icon-button"; import "../../../components/ha-menu-button"; import "../../../components/ha-button-menu"; +import "../../../components/ha-svg-icon"; import { CloudStatus } from "../../../data/cloud"; import { refreshSupervisorAvailableUpdates, @@ -34,6 +41,7 @@ import "./ha-config-updates"; import { fireEvent } from "../../../common/dom/fire_event"; import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import { showToast } from "../../../util/toast"; +import { documentationUrl } from "../../../util/documentation-url"; @customElement("ha-config-dashboard") class HaConfigDashboard extends LitElement { @@ -134,6 +142,51 @@ class HaConfigDashboard extends LitElement { .pages=${configSections.dashboard} > `} +
    + + Tip! + + ${this.hass.localize( + "ui.panel.config.tips.join", + "forums", + html`Forums`, + "twitter", + html`Twitter`, + "discord", + html`Chat`, + "blog", + html`Blog`, + "newsletter", + html`Newsletter + ` + )} + +
    `; @@ -192,8 +245,11 @@ class HaConfigDashboard extends LitElement { return [ haStyle, css` + ha-card:last-child { + margin-bottom: env(safe-area-inset-bottom); + } :host(:not([narrow])) ha-card:last-child { - margin-bottom: 24px; + margin-bottom: max(24px, env(safe-area-inset-bottom)); } ha-config-section { margin: auto; @@ -220,6 +276,23 @@ class HaConfigDashboard extends LitElement { :host([narrow]) ha-config-section { margin-top: -42px; } + + .tips { + text-align: center; + margin-bottom: max(env(safe-area-inset-bottom), 8px); + } + + .tips .text { + color: var(--secondary-text-color); + } + + .tip-word { + font-weight: 500; + } + + .new { + color: var(--primary-color); + } `, ]; } diff --git a/src/panels/config/devices/device-detail/ha-device-automation-dialog.ts b/src/panels/config/devices/device-detail/ha-device-automation-dialog.ts index b42d2f069b..cef11fc0fe 100644 --- a/src/panels/config/devices/device-detail/ha-device-automation-dialog.ts +++ b/src/panels/config/devices/device-detail/ha-device-automation-dialog.ts @@ -60,18 +60,18 @@ export class DialogDeviceAutomation extends LitElement { return; } - const { deviceId, script } = this._params; + const { device, script } = this._params; - fetchDeviceActions(this.hass, deviceId).then((actions) => { + fetchDeviceActions(this.hass, device.id).then((actions) => { this._actions = actions; }); if (script) { return; } - fetchDeviceTriggers(this.hass, deviceId).then((triggers) => { + fetchDeviceTriggers(this.hass, device.id).then((triggers) => { this._triggers = triggers; }); - fetchDeviceConditions(this.hass, deviceId).then((conditions) => { + fetchDeviceConditions(this.hass, device.id).then((conditions) => { this._conditions = conditions; }); } @@ -88,7 +88,14 @@ export class DialogDeviceAutomation extends LitElement { .heading=${this.hass.localize( `ui.panel.config.devices.${ this._params.script ? "script" : "automation" - }.create` + }.create`, + { + type: this.hass.localize( + `ui.panel.config.devices.type.${ + this._params.device.entry_type || "device" + }` + ), + } )} >
    diff --git a/src/panels/config/devices/device-detail/ha-device-entities-card.ts b/src/panels/config/devices/device-detail/ha-device-entities-card.ts index 3b4d461145..ebfbd159dd 100644 --- a/src/panels/config/devices/device-detail/ha-device-entities-card.ts +++ b/src/panels/config/devices/device-detail/ha-device-entities-card.ts @@ -165,7 +165,7 @@ export class HaDeviceEntitiesCard extends LitElement { const stateObj = this.hass.states[entry.entity_id]; const name = stripPrefixFromEntityName( computeStateName(stateObj), - `${this.deviceName} `.toLowerCase() + this.deviceName.toLowerCase() ); if (name) { config.name = name; @@ -198,7 +198,7 @@ export class HaDeviceEntitiesCard extends LitElement { ${name ? stripPrefixFromEntityName( name, - `${this.deviceName} `.toLowerCase() + this.deviceName.toLowerCase() ) || name : entry.entity_id}
    diff --git a/src/panels/config/devices/device-detail/ha-device-info-card.ts b/src/panels/config/devices/device-detail/ha-device-info-card.ts index 19176d0fbe..ce8e553b06 100644 --- a/src/panels/config/devices/device-detail/ha-device-info-card.ts +++ b/src/panels/config/devices/device-detail/ha-device-info-card.ts @@ -5,6 +5,7 @@ import { computeDeviceName, DeviceRegistryEntry, } from "../../../../data/device_registry"; +import { haStyle } from "../../../../resources/styles"; import { HomeAssistant } from "../../../../types"; import { loadDeviceRegistryDetailDialog } from "../device-registry-detail/show-dialog-device-registry-detail"; @@ -55,10 +56,13 @@ export class HaDeviceCard extends LitElement { "ui.panel.config.integrations.config_entry.via" )} ${this._computeDeviceName( - this.devices, - this.device.via_device_id - )}${this._computeDeviceName( + this.devices, + this.device.via_device_id + )}
    ` @@ -112,29 +116,32 @@ export class HaDeviceCard extends LitElement { } static get styles(): CSSResultGroup { - return css` - :host { - display: block; - } - ha-card { - flex: 1 0 100%; - min-width: 0; - } - .device { - width: 30%; - } - .area { - color: var(--primary-text-color); - } - .extra-info { - margin-top: 8px; - word-wrap: break-word; - } - .manuf, - .model { - color: var(--secondary-text-color); - word-wrap: break-word; - } - `; + return [ + haStyle, + css` + :host { + display: block; + } + ha-card { + flex: 1 0 100%; + min-width: 0; + } + .device { + width: 30%; + } + .area { + color: var(--primary-text-color); + } + .extra-info { + margin-top: 8px; + word-wrap: break-word; + } + .manuf, + .model { + color: var(--secondary-text-color); + word-wrap: break-word; + } + `, + ]; } } diff --git a/src/panels/config/devices/device-detail/integration-elements/mqtt/dialog-mqtt-device-debug-info.ts b/src/panels/config/devices/device-detail/integration-elements/mqtt/dialog-mqtt-device-debug-info.ts index dd85ebe90f..93c3d6a5a9 100644 --- a/src/panels/config/devices/device-detail/integration-elements/mqtt/dialog-mqtt-device-debug-info.ts +++ b/src/panels/config/devices/device-detail/integration-elements/mqtt/dialog-mqtt-device-debug-info.ts @@ -71,6 +71,7 @@ class DialogMQTTDeviceDebugInfo extends LitElement { @@ -164,6 +165,7 @@ class DialogMQTTDeviceDebugInfo extends LitElement { ${topic.topic} - + + + ` + )} + + Transmitted messages: +