diff --git a/src/components/ha-items-display-editor.ts b/src/components/ha-items-display-editor.ts index d0e1319be0..77e1dfff35 100644 --- a/src/components/ha-items-display-editor.ts +++ b/src/components/ha-items-display-editor.ts @@ -2,7 +2,7 @@ import { ResizeController } from "@lit-labs/observers/resize-controller"; import { mdiDrag, mdiEye, mdiEyeOff } from "@mdi/js"; import type { TemplateResult } from "lit"; import { LitElement, css, html, nothing } from "lit"; -import { customElement, property } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { ifDefined } from "lit/directives/if-defined"; import { repeat } from "lit/directives/repeat"; @@ -64,92 +64,15 @@ export class HaItemDisplayEditor extends LitElement { item: DisplayItem ) => TemplateResult<1> | typeof nothing; + /** + * Used to sort items by keyboard navigation. + */ + @state() private _dragIndex: number | null = null; + private _showIcon = new ResizeController(this, { callback: (entries) => entries[0]?.contentRect.width > 450, }); - private _toggle(ev) { - ev.stopPropagation(); - const value = ev.currentTarget.value; - - const hiddenItems = this._hiddenItems(this.items, this.value.hidden); - - const newHidden = hiddenItems.map((item) => item.value); - - if (newHidden.includes(value)) { - newHidden.splice(newHidden.indexOf(value), 1); - } else { - newHidden.push(value); - } - - const newVisibleItems = this._visibleItems( - this.items, - newHidden, - this.value.order - ); - const newOrder = newVisibleItems.map((a) => a.value); - - this.value = { - hidden: newHidden, - order: newOrder, - }; - fireEvent(this, "value-changed", { value: this.value }); - } - - private _itemMoved(ev: CustomEvent): void { - ev.stopPropagation(); - const { oldIndex, newIndex } = ev.detail; - - const visibleItems = this._visibleItems( - this.items, - this.value.hidden, - this.value.order - ); - const newOrder = visibleItems.map((item) => item.value); - - const movedItem = newOrder.splice(oldIndex, 1)[0]; - newOrder.splice(newIndex, 0, movedItem); - - this.value = { - ...this.value, - order: newOrder, - }; - fireEvent(this, "value-changed", { value: this.value }); - } - - private _navigate(ev) { - const value = ev.currentTarget.value; - fireEvent(this, "item-display-navigate-clicked", { value }); - ev.stopPropagation(); - } - - private _visibleItems = memoizeOne( - (items: DisplayItem[], hidden: string[], order: string[]) => { - const compare = orderCompare(order); - - const visibleItems = items.filter((item) => !hidden.includes(item.value)); - if (this.dontSortVisible) { - return visibleItems; - } - - return items.sort((a, b) => - a.disableSorting && !b.disableSorting ? -1 : compare(a.value, b.value) - ); - } - ); - - private _allItems = memoizeOne( - (items: DisplayItem[], hidden: string[], order: string[]) => { - const visibleItems = this._visibleItems(items, hidden, order); - const hiddenItems = this._hiddenItems(items, hidden); - return [...visibleItems, ...hiddenItems]; - } - ); - - private _hiddenItems = memoizeOne((items: DisplayItem[], hidden: string[]) => - items.filter((item) => hidden.includes(item.value)) - ); - protected render() { const allItems = this._allItems( this.items, @@ -168,7 +91,7 @@ export class HaItemDisplayEditor extends LitElement { ${repeat( allItems, (item) => item.value, - (item: DisplayItem, _idx) => { + (item: DisplayItem, idx) => { const isVisible = !this.value.hidden.includes(item.value); const { label, @@ -180,9 +103,7 @@ export class HaItemDisplayEditor extends LitElement { } = item; return html` ${label} ${description @@ -199,6 +125,13 @@ export class HaItemDisplayEditor extends LitElement { ${isVisible && !disableSorting ? html` item.value); + + if (newHidden.includes(value)) { + newHidden.splice(newHidden.indexOf(value), 1); + } else { + newHidden.push(value); + } + + const newVisibleItems = this._visibleItems( + this.items, + newHidden, + this.value.order + ); + const newOrder = newVisibleItems.map((a) => a.value); + + this.value = { + hidden: newHidden, + order: newOrder, + }; + fireEvent(this, "value-changed", { value: this.value }); + } + + private _itemMoved(ev: CustomEvent): void { + ev.stopPropagation(); + const { oldIndex, newIndex } = ev.detail; + + this._moveItem(oldIndex, newIndex); + } + + private _moveItem(oldIndex, newIndex) { + if (oldIndex === newIndex) { + return; + } + + const visibleItems = this._visibleItems( + this.items, + this.value.hidden, + this.value.order + ); + const newOrder = visibleItems.map((item) => item.value); + + const movedItem = newOrder.splice(oldIndex, 1)[0]; + newOrder.splice(newIndex, 0, movedItem); + + this.value = { + ...this.value, + order: newOrder, + }; + fireEvent(this, "value-changed", { value: this.value }); + } + + private _navigate(ev) { + const value = ev.currentTarget.value; + fireEvent(this, "item-display-navigate-clicked", { value }); + ev.stopPropagation(); + } + + private _visibleItems = memoizeOne( + (items: DisplayItem[], hidden: string[], order: string[]) => { + const compare = orderCompare(order); + + const visibleItems = items.filter((item) => !hidden.includes(item.value)); + if (this.dontSortVisible) { + return [ + ...visibleItems.filter((item) => !item.disableSorting), + ...visibleItems.filter((item) => item.disableSorting), + ]; + } + + return items.sort((a, b) => + a.disableSorting && !b.disableSorting ? -1 : compare(a.value, b.value) + ); + } + ); + + private _allItems = memoizeOne( + (items: DisplayItem[], hidden: string[], order: string[]) => { + const visibleItems = this._visibleItems(items, hidden, order); + const hiddenItems = this._hiddenItems(items, hidden); + return [...visibleItems, ...hiddenItems]; + } + ); + + private _hiddenItems = memoizeOne((items: DisplayItem[], hidden: string[]) => + items.filter((item) => hidden.includes(item.value)) + ); + + private _maxSortableIndex = memoizeOne( + (items: DisplayItem[], hidden: string[]) => + items.filter( + (item) => !item.disableSorting && !hidden.includes(item.value) + ).length - 1 + ); + + private _keyActivatedMove = (ev: KeyboardEvent, clearDragIndex = false) => { + const oldIndex = this._dragIndex; + + if (ev.key === "ArrowUp") { + this._dragIndex = Math.max(0, this._dragIndex! - 1); + } else { + this._dragIndex = Math.min( + this._maxSortableIndex(this.items, this.value.hidden), + this._dragIndex! + 1 + ); + } + this._moveItem(oldIndex, this._dragIndex); + + // refocus the item after the sort + setTimeout(async () => { + await this.updateComplete; + const selectedElement = this.shadowRoot?.querySelector( + `ha-md-list-item:nth-child(${this._dragIndex! + 1})` + ) as HTMLElement | null; + selectedElement?.focus(); + if (clearDragIndex) { + this._dragIndex = null; + } + }); + }; + + private _sortKeydown = (ev: KeyboardEvent) => { + if ( + this._dragIndex !== null && + (ev.key === "ArrowUp" || ev.key === "ArrowDown") + ) { + ev.preventDefault(); + this._keyActivatedMove(ev); + } else if (this._dragIndex !== null && ev.key === "Escape") { + ev.preventDefault(); + ev.stopPropagation(); + this._dragIndex = null; + this.removeEventListener("keydown", this._sortKeydown); + } + }; + + private _listElementKeydown = (ev: KeyboardEvent) => { + if (ev.altKey && (ev.key === "ArrowUp" || ev.key === "ArrowDown")) { + ev.preventDefault(); + this._dragIndex = (ev.target as any).idx; + this._keyActivatedMove(ev, true); + } else if ( + (!this.showNavigationButton && ev.key === "Enter") || + ev.key === " " + ) { + this._dragHandleKeydown(ev); + } + }; + + private _dragHandleKeydown(ev: KeyboardEvent): void { + if (ev.key === "Enter" || ev.key === " ") { + ev.preventDefault(); + ev.stopPropagation(); + if (this._dragIndex === null) { + this._dragIndex = (ev.target as any).idx; + this.addEventListener("keydown", this._sortKeydown); + } else { + this.removeEventListener("keydown", this._sortKeydown); + this._dragIndex = null; + } + } + } + + disconnectedCallback(): void { + super.disconnectedCallback(); + this.removeEventListener("keydown", this._sortKeydown); + } + static styles = css` :host { display: block; @@ -273,6 +380,12 @@ export class HaItemDisplayEditor extends LitElement { --md-list-item-two-line-container-height: 48px; --md-list-item-one-line-container-height: 48px; } + ha-md-list-item.drag-selected { + box-shadow: + 0px 0px 8px 4px rgba(var(--rgb-accent-color), 0.8), + inset 0px 2px 8px 4px rgba(var(--rgb-accent-color), 0.4); + border-radius: 8px; + } ha-md-list-item ha-icon-button { margin-left: -12px; margin-right: -12px;