mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-02 00:27:49 +01:00
Extract AutomationSortableListMixin to deduplicate automation list components (#29977)
* Initial plan * Extract shared automation rows logic into AutomationRowsMixin Create ha-automation-rows-mixin.ts with common list manipulation methods (moveUp, moveDown, move, itemMoved, itemAdded, itemRemoved, itemChanged, duplicateItem, insertAfter, handleDragKeydown, stopSortSelection, getKey) and shared properties (hass, disabled, narrow, optionsInSidebar, _rowSortSelected, _clipboard). Refactor ha-automation-trigger, ha-automation-condition, ha-automation-action, and ha-automation-option to use the mixin, eliminating significant code duplication. Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com> * Address review feedback: rename to AutomationSortableListMixin, remove _ prefix from protected members, throw in setHighlightedItems - Rename AutomationRowsMixin to AutomationSortableListMixin - Rename file to ha-automation-sortable-list-mixin.ts - Remove _ prefix from all protected methods/properties - Make setHighlightedItems throw Not implemented error - Update all 4 component files with new references Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com> * Add missing this.items = items in itemChanged to avoid UI jump The original _conditionChanged had this.conditions = conditions before fireEvent to update local state and avoid UI jumps. This was lost during the mixin extraction. Co-authored-by: MindFreeze <5219205+MindFreeze@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com> Co-authored-by: MindFreeze <5219205+MindFreeze@users.noreply.github.com>
This commit is contained in:
@@ -3,13 +3,10 @@ import deepClone from "deep-clone-simple";
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, queryAll, state } from "lit/decorators";
|
||||
import { customElement, property, queryAll } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { ensureArray } from "../../../../common/array/ensure-array";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { nextRender } from "../../../../common/util/render-status";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-sortable";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
@@ -17,57 +14,41 @@ import {
|
||||
ACTION_BUILDING_BLOCKS,
|
||||
VIRTUAL_ACTIONS,
|
||||
} from "../../../../data/action";
|
||||
import {
|
||||
getValueFromDynamic,
|
||||
isDynamic,
|
||||
type AutomationClipboard,
|
||||
} from "../../../../data/automation";
|
||||
import { getValueFromDynamic, isDynamic } from "../../../../data/automation";
|
||||
import type { Action } from "../../../../data/script";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
PASTE_VALUE,
|
||||
showAddAutomationElementDialog,
|
||||
} from "../show-add-automation-element-dialog";
|
||||
import { AutomationSortableListMixin } from "../ha-automation-sortable-list-mixin";
|
||||
import { automationRowsStyles } from "../styles";
|
||||
import type HaAutomationActionRow from "./ha-automation-action-row";
|
||||
import { getAutomationActionType } from "./ha-automation-action-row";
|
||||
|
||||
@customElement("ha-automation-action")
|
||||
export default class HaAutomationAction extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
export default class HaAutomationAction extends AutomationSortableListMixin<Action>(
|
||||
LitElement
|
||||
) {
|
||||
@property({ type: Boolean }) public root = false;
|
||||
|
||||
@property({ attribute: false }) public actions!: Action[];
|
||||
|
||||
@property({ attribute: false }) public highlightedActions?: Action[];
|
||||
|
||||
@property({ type: Boolean, attribute: "sidebar" }) public optionsInSidebar =
|
||||
false;
|
||||
|
||||
@state() private _rowSortSelected?: number;
|
||||
|
||||
@state()
|
||||
@storage({
|
||||
key: "automationClipboard",
|
||||
state: true,
|
||||
subscribe: true,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
public _clipboard?: AutomationClipboard;
|
||||
|
||||
@queryAll("ha-automation-action-row")
|
||||
private _actionRowElements?: HaAutomationActionRow[];
|
||||
|
||||
private _focusLastActionOnChange = false;
|
||||
protected get items(): Action[] {
|
||||
return this.actions;
|
||||
}
|
||||
|
||||
private _focusActionIndexOnChange?: number;
|
||||
protected set items(items: Action[]) {
|
||||
this.actions = items;
|
||||
}
|
||||
|
||||
private _actionKeys = new WeakMap<Action, string>();
|
||||
protected setHighlightedItems(items: Action[]) {
|
||||
this.highlightedActions = items;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
@@ -77,14 +58,14 @@ export default class HaAutomationAction extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
group="actions"
|
||||
invert-swap
|
||||
@item-moved=${this._actionMoved}
|
||||
@item-added=${this._actionAdded}
|
||||
@item-removed=${this._actionRemoved}
|
||||
@item-moved=${this.itemMoved}
|
||||
@item-added=${this.itemAdded}
|
||||
@item-removed=${this.itemRemoved}
|
||||
>
|
||||
<div class="rows ${!this.optionsInSidebar ? "no-sidebar" : ""}">
|
||||
${repeat(
|
||||
this.actions,
|
||||
(action) => this._getKey(action),
|
||||
(action) => this.getKey(action),
|
||||
(action, idx) => html`
|
||||
<ha-automation-action-row
|
||||
.root=${this.root}
|
||||
@@ -95,26 +76,26 @@ export default class HaAutomationAction extends LitElement {
|
||||
.action=${action}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled}
|
||||
@duplicate=${this._duplicateAction}
|
||||
@insert-after=${this._insertAfter}
|
||||
@move-down=${this._moveDown}
|
||||
@move-up=${this._moveUp}
|
||||
@value-changed=${this._actionChanged}
|
||||
@duplicate=${this.duplicateItem}
|
||||
@insert-after=${this.insertAfter}
|
||||
@move-down=${this.moveDown}
|
||||
@move-up=${this.moveUp}
|
||||
@value-changed=${this.itemChanged}
|
||||
.hass=${this.hass}
|
||||
.highlight=${this.highlightedActions?.includes(action)}
|
||||
.optionsInSidebar=${this.optionsInSidebar}
|
||||
.sortSelected=${this._rowSortSelected === idx}
|
||||
@stop-sort-selection=${this._stopSortSelection}
|
||||
.sortSelected=${this.rowSortSelected === idx}
|
||||
@stop-sort-selection=${this.stopSortSelection}
|
||||
>
|
||||
${!this.disabled
|
||||
? html`
|
||||
<div
|
||||
tabindex="0"
|
||||
class="handle ${this._rowSortSelected === idx
|
||||
class="handle ${this.rowSortSelected === idx
|
||||
? "active"
|
||||
: ""}"
|
||||
slot="icons"
|
||||
@keydown=${this._handleDragKeydown}
|
||||
@keydown=${this.handleDragKeydown}
|
||||
@click=${stopPropagation}
|
||||
.index=${idx}
|
||||
>
|
||||
@@ -150,17 +131,16 @@ export default class HaAutomationAction extends LitElement {
|
||||
|
||||
if (
|
||||
changedProps.has("actions") &&
|
||||
(this._focusLastActionOnChange ||
|
||||
this._focusActionIndexOnChange !== undefined)
|
||||
(this.focusLastItemOnChange || this.focusItemIndexOnChange !== undefined)
|
||||
) {
|
||||
const mode = this._focusLastActionOnChange ? "new" : "moved";
|
||||
const mode = this.focusLastItemOnChange ? "new" : "moved";
|
||||
|
||||
const row = this.shadowRoot!.querySelector<HaAutomationActionRow>(
|
||||
`ha-automation-action-row:${mode === "new" ? "last-of-type" : `nth-of-type(${this._focusActionIndexOnChange! + 1})`}`
|
||||
`ha-automation-action-row:${mode === "new" ? "last-of-type" : `nth-of-type(${this.focusItemIndexOnChange! + 1})`}`
|
||||
)!;
|
||||
|
||||
this._focusLastActionOnChange = false;
|
||||
this._focusActionIndexOnChange = undefined;
|
||||
this.focusLastItemOnChange = false;
|
||||
this.focusItemIndexOnChange = undefined;
|
||||
|
||||
row.updateComplete.then(() => {
|
||||
// on new condition open the settings in the sidebar, except for building blocks
|
||||
@@ -234,158 +214,10 @@ export default class HaAutomationAction extends LitElement {
|
||||
elClass ? { ...elClass.defaultConfig } : { [action]: {} }
|
||||
);
|
||||
}
|
||||
this._focusLastActionOnChange = true;
|
||||
this.focusLastItemOnChange = true;
|
||||
fireEvent(this, "value-changed", { value: actions });
|
||||
};
|
||||
|
||||
private _getKey(action: Action) {
|
||||
if (!this._actionKeys.has(action)) {
|
||||
this._actionKeys.set(action, Math.random().toString());
|
||||
}
|
||||
|
||||
return this._actionKeys.get(action)!;
|
||||
}
|
||||
|
||||
private async _moveUp(ev) {
|
||||
ev.stopPropagation();
|
||||
const index = (ev.target as any).index;
|
||||
if (!(ev.target as HaAutomationActionRow).first) {
|
||||
const newIndex = index - 1;
|
||||
this._move(index, newIndex);
|
||||
if (this._rowSortSelected === index) {
|
||||
this._rowSortSelected = newIndex;
|
||||
}
|
||||
ev.target.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private async _moveDown(ev) {
|
||||
ev.stopPropagation();
|
||||
const index = (ev.target as any).index;
|
||||
if (!(ev.target as HaAutomationActionRow).last) {
|
||||
const newIndex = index + 1;
|
||||
this._move(index, newIndex);
|
||||
if (this._rowSortSelected === index) {
|
||||
this._rowSortSelected = newIndex;
|
||||
}
|
||||
ev.target.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private _move(oldIndex: number, newIndex: number) {
|
||||
const actions = this.actions.concat();
|
||||
const item = actions.splice(oldIndex, 1)[0];
|
||||
actions.splice(newIndex, 0, item);
|
||||
this.actions = actions;
|
||||
fireEvent(this, "value-changed", { value: actions });
|
||||
}
|
||||
|
||||
private _actionMoved(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
this._move(oldIndex, newIndex);
|
||||
}
|
||||
|
||||
private async _actionAdded(ev: CustomEvent): Promise<void> {
|
||||
ev.stopPropagation();
|
||||
const { index, data } = ev.detail;
|
||||
const item = ev.detail.item as HaAutomationActionRow;
|
||||
const selected = item.selected;
|
||||
|
||||
let actions = [
|
||||
...this.actions.slice(0, index),
|
||||
data,
|
||||
...this.actions.slice(index),
|
||||
];
|
||||
// Add action locally to avoid UI jump
|
||||
this.actions = actions;
|
||||
if (selected) {
|
||||
this._focusActionIndexOnChange = actions.length === 1 ? 0 : index;
|
||||
}
|
||||
await nextRender();
|
||||
if (this.actions !== actions) {
|
||||
// Ensure action is added even after update
|
||||
actions = [
|
||||
...this.actions.slice(0, index),
|
||||
data,
|
||||
...this.actions.slice(index),
|
||||
];
|
||||
if (selected) {
|
||||
this._focusActionIndexOnChange = actions.length === 1 ? 0 : index;
|
||||
}
|
||||
}
|
||||
fireEvent(this, "value-changed", { value: actions });
|
||||
}
|
||||
|
||||
private async _actionRemoved(ev: CustomEvent): Promise<void> {
|
||||
ev.stopPropagation();
|
||||
const { index } = ev.detail;
|
||||
const action = this.actions[index];
|
||||
// Remove action locally to avoid UI jump
|
||||
this.actions = this.actions.filter((a) => a !== action);
|
||||
await nextRender();
|
||||
// Ensure action is removed even after update
|
||||
const actions = this.actions.filter((a) => a !== action);
|
||||
fireEvent(this, "value-changed", { value: actions });
|
||||
}
|
||||
|
||||
private _actionChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const actions = [...this.actions];
|
||||
const newValue = ev.detail.value;
|
||||
const index = (ev.target as any).index;
|
||||
|
||||
if (newValue === null) {
|
||||
actions.splice(index, 1);
|
||||
} else {
|
||||
// Store key on new value.
|
||||
const key = this._getKey(actions[index]);
|
||||
this._actionKeys.set(newValue, key);
|
||||
|
||||
actions[index] = newValue;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", { value: actions });
|
||||
}
|
||||
|
||||
private _duplicateAction(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const index = (ev.target as any).index;
|
||||
fireEvent(this, "value-changed", {
|
||||
// @ts-expect-error Requires library bump to ES2023
|
||||
value: this.actions.toSpliced(
|
||||
index + 1,
|
||||
0,
|
||||
deepClone(this.actions[index])
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
private _insertAfter(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const index = (ev.target as any).index;
|
||||
const inserted = ensureArray(ev.detail.value);
|
||||
this.highlightedActions = inserted;
|
||||
fireEvent(this, "value-changed", {
|
||||
// @ts-expect-error Requires library bump to ES2023
|
||||
value: this.actions.toSpliced(index + 1, 0, ...inserted),
|
||||
});
|
||||
}
|
||||
|
||||
private _handleDragKeydown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
ev.stopPropagation();
|
||||
this._rowSortSelected =
|
||||
this._rowSortSelected === undefined
|
||||
? (ev.target as any).index
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _stopSortSelection() {
|
||||
this._rowSortSelected = undefined;
|
||||
}
|
||||
|
||||
static styles = automationRowsStyles;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,18 +8,14 @@ import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, queryAll, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { ensureArray } from "../../../../common/array/ensure-array";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { nextRender } from "../../../../common/util/render-status";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-sortable";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import {
|
||||
getValueFromDynamic,
|
||||
isDynamic,
|
||||
type AutomationClipboard,
|
||||
type Condition,
|
||||
} from "../../../../data/automation";
|
||||
import type { ConditionDescriptions } from "../../../../data/condition";
|
||||
@@ -29,58 +25,46 @@ import {
|
||||
} from "../../../../data/condition";
|
||||
import { subscribeLabFeature } from "../../../../data/labs";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
PASTE_VALUE,
|
||||
showAddAutomationElementDialog,
|
||||
} from "../show-add-automation-element-dialog";
|
||||
import { AutomationSortableListMixin } from "../ha-automation-sortable-list-mixin";
|
||||
import { automationRowsStyles } from "../styles";
|
||||
import "./ha-automation-condition-row";
|
||||
import type HaAutomationConditionRow from "./ha-automation-condition-row";
|
||||
|
||||
@customElement("ha-automation-condition")
|
||||
export default class HaAutomationCondition extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
export default class HaAutomationCondition extends AutomationSortableListMixin<Condition>(
|
||||
SubscribeMixin(LitElement)
|
||||
) {
|
||||
@property({ attribute: false }) public conditions!: Condition[];
|
||||
|
||||
@property({ attribute: false }) public highlightedConditions?: Condition[];
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean }) public root = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "sidebar" }) public optionsInSidebar =
|
||||
false;
|
||||
|
||||
@state() private _rowSortSelected?: number;
|
||||
|
||||
@state() private _conditionDescriptions: ConditionDescriptions = {};
|
||||
|
||||
@state()
|
||||
@storage({
|
||||
key: "automationClipboard",
|
||||
state: true,
|
||||
subscribe: true,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
public _clipboard?: AutomationClipboard;
|
||||
|
||||
@queryAll("ha-automation-condition-row")
|
||||
private _conditionRowElements?: HaAutomationConditionRow[];
|
||||
|
||||
private _focusLastConditionOnChange = false;
|
||||
|
||||
private _focusConditionIndexOnChange?: number;
|
||||
|
||||
private _conditionKeys = new WeakMap<Condition, string>();
|
||||
// @ts-ignore
|
||||
@state() private _newTriggersAndConditions = false;
|
||||
|
||||
private _unsub?: Promise<UnsubscribeFunc>;
|
||||
|
||||
// @ts-ignore
|
||||
@state() private _newTriggersAndConditions = false;
|
||||
protected get items(): Condition[] {
|
||||
return this.conditions;
|
||||
}
|
||||
|
||||
protected set items(items: Condition[]) {
|
||||
this.conditions = items;
|
||||
}
|
||||
|
||||
protected setHighlightedItems(items: Condition[]) {
|
||||
this.highlightedConditions = items;
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
@@ -155,17 +139,17 @@ export default class HaAutomationCondition extends SubscribeMixin(LitElement) {
|
||||
value: updatedConditions,
|
||||
});
|
||||
} else if (
|
||||
this._focusLastConditionOnChange ||
|
||||
this._focusConditionIndexOnChange !== undefined
|
||||
this.focusLastItemOnChange ||
|
||||
this.focusItemIndexOnChange !== undefined
|
||||
) {
|
||||
const mode = this._focusLastConditionOnChange ? "new" : "moved";
|
||||
const mode = this.focusLastItemOnChange ? "new" : "moved";
|
||||
|
||||
const row = this.shadowRoot!.querySelector<HaAutomationConditionRow>(
|
||||
`ha-automation-condition-row:${mode === "new" ? "last-of-type" : `nth-of-type(${this._focusConditionIndexOnChange! + 1})`}`
|
||||
`ha-automation-condition-row:${mode === "new" ? "last-of-type" : `nth-of-type(${this.focusItemIndexOnChange! + 1})`}`
|
||||
)!;
|
||||
|
||||
this._focusLastConditionOnChange = false;
|
||||
this._focusConditionIndexOnChange = undefined;
|
||||
this.focusLastItemOnChange = false;
|
||||
this.focusItemIndexOnChange = undefined;
|
||||
|
||||
row.updateComplete.then(() => {
|
||||
// on new condition open the settings in the sidebar, except for building blocks
|
||||
@@ -217,14 +201,14 @@ export default class HaAutomationCondition extends SubscribeMixin(LitElement) {
|
||||
.disabled=${this.disabled}
|
||||
group="conditions"
|
||||
invert-swap
|
||||
@item-moved=${this._conditionMoved}
|
||||
@item-added=${this._conditionAdded}
|
||||
@item-removed=${this._conditionRemoved}
|
||||
@item-moved=${this.itemMoved}
|
||||
@item-added=${this.itemAdded}
|
||||
@item-removed=${this.itemRemoved}
|
||||
>
|
||||
<div class="rows ${!this.optionsInSidebar ? "no-sidebar" : ""}">
|
||||
${repeat(
|
||||
this.conditions.filter((c) => typeof c === "object"),
|
||||
(condition) => this._getKey(condition),
|
||||
(condition) => this.getKey(condition),
|
||||
(cond, idx) => html`
|
||||
<ha-automation-condition-row
|
||||
.root=${this.root}
|
||||
@@ -237,26 +221,26 @@ export default class HaAutomationCondition extends SubscribeMixin(LitElement) {
|
||||
.conditionDescriptions=${this._conditionDescriptions}
|
||||
.disabled=${this.disabled}
|
||||
.narrow=${this.narrow}
|
||||
@duplicate=${this._duplicateCondition}
|
||||
@insert-after=${this._insertAfter}
|
||||
@move-down=${this._moveDown}
|
||||
@move-up=${this._moveUp}
|
||||
@value-changed=${this._conditionChanged}
|
||||
@duplicate=${this.duplicateItem}
|
||||
@insert-after=${this.insertAfter}
|
||||
@move-down=${this.moveDown}
|
||||
@move-up=${this.moveUp}
|
||||
@value-changed=${this.itemChanged}
|
||||
.hass=${this.hass}
|
||||
.highlight=${this.highlightedConditions?.includes(cond)}
|
||||
.optionsInSidebar=${this.optionsInSidebar}
|
||||
.sortSelected=${this._rowSortSelected === idx}
|
||||
@stop-sort-selection=${this._stopSortSelection}
|
||||
.sortSelected=${this.rowSortSelected === idx}
|
||||
@stop-sort-selection=${this.stopSortSelection}
|
||||
>
|
||||
${!this.disabled
|
||||
? html`
|
||||
<div
|
||||
tabindex="0"
|
||||
class="handle ${this._rowSortSelected === idx
|
||||
class="handle ${this.rowSortSelected === idx
|
||||
? "active"
|
||||
: ""}"
|
||||
slot="icons"
|
||||
@keydown=${this._handleDragKeydown}
|
||||
@keydown=${this.handleDragKeydown}
|
||||
@click=${stopPropagation}
|
||||
.index=${idx}
|
||||
>
|
||||
@@ -321,159 +305,10 @@ export default class HaAutomationCondition extends SubscribeMixin(LitElement) {
|
||||
...(target?.entity_id ? { entity_id: target.entity_id } : {}),
|
||||
});
|
||||
}
|
||||
this._focusLastConditionOnChange = true;
|
||||
this.focusLastItemOnChange = true;
|
||||
fireEvent(this, "value-changed", { value: conditions });
|
||||
};
|
||||
|
||||
private _getKey(condition: Condition) {
|
||||
if (!this._conditionKeys.has(condition)) {
|
||||
this._conditionKeys.set(condition, Math.random().toString());
|
||||
}
|
||||
|
||||
return this._conditionKeys.get(condition)!;
|
||||
}
|
||||
|
||||
private _moveUp(ev) {
|
||||
ev.stopPropagation();
|
||||
const index = (ev.target as any).index;
|
||||
if (!(ev.target as HaAutomationConditionRow).first) {
|
||||
const newIndex = index - 1;
|
||||
this._move(index, newIndex);
|
||||
if (this._rowSortSelected === index) {
|
||||
this._rowSortSelected = newIndex;
|
||||
}
|
||||
ev.target.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private _moveDown(ev) {
|
||||
ev.stopPropagation();
|
||||
const index = (ev.target as any).index;
|
||||
if (!(ev.target as HaAutomationConditionRow).last) {
|
||||
const newIndex = index + 1;
|
||||
this._move(index, newIndex);
|
||||
if (this._rowSortSelected === index) {
|
||||
this._rowSortSelected = newIndex;
|
||||
}
|
||||
ev.target.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private _move(oldIndex: number, newIndex: number) {
|
||||
const conditions = this.conditions.concat();
|
||||
const item = conditions.splice(oldIndex, 1)[0];
|
||||
conditions.splice(newIndex, 0, item);
|
||||
this.conditions = conditions;
|
||||
fireEvent(this, "value-changed", { value: conditions });
|
||||
}
|
||||
|
||||
private _conditionMoved(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
this._move(oldIndex, newIndex);
|
||||
}
|
||||
|
||||
private async _conditionAdded(ev: CustomEvent): Promise<void> {
|
||||
ev.stopPropagation();
|
||||
const { index, data } = ev.detail;
|
||||
const item = ev.detail.item as HaAutomationConditionRow;
|
||||
const selected = item.selected;
|
||||
let conditions = [
|
||||
...this.conditions.slice(0, index),
|
||||
data,
|
||||
...this.conditions.slice(index),
|
||||
];
|
||||
// Add condition locally to avoid UI jump
|
||||
this.conditions = conditions;
|
||||
if (selected) {
|
||||
this._focusConditionIndexOnChange = conditions.length === 1 ? 0 : index;
|
||||
}
|
||||
await nextRender();
|
||||
if (this.conditions !== conditions) {
|
||||
// Ensure condition is added even after update
|
||||
conditions = [
|
||||
...this.conditions.slice(0, index),
|
||||
data,
|
||||
...this.conditions.slice(index),
|
||||
];
|
||||
if (selected) {
|
||||
this._focusConditionIndexOnChange = conditions.length === 1 ? 0 : index;
|
||||
}
|
||||
}
|
||||
fireEvent(this, "value-changed", { value: conditions });
|
||||
}
|
||||
|
||||
private async _conditionRemoved(ev: CustomEvent): Promise<void> {
|
||||
ev.stopPropagation();
|
||||
const { index } = ev.detail;
|
||||
const condition = this.conditions[index];
|
||||
// Remove condition locally to avoid UI jump
|
||||
this.conditions = this.conditions.filter((c) => c !== condition);
|
||||
await nextRender();
|
||||
// Ensure condition is removed even after update
|
||||
const conditions = this.conditions.filter((c) => c !== condition);
|
||||
fireEvent(this, "value-changed", { value: conditions });
|
||||
}
|
||||
|
||||
private _conditionChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const conditions = [...this.conditions];
|
||||
const newValue = ev.detail.value;
|
||||
const index = (ev.target as any).index;
|
||||
|
||||
if (newValue === null) {
|
||||
conditions.splice(index, 1);
|
||||
} else {
|
||||
// Store key on new value.
|
||||
const key = this._getKey(conditions[index]);
|
||||
this._conditionKeys.set(newValue, key);
|
||||
|
||||
conditions[index] = newValue;
|
||||
}
|
||||
|
||||
this.conditions = conditions;
|
||||
|
||||
fireEvent(this, "value-changed", { value: conditions });
|
||||
}
|
||||
|
||||
private _duplicateCondition(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const index = (ev.target as any).index;
|
||||
fireEvent(this, "value-changed", {
|
||||
// @ts-expect-error Requires library bump to ES2023
|
||||
value: this.conditions.toSpliced(
|
||||
index + 1,
|
||||
0,
|
||||
deepClone(this.conditions[index])
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
private _insertAfter(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const index = (ev.target as any).index;
|
||||
const inserted = ensureArray(ev.detail.value);
|
||||
this.highlightedConditions = inserted;
|
||||
fireEvent(this, "value-changed", {
|
||||
// @ts-expect-error Requires library bump to ES2023
|
||||
value: this.conditions.toSpliced(index + 1, 0, ...inserted),
|
||||
});
|
||||
}
|
||||
|
||||
private _handleDragKeydown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
ev.stopPropagation();
|
||||
this._rowSortSelected =
|
||||
this._rowSortSelected === undefined
|
||||
? (ev.target as any).index
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _stopSortSelection() {
|
||||
this._rowSortSelected = undefined;
|
||||
}
|
||||
|
||||
static styles = automationRowsStyles;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type { LitElement } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { ensureArray } from "../../../common/array/ensure-array";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { nextRender } from "../../../common/util/render-status";
|
||||
import type { AutomationClipboard } from "../../../data/automation";
|
||||
import type { Constructor, HomeAssistant } from "../../../types";
|
||||
|
||||
export const AutomationSortableListMixin = <T extends object>(
|
||||
superClass: Constructor<LitElement>
|
||||
) => {
|
||||
class AutomationSortableListClass extends superClass {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "sidebar" }) public optionsInSidebar =
|
||||
false;
|
||||
|
||||
@state() protected rowSortSelected?: number;
|
||||
|
||||
@state()
|
||||
@storage({
|
||||
key: "automationClipboard",
|
||||
state: true,
|
||||
subscribe: true,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
public _clipboard?: AutomationClipboard;
|
||||
|
||||
protected focusLastItemOnChange = false;
|
||||
|
||||
protected focusItemIndexOnChange?: number;
|
||||
|
||||
private _itemKeys = new WeakMap<T, string>();
|
||||
|
||||
protected get items(): T[] {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
protected set items(_items: T[]) {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
protected getKey(item: T): string {
|
||||
if (!this._itemKeys.has(item)) {
|
||||
this._itemKeys.set(item, Math.random().toString());
|
||||
}
|
||||
|
||||
return this._itemKeys.get(item)!;
|
||||
}
|
||||
|
||||
protected moveUp(ev) {
|
||||
ev.stopPropagation();
|
||||
const index = (ev.target as any).index;
|
||||
if (!(ev.target as any).first) {
|
||||
const newIndex = index - 1;
|
||||
this.move(index, newIndex);
|
||||
if (this.rowSortSelected === index) {
|
||||
this.rowSortSelected = newIndex;
|
||||
}
|
||||
ev.target.focus();
|
||||
}
|
||||
}
|
||||
|
||||
protected moveDown(ev) {
|
||||
ev.stopPropagation();
|
||||
const index = (ev.target as any).index;
|
||||
if (!(ev.target as any).last) {
|
||||
const newIndex = index + 1;
|
||||
this.move(index, newIndex);
|
||||
if (this.rowSortSelected === index) {
|
||||
this.rowSortSelected = newIndex;
|
||||
}
|
||||
ev.target.focus();
|
||||
}
|
||||
}
|
||||
|
||||
protected move(oldIndex: number, newIndex: number) {
|
||||
const items = this.items.concat();
|
||||
const item = items.splice(oldIndex, 1)[0];
|
||||
items.splice(newIndex, 0, item);
|
||||
this.items = items;
|
||||
fireEvent(this, "value-changed", { value: items });
|
||||
}
|
||||
|
||||
protected itemMoved(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
this.move(oldIndex, newIndex);
|
||||
}
|
||||
|
||||
protected async itemAdded(ev: CustomEvent): Promise<void> {
|
||||
ev.stopPropagation();
|
||||
const { index, data } = ev.detail;
|
||||
const selected = (ev.detail.item as any).selected;
|
||||
|
||||
let items = [
|
||||
...this.items.slice(0, index),
|
||||
data,
|
||||
...this.items.slice(index),
|
||||
];
|
||||
// Add item locally to avoid UI jump
|
||||
this.items = items;
|
||||
if (selected) {
|
||||
this.focusItemIndexOnChange = items.length === 1 ? 0 : index;
|
||||
}
|
||||
await nextRender();
|
||||
if (this.items !== items) {
|
||||
// Ensure item is added even after update
|
||||
items = [
|
||||
...this.items.slice(0, index),
|
||||
data,
|
||||
...this.items.slice(index),
|
||||
];
|
||||
if (selected) {
|
||||
this.focusItemIndexOnChange = items.length === 1 ? 0 : index;
|
||||
}
|
||||
}
|
||||
fireEvent(this, "value-changed", { value: items });
|
||||
}
|
||||
|
||||
protected async itemRemoved(ev: CustomEvent): Promise<void> {
|
||||
ev.stopPropagation();
|
||||
const { index } = ev.detail;
|
||||
const item = this.items[index];
|
||||
// Remove item locally to avoid UI jump
|
||||
this.items = this.items.filter((i) => i !== item);
|
||||
await nextRender();
|
||||
// Ensure item is removed even after update
|
||||
const items = this.items.filter((i) => i !== item);
|
||||
fireEvent(this, "value-changed", { value: items });
|
||||
}
|
||||
|
||||
protected itemChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const items = [...this.items];
|
||||
const newValue = ev.detail.value;
|
||||
const index = (ev.target as any).index;
|
||||
|
||||
if (newValue === null) {
|
||||
items.splice(index, 1);
|
||||
} else {
|
||||
// Store key on new value.
|
||||
const key = this.getKey(items[index]);
|
||||
this._itemKeys.set(newValue, key);
|
||||
|
||||
items[index] = newValue;
|
||||
}
|
||||
|
||||
this.items = items;
|
||||
fireEvent(this, "value-changed", { value: items });
|
||||
}
|
||||
|
||||
protected duplicateItem(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const index = (ev.target as any).index;
|
||||
fireEvent(this, "value-changed", {
|
||||
// @ts-expect-error Requires library bump to ES2023
|
||||
value: this.items.toSpliced(index + 1, 0, deepClone(this.items[index])),
|
||||
});
|
||||
}
|
||||
|
||||
protected insertAfter(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const index = (ev.target as any).index;
|
||||
const inserted = ensureArray(ev.detail.value);
|
||||
this.setHighlightedItems(inserted);
|
||||
fireEvent(this, "value-changed", {
|
||||
// @ts-expect-error Requires library bump to ES2023
|
||||
value: this.items.toSpliced(index + 1, 0, ...inserted),
|
||||
});
|
||||
}
|
||||
|
||||
protected setHighlightedItems(_items: T[]) {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
protected handleDragKeydown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
ev.stopPropagation();
|
||||
this.rowSortSelected =
|
||||
this.rowSortSelected === undefined
|
||||
? (ev.target as any).index
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected stopSortSelection() {
|
||||
this.rowSortSelected = undefined;
|
||||
}
|
||||
}
|
||||
return AutomationSortableListClass;
|
||||
};
|
||||
@@ -1,58 +1,38 @@
|
||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, queryAll, state } from "lit/decorators";
|
||||
import { customElement, property, queryAll } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { nextRender } from "../../../../common/util/render-status";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-sortable";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { AutomationClipboard } from "../../../../data/automation";
|
||||
import type { Option } from "../../../../data/script";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { AutomationSortableListMixin } from "../ha-automation-sortable-list-mixin";
|
||||
import { automationRowsStyles } from "../styles";
|
||||
import "./ha-automation-option-row";
|
||||
import type HaAutomationOptionRow from "./ha-automation-option-row";
|
||||
|
||||
@customElement("ha-automation-option")
|
||||
export default class HaAutomationOption extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
export default class HaAutomationOption extends AutomationSortableListMixin<Option>(
|
||||
LitElement
|
||||
) {
|
||||
@property({ attribute: false }) public options!: Option[];
|
||||
|
||||
@property({ type: Boolean, attribute: "sidebar" }) public optionsInSidebar =
|
||||
false;
|
||||
|
||||
@property({ type: Boolean, attribute: "show-default" })
|
||||
public showDefaultActions = false;
|
||||
|
||||
@state() private _rowSortSelected?: number;
|
||||
|
||||
@state()
|
||||
@storage({
|
||||
key: "automationClipboard",
|
||||
state: true,
|
||||
subscribe: true,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
public _clipboard?: AutomationClipboard;
|
||||
|
||||
@queryAll("ha-automation-option-row")
|
||||
private _optionRowElements?: HaAutomationOptionRow[];
|
||||
|
||||
private _focusLastOptionOnChange = false;
|
||||
protected get items(): Option[] {
|
||||
return this.options;
|
||||
}
|
||||
|
||||
private _focusOptionIndexOnChange?: number;
|
||||
|
||||
private _optionsKeys = new WeakMap<Option, string>();
|
||||
protected set items(items: Option[]) {
|
||||
this.options = items;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
@@ -62,14 +42,14 @@ export default class HaAutomationOption extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
group="options"
|
||||
invert-swap
|
||||
@item-moved=${this._optionMoved}
|
||||
@item-added=${this._optionAdded}
|
||||
@item-removed=${this._optionRemoved}
|
||||
@item-moved=${this.itemMoved}
|
||||
@item-added=${this.itemAdded}
|
||||
@item-removed=${this.itemRemoved}
|
||||
>
|
||||
<div class="rows ${!this.optionsInSidebar ? "no-sidebar" : ""}">
|
||||
${repeat(
|
||||
this.options,
|
||||
(option) => this._getKey(option),
|
||||
(option) => this.getKey(option),
|
||||
(option, idx) => html`
|
||||
<ha-automation-option-row
|
||||
.sortableData=${option}
|
||||
@@ -79,24 +59,24 @@ export default class HaAutomationOption extends LitElement {
|
||||
.option=${option}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled}
|
||||
@duplicate=${this._duplicateOption}
|
||||
@move-down=${this._moveDown}
|
||||
@move-up=${this._moveUp}
|
||||
@value-changed=${this._optionChanged}
|
||||
@duplicate=${this.duplicateItem}
|
||||
@move-down=${this.moveDown}
|
||||
@move-up=${this.moveUp}
|
||||
@value-changed=${this.itemChanged}
|
||||
.hass=${this.hass}
|
||||
.optionsInSidebar=${this.optionsInSidebar}
|
||||
.sortSelected=${this._rowSortSelected === idx}
|
||||
@stop-sort-selection=${this._stopSortSelection}
|
||||
.sortSelected=${this.rowSortSelected === idx}
|
||||
@stop-sort-selection=${this.stopSortSelection}
|
||||
>
|
||||
${!this.disabled
|
||||
? html`
|
||||
<div
|
||||
tabindex="0"
|
||||
class="handle ${this._rowSortSelected === idx
|
||||
class="handle ${this.rowSortSelected === idx
|
||||
? "active"
|
||||
: ""}"
|
||||
slot="icons"
|
||||
@keydown=${this._handleDragKeydown}
|
||||
@keydown=${this.handleDragKeydown}
|
||||
@click=${stopPropagation}
|
||||
.index=${idx}
|
||||
>
|
||||
@@ -145,17 +125,16 @@ export default class HaAutomationOption extends LitElement {
|
||||
|
||||
if (
|
||||
changedProps.has("options") &&
|
||||
(this._focusLastOptionOnChange ||
|
||||
this._focusOptionIndexOnChange !== undefined)
|
||||
(this.focusLastItemOnChange || this.focusItemIndexOnChange !== undefined)
|
||||
) {
|
||||
const mode = this._focusLastOptionOnChange ? "new" : "moved";
|
||||
const mode = this.focusLastItemOnChange ? "new" : "moved";
|
||||
|
||||
const row = this.shadowRoot!.querySelector<HaAutomationOptionRow>(
|
||||
`ha-automation-option-row:${mode === "new" ? "last-of-type" : `nth-of-type(${this._focusOptionIndexOnChange! + 1})`}`
|
||||
`ha-automation-option-row:${mode === "new" ? "last-of-type" : `nth-of-type(${this.focusItemIndexOnChange! + 1})`}`
|
||||
)!;
|
||||
|
||||
this._focusLastOptionOnChange = false;
|
||||
this._focusOptionIndexOnChange = undefined;
|
||||
this.focusLastItemOnChange = false;
|
||||
this.focusItemIndexOnChange = undefined;
|
||||
|
||||
row.updateComplete.then(() => {
|
||||
if (this.narrow) {
|
||||
@@ -188,140 +167,14 @@ export default class HaAutomationOption extends LitElement {
|
||||
|
||||
private _addOption = () => {
|
||||
const options = this.options.concat({ conditions: [], sequence: [] });
|
||||
this._focusLastOptionOnChange = true;
|
||||
this.focusLastItemOnChange = true;
|
||||
fireEvent(this, "value-changed", { value: options });
|
||||
};
|
||||
|
||||
private _getKey(option: Option) {
|
||||
if (!this._optionsKeys.has(option)) {
|
||||
this._optionsKeys.set(option, Math.random().toString());
|
||||
}
|
||||
|
||||
return this._optionsKeys.get(option)!;
|
||||
}
|
||||
|
||||
private _moveUp(ev) {
|
||||
ev.stopPropagation();
|
||||
const index = (ev.target as any).index;
|
||||
if (!(ev.target as HaAutomationOptionRow).first) {
|
||||
const newIndex = index - 1;
|
||||
this._move(index, newIndex);
|
||||
if (this._rowSortSelected === index) {
|
||||
this._rowSortSelected = newIndex;
|
||||
}
|
||||
ev.target.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private _moveDown(ev) {
|
||||
ev.stopPropagation();
|
||||
const index = (ev.target as any).index;
|
||||
if (!(ev.target as HaAutomationOptionRow).last) {
|
||||
const newIndex = index + 1;
|
||||
this._move(index, newIndex);
|
||||
if (this._rowSortSelected === index) {
|
||||
this._rowSortSelected = newIndex;
|
||||
}
|
||||
ev.target.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private _move(oldIndex: number, newIndex: number) {
|
||||
const options = this.options.concat();
|
||||
const item = options.splice(oldIndex, 1)[0];
|
||||
options.splice(newIndex, 0, item);
|
||||
this.options = options;
|
||||
fireEvent(this, "value-changed", { value: options });
|
||||
}
|
||||
|
||||
private _optionMoved(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
this._move(oldIndex, newIndex);
|
||||
}
|
||||
|
||||
private async _optionAdded(ev: CustomEvent): Promise<void> {
|
||||
ev.stopPropagation();
|
||||
const { index, data } = ev.detail;
|
||||
const item = ev.detail.item as HaAutomationOptionRow;
|
||||
const selected = item.selected;
|
||||
|
||||
const options = [
|
||||
...this.options.slice(0, index),
|
||||
data,
|
||||
...this.options.slice(index),
|
||||
];
|
||||
// Add option locally to avoid UI jump
|
||||
this.options = options;
|
||||
if (selected) {
|
||||
this._focusOptionIndexOnChange = options.length === 1 ? 0 : index;
|
||||
}
|
||||
await nextRender();
|
||||
fireEvent(this, "value-changed", { value: this.options });
|
||||
}
|
||||
|
||||
private async _optionRemoved(ev: CustomEvent): Promise<void> {
|
||||
ev.stopPropagation();
|
||||
const { index } = ev.detail;
|
||||
const option = this.options[index];
|
||||
// Remove option locally to avoid UI jump
|
||||
this.options = this.options.filter((o) => o !== option);
|
||||
await nextRender();
|
||||
// Ensure option is removed even after update
|
||||
const options = this.options.filter((o) => o !== option);
|
||||
fireEvent(this, "value-changed", { value: options });
|
||||
}
|
||||
|
||||
private _optionChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const options = [...this.options];
|
||||
const newValue = ev.detail.value;
|
||||
const index = (ev.target as any).index;
|
||||
|
||||
if (newValue === null) {
|
||||
options.splice(index, 1);
|
||||
} else {
|
||||
// Store key on new value.
|
||||
const key = this._getKey(options[index]);
|
||||
this._optionsKeys.set(newValue, key);
|
||||
|
||||
options[index] = newValue;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", { value: options });
|
||||
}
|
||||
|
||||
private _duplicateOption(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const index = (ev.target as any).index;
|
||||
fireEvent(this, "value-changed", {
|
||||
// @ts-expect-error Requires library bump to ES2023
|
||||
value: this.options.toSpliced(
|
||||
index + 1,
|
||||
0,
|
||||
deepClone(this.options[index])
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
private _showDefaultActions = () => {
|
||||
fireEvent(this, "show-default-actions");
|
||||
};
|
||||
|
||||
private _handleDragKeydown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
ev.stopPropagation();
|
||||
this._rowSortSelected =
|
||||
this._rowSortSelected === undefined
|
||||
? (ev.target as any).index
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _stopSortSelection() {
|
||||
this._rowSortSelected = undefined;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
automationRowsStyles,
|
||||
css`
|
||||
|
||||
@@ -8,18 +8,14 @@ import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { ensureArray } from "../../../../common/array/ensure-array";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { nextRender } from "../../../../common/util/render-status";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-sortable";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import {
|
||||
getValueFromDynamic,
|
||||
isDynamic,
|
||||
type AutomationClipboard,
|
||||
type Trigger,
|
||||
type TriggerList,
|
||||
} from "../../../../data/automation";
|
||||
@@ -27,56 +23,44 @@ import { subscribeLabFeature } from "../../../../data/labs";
|
||||
import type { TriggerDescriptions } from "../../../../data/trigger";
|
||||
import { isTriggerList, subscribeTriggers } from "../../../../data/trigger";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
PASTE_VALUE,
|
||||
showAddAutomationElementDialog,
|
||||
} from "../show-add-automation-element-dialog";
|
||||
import { AutomationSortableListMixin } from "../ha-automation-sortable-list-mixin";
|
||||
import { automationRowsStyles } from "../styles";
|
||||
import "./ha-automation-trigger-row";
|
||||
import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
|
||||
|
||||
@customElement("ha-automation-trigger")
|
||||
export default class HaAutomationTrigger extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
export default class HaAutomationTrigger extends AutomationSortableListMixin<Trigger>(
|
||||
SubscribeMixin(LitElement)
|
||||
) {
|
||||
@property({ attribute: false }) public triggers!: Trigger[];
|
||||
|
||||
@property({ attribute: false }) public highlightedTriggers?: Trigger[];
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "sidebar" }) public optionsInSidebar =
|
||||
false;
|
||||
|
||||
@property({ type: Boolean }) public root = false;
|
||||
|
||||
@state() private _rowSortSelected?: number;
|
||||
|
||||
@state()
|
||||
@storage({
|
||||
key: "automationClipboard",
|
||||
state: true,
|
||||
subscribe: true,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
public _clipboard?: AutomationClipboard;
|
||||
|
||||
private _focusLastTriggerOnChange = false;
|
||||
|
||||
private _focusTriggerIndexOnChange?: number;
|
||||
|
||||
private _triggerKeys = new WeakMap<Trigger, string>();
|
||||
|
||||
private _unsub?: Promise<UnsubscribeFunc>;
|
||||
|
||||
@state() private _triggerDescriptions: TriggerDescriptions = {};
|
||||
|
||||
// @ts-ignore
|
||||
@state() private _newTriggersAndConditions = false;
|
||||
|
||||
private _unsub?: Promise<UnsubscribeFunc>;
|
||||
|
||||
protected get items(): Trigger[] {
|
||||
return this.triggers;
|
||||
}
|
||||
|
||||
protected set items(items: Trigger[]) {
|
||||
this.triggers = items;
|
||||
}
|
||||
|
||||
protected setHighlightedItems(items: Trigger[]) {
|
||||
this.highlightedTriggers = items;
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._unsubscribe();
|
||||
@@ -133,14 +117,14 @@ export default class HaAutomationTrigger extends SubscribeMixin(LitElement) {
|
||||
.disabled=${this.disabled}
|
||||
group="triggers"
|
||||
invert-swap
|
||||
@item-moved=${this._triggerMoved}
|
||||
@item-added=${this._triggerAdded}
|
||||
@item-removed=${this._triggerRemoved}
|
||||
@item-moved=${this.itemMoved}
|
||||
@item-added=${this.itemAdded}
|
||||
@item-removed=${this.itemRemoved}
|
||||
>
|
||||
<div class="rows ${!this.optionsInSidebar ? "no-sidebar" : ""}">
|
||||
${repeat(
|
||||
this.triggers,
|
||||
(trigger) => this._getKey(trigger),
|
||||
(trigger) => this.getKey(trigger),
|
||||
(trg, idx) => html`
|
||||
<ha-automation-trigger-row
|
||||
.sortableData=${trg}
|
||||
@@ -149,28 +133,28 @@ export default class HaAutomationTrigger extends SubscribeMixin(LitElement) {
|
||||
.last=${idx === this.triggers.length - 1}
|
||||
.trigger=${trg}
|
||||
.triggerDescriptions=${this._triggerDescriptions}
|
||||
@duplicate=${this._duplicateTrigger}
|
||||
@insert-after=${this._insertAfter}
|
||||
@move-down=${this._moveDown}
|
||||
@move-up=${this._moveUp}
|
||||
@value-changed=${this._triggerChanged}
|
||||
@duplicate=${this.duplicateItem}
|
||||
@insert-after=${this.insertAfter}
|
||||
@move-down=${this.moveDown}
|
||||
@move-up=${this.moveUp}
|
||||
@value-changed=${this.itemChanged}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.narrow=${this.narrow}
|
||||
.highlight=${this.highlightedTriggers?.includes(trg)}
|
||||
.optionsInSidebar=${this.optionsInSidebar}
|
||||
.sortSelected=${this._rowSortSelected === idx}
|
||||
@stop-sort-selection=${this._stopSortSelection}
|
||||
.sortSelected=${this.rowSortSelected === idx}
|
||||
@stop-sort-selection=${this.stopSortSelection}
|
||||
>
|
||||
${!this.disabled
|
||||
? html`
|
||||
<div
|
||||
tabindex="0"
|
||||
class="handle ${this._rowSortSelected === idx
|
||||
class="handle ${this.rowSortSelected === idx
|
||||
? "active"
|
||||
: ""}"
|
||||
slot="icons"
|
||||
@keydown=${this._handleDragKeydown}
|
||||
@keydown=${this.handleDragKeydown}
|
||||
@click=${stopPropagation}
|
||||
.index=${idx}
|
||||
>
|
||||
@@ -237,7 +221,7 @@ export default class HaAutomationTrigger extends SubscribeMixin(LitElement) {
|
||||
...(target?.entity_id ? { entity_id: target.entity_id } : {}),
|
||||
});
|
||||
}
|
||||
this._focusLastTriggerOnChange = true;
|
||||
this.focusLastItemOnChange = true;
|
||||
fireEvent(this, "value-changed", { value: triggers });
|
||||
};
|
||||
|
||||
@@ -246,15 +230,14 @@ export default class HaAutomationTrigger extends SubscribeMixin(LitElement) {
|
||||
|
||||
if (
|
||||
changedProps.has("triggers") &&
|
||||
(this._focusLastTriggerOnChange ||
|
||||
this._focusTriggerIndexOnChange !== undefined)
|
||||
(this.focusLastItemOnChange || this.focusItemIndexOnChange !== undefined)
|
||||
) {
|
||||
const row = this.shadowRoot!.querySelector<HaAutomationTriggerRow>(
|
||||
`ha-automation-trigger-row:${this._focusLastTriggerOnChange ? "last-of-type" : `nth-of-type(${this._focusTriggerIndexOnChange! + 1})`}`
|
||||
`ha-automation-trigger-row:${this.focusLastItemOnChange ? "last-of-type" : `nth-of-type(${this.focusItemIndexOnChange! + 1})`}`
|
||||
)!;
|
||||
|
||||
this._focusLastTriggerOnChange = false;
|
||||
this._focusTriggerIndexOnChange = undefined;
|
||||
this.focusLastItemOnChange = false;
|
||||
this.focusItemIndexOnChange = undefined;
|
||||
|
||||
row.updateComplete.then(() => {
|
||||
if (this.optionsInSidebar) {
|
||||
@@ -283,154 +266,6 @@ export default class HaAutomationTrigger extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private _getKey(action: Trigger) {
|
||||
if (!this._triggerKeys.has(action)) {
|
||||
this._triggerKeys.set(action, Math.random().toString());
|
||||
}
|
||||
|
||||
return this._triggerKeys.get(action)!;
|
||||
}
|
||||
|
||||
private _moveUp(ev) {
|
||||
ev.stopPropagation();
|
||||
const index = (ev.target as any).index;
|
||||
if (!(ev.target as HaAutomationTriggerRow).first) {
|
||||
const newIndex = index - 1;
|
||||
this._move(index, newIndex);
|
||||
if (this._rowSortSelected === index) {
|
||||
this._rowSortSelected = newIndex;
|
||||
}
|
||||
ev.target.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private _moveDown(ev) {
|
||||
ev.stopPropagation();
|
||||
const index = (ev.target as any).index;
|
||||
if (!(ev.target as HaAutomationTriggerRow).last) {
|
||||
const newIndex = index + 1;
|
||||
this._move(index, newIndex);
|
||||
if (this._rowSortSelected === index) {
|
||||
this._rowSortSelected = newIndex;
|
||||
}
|
||||
ev.target.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private _move(oldIndex: number, newIndex: number) {
|
||||
const triggers = this.triggers.concat();
|
||||
const item = triggers.splice(oldIndex, 1)[0];
|
||||
triggers.splice(newIndex, 0, item);
|
||||
this.triggers = triggers;
|
||||
fireEvent(this, "value-changed", { value: triggers });
|
||||
}
|
||||
|
||||
private _triggerMoved(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
this._move(oldIndex, newIndex);
|
||||
}
|
||||
|
||||
private async _triggerAdded(ev: CustomEvent): Promise<void> {
|
||||
ev.stopPropagation();
|
||||
const { index, data } = ev.detail;
|
||||
const item = ev.detail.item as HaAutomationTriggerRow;
|
||||
const selected = item.selected;
|
||||
|
||||
let triggers = [
|
||||
...this.triggers.slice(0, index),
|
||||
data,
|
||||
...this.triggers.slice(index),
|
||||
];
|
||||
// Add trigger locally to avoid UI jump
|
||||
this.triggers = triggers;
|
||||
if (selected) {
|
||||
this._focusTriggerIndexOnChange = triggers.length === 1 ? 0 : index;
|
||||
}
|
||||
await nextRender();
|
||||
if (this.triggers !== triggers) {
|
||||
// Ensure trigger is added even after update
|
||||
triggers = [
|
||||
...this.triggers.slice(0, index),
|
||||
data,
|
||||
...this.triggers.slice(index),
|
||||
];
|
||||
if (selected) {
|
||||
this._focusTriggerIndexOnChange = triggers.length === 1 ? 0 : index;
|
||||
}
|
||||
}
|
||||
fireEvent(this, "value-changed", { value: triggers });
|
||||
}
|
||||
|
||||
private async _triggerRemoved(ev: CustomEvent): Promise<void> {
|
||||
ev.stopPropagation();
|
||||
const { index } = ev.detail;
|
||||
const trigger = this.triggers[index];
|
||||
// Remove trigger locally to avoid UI jump
|
||||
this.triggers = this.triggers.filter((t) => t !== trigger);
|
||||
await nextRender();
|
||||
// Ensure trigger is removed even after update
|
||||
const triggers = this.triggers.filter((t) => t !== trigger);
|
||||
fireEvent(this, "value-changed", { value: triggers });
|
||||
}
|
||||
|
||||
private _triggerChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const triggers = [...this.triggers];
|
||||
const newValue = ev.detail.value;
|
||||
const index = (ev.target as any).index;
|
||||
|
||||
if (newValue === null) {
|
||||
triggers.splice(index, 1);
|
||||
} else {
|
||||
// Store key on new value.
|
||||
const key = this._getKey(triggers[index]);
|
||||
this._triggerKeys.set(newValue, key);
|
||||
|
||||
triggers[index] = newValue;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", { value: triggers });
|
||||
}
|
||||
|
||||
private _duplicateTrigger(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const index = (ev.target as any).index;
|
||||
fireEvent(this, "value-changed", {
|
||||
// @ts-expect-error Requires library bump to ES2023
|
||||
value: this.triggers.toSpliced(
|
||||
index + 1,
|
||||
0,
|
||||
deepClone(this.triggers[index])
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
private _insertAfter(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const index = (ev.target as any).index;
|
||||
const inserted = ensureArray(ev.detail.value);
|
||||
this.highlightedTriggers = inserted;
|
||||
fireEvent(this, "value-changed", {
|
||||
// @ts-expect-error Requires library bump to ES2023
|
||||
value: this.triggers.toSpliced(index + 1, 0, ...inserted),
|
||||
});
|
||||
}
|
||||
|
||||
private _handleDragKeydown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
ev.stopPropagation();
|
||||
this._rowSortSelected =
|
||||
this._rowSortSelected === undefined
|
||||
? (ev.target as any).index
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _stopSortSelection() {
|
||||
this._rowSortSelected = undefined;
|
||||
}
|
||||
|
||||
static styles = automationRowsStyles;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user