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 f77328bf4b..ebea8d09f9 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,7 +1,13 @@ import { consume } from "@lit-labs/context"; -import { mdiDelete, mdiPlus } from "@mdi/js"; +import type { SortableEvent } from "sortablejs"; +import { mdiDelete, mdiPlus, mdiArrowUp, mdiArrowDown, mdiDrag } from "@mdi/js"; import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { repeat } from "lit/directives/repeat"; +import { + loadSortable, + SortableInstance, +} from "../../../../../resources/sortable.ondemand"; import { ensureArray } from "../../../../../common/array/ensure-array"; import { fireEvent } from "../../../../../common/dom/fire_event"; import "../../../../../components/ha-button"; @@ -14,6 +20,7 @@ import { ActionElement } from "../ha-automation-action-row"; import { describeCondition } from "../../../../../data/automation_i18n"; import { fullEntitiesContext } from "../../../../../data/context"; import { EntityRegistryEntry } from "../../../../../data/entity_registry"; +import { sortableStyles } from "../../../../../resources/ha-sortable-style"; @customElement("ha-automation-action-choose") export class HaChooseAction extends LitElement implements ActionElement { @@ -27,59 +34,30 @@ export class HaChooseAction extends LitElement implements ActionElement { @state() private _showDefault = false; - @state() private expandedUpdateFlag = false; + @state() private _expandedStates: boolean[] = []; @state() @consume({ context: fullEntitiesContext, subscribe: true }) _entityReg!: EntityRegistryEntry[]; + private _expandLast = false; + + private _sortable?: SortableInstance; + public static get defaultConfig() { return { choose: [{ conditions: [], sequence: [] }] }; } - protected willUpdate(changedProperties: PropertyValues) { - if (!changedProperties.has("action")) { - return; - } - - const oldCnt = - changedProperties.get("action") === undefined || - changedProperties.get("action").choose === undefined - ? 0 - : ensureArray(changedProperties.get("action").choose).length; - const newCnt = this.action.choose - ? ensureArray(this.action.choose).length - : 0; - if (newCnt === oldCnt + 1) { - this.expand(newCnt - 1); - } - } - - private expand(i: number) { - this.updateComplete.then(() => { - this.shadowRoot!.querySelectorAll("ha-expansion-panel")[i].expanded = - true; - this.expandedUpdateFlag = !this.expandedUpdateFlag; - }); - } - - private isExpanded(i: number) { - const nodes = this.shadowRoot!.querySelectorAll("ha-expansion-panel"); - if (nodes[i]) { - return nodes[i].expanded; - } - return false; - } - - private _expandedChanged() { - this.expandedUpdateFlag = !this.expandedUpdateFlag; + private _expandedChanged(ev) { + this._expandedStates = this._expandedStates.concat(); + this._expandedStates[ev.target!.index] = ev.detail.expanded; } private _getDescription(option, idx: number) { if (option.alias) { return option.alias; } - if (this.isExpanded(idx)) { + if (this._expandedStates[idx]) { return ""; } const conditions = ensureArray(option.conditions); @@ -108,67 +86,100 @@ export class HaChooseAction extends LitElement implements ActionElement { const action = this.action; return html` - ${(action.choose ? ensureArray(action.choose) : []).map( - (option, idx) => - html` - -

- ${this.hass.localize( - "ui.panel.config.automation.editor.actions.type.choose.option", - "number", - idx + 1 - )}: - ${this._getDescription(option, idx)} -

- - -
-

+
+ ${repeat( + action.choose ? ensureArray(action.choose) : [], + (option) => option, + (option, idx) => + html` + +

${this.hass.localize( - "ui.panel.config.automation.editor.actions.type.choose.conditions" + "ui.panel.config.automation.editor.actions.type.choose.option", + "number", + idx + 1 )}: -

- ( - option.conditions - )} - .reOrderMode=${this.reOrderMode} - .disabled=${this.disabled} - .hass=${this.hass} - .idx=${idx} - @value-changed=${this._conditionChanged} - > -

- ${this.hass.localize( - "ui.panel.config.automation.editor.actions.type.choose.sequence" - )}: -

- -
- - ` - )} + ${this._getDescription(option, idx)} +

+ ${this.reOrderMode + ? html` + + +
+ +
+ ` + : html` + + `} +
+

+ ${this.hass.localize( + "ui.panel.config.automation.editor.actions.type.choose.conditions" + )}: +

+ ( + option.conditions + )} + .reOrderMode=${this.reOrderMode} + .disabled=${this.disabled} + .hass=${this.hass} + .idx=${idx} + @value-changed=${this._conditionChanged} + > +

+ ${this.hass.localize( + "ui.panel.config.automation.editor.actions.type.choose.sequence" + )}: +

+ +
+ + ` + )} +
+ this._expandedStates.push(false) + ); + } + + protected updated(changedProps: PropertyValues) { + super.updated(changedProps); + + if (changedProps.has("reOrderMode")) { + if (this.reOrderMode) { + this._createSortable(); + } else { + this._destroySortable(); + } + } + + if (this._expandLast) { + const nodes = this.shadowRoot!.querySelectorAll("ha-expansion-panel"); + nodes[nodes.length - 1].expanded = true; + this._expandLast = false; + } + } + private _addDefault() { this._showDefault = true; } @@ -247,6 +282,38 @@ export class HaChooseAction extends LitElement implements ActionElement { fireEvent(this, "value-changed", { value: { ...this.action, choose }, }); + this._expandLast = true; + this._expandedStates[choose.length - 1] = true; + } + + private _moveUp(ev) { + const index = (ev.target as any).index; + const newIndex = index - 1; + this._move(index, newIndex); + } + + private _moveDown(ev) { + const index = (ev.target as any).index; + const newIndex = index + 1; + this._move(index, newIndex); + } + + private _dragged(ev: SortableEvent): void { + if (ev.oldIndex === ev.newIndex) return; + this._move(ev.oldIndex!, ev.newIndex!); + } + + private _move(index: number, newIndex: number) { + const options = ensureArray(this.action.choose)!.concat(); + const item = options.splice(index, 1)[0]; + options.splice(newIndex, 0, item); + + const expanded = this._expandedStates.splice(index, 1)[0]; + this._expandedStates.splice(newIndex, 0, expanded); + + fireEvent(this, "value-changed", { + value: { ...this.action, choose: options }, + }); } private _removeOption(ev: CustomEvent) { @@ -255,6 +322,7 @@ export class HaChooseAction extends LitElement implements ActionElement { ? [...ensureArray(this.action.choose)] : []; choose.splice(index, 1); + this._expandedStates.splice(index, 1); fireEvent(this, "value-changed", { value: { ...this.action, choose }, }); @@ -271,9 +339,37 @@ export class HaChooseAction extends LitElement implements ActionElement { }); } + private async _createSortable() { + const Sortable = await loadSortable(); + this._sortable = new Sortable(this.shadowRoot!.querySelector(".options")!, { + animation: 150, + fallbackClass: "sortable-fallback", + handle: ".handle", + onChoose: (evt: SortableEvent) => { + (evt.item as any).placeholder = + document.createComment("sort-placeholder"); + evt.item.after((evt.item as any).placeholder); + }, + onEnd: (evt: SortableEvent) => { + // put back in original location + if ((evt.item as any).placeholder) { + (evt.item as any).placeholder.replaceWith(evt.item); + delete (evt.item as any).placeholder; + } + this._dragged(evt); + }, + }); + } + + private _destroySortable() { + this._sortable?.destroy(); + this._sortable = undefined; + } + static get styles(): CSSResultGroup { return [ haStyle, + sortableStyles, css` ha-card { margin: 0 0 16px 0; @@ -292,8 +388,6 @@ export class HaChooseAction extends LitElement implements ActionElement { font-weight: inherit; } ha-icon-button { - position: absolute; - right: 0; inset-inline-start: initial; inset-inline-end: 0; direction: var(--direction); @@ -307,6 +401,14 @@ export class HaChooseAction extends LitElement implements ActionElement { .card-content { padding: 0 16px 16px 16px; } + .handle { + cursor: move; + padding: 12px; + } + .handle ha-svg-icon { + pointer-events: none; + height: 24px; + } `, ]; }