mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-21 08:16:36 +00:00
Add ha-sortable component (#19294)
This commit is contained in:
parent
17e62c10d4
commit
104aef3dec
@ -1,16 +1,13 @@
|
|||||||
import "@material/mwc-list/mwc-list-item";
|
import "@material/mwc-list/mwc-list-item";
|
||||||
import { mdiDrag } from "@mdi/js";
|
import { mdiDrag } from "@mdi/js";
|
||||||
import { LitElement, PropertyValues, css, html, nothing } from "lit";
|
import { LitElement, css, html, nothing } from "lit";
|
||||||
import { customElement, property, query } from "lit/decorators";
|
import { customElement, property, query } from "lit/decorators";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import { SortableEvent } from "sortablejs";
|
|
||||||
import { ensureArray } from "../../common/array/ensure-array";
|
import { ensureArray } from "../../common/array/ensure-array";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||||
import type { SelectOption, SelectSelector } from "../../data/selector";
|
import type { SelectOption, SelectSelector } from "../../data/selector";
|
||||||
import { sortableStyles } from "../../resources/ha-sortable-style";
|
|
||||||
import { SortableInstance } from "../../resources/sortable";
|
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "../chips/ha-chip-set";
|
import "../chips/ha-chip-set";
|
||||||
import "../chips/ha-input-chip";
|
import "../chips/ha-input-chip";
|
||||||
@ -21,6 +18,7 @@ import "../ha-formfield";
|
|||||||
import "../ha-input-helper-text";
|
import "../ha-input-helper-text";
|
||||||
import "../ha-radio";
|
import "../ha-radio";
|
||||||
import "../ha-select";
|
import "../ha-select";
|
||||||
|
import "../ha-sortable";
|
||||||
|
|
||||||
@customElement("ha-selector-select")
|
@customElement("ha-selector-select")
|
||||||
export class HaSelectSelector extends LitElement {
|
export class HaSelectSelector extends LitElement {
|
||||||
@ -42,50 +40,10 @@ export class HaSelectSelector extends LitElement {
|
|||||||
|
|
||||||
@query("ha-combo-box", true) private comboBox!: HaComboBox;
|
@query("ha-combo-box", true) private comboBox!: HaComboBox;
|
||||||
|
|
||||||
private _sortable?: SortableInstance;
|
private _itemMoved(ev: CustomEvent): void {
|
||||||
|
ev.stopPropagation();
|
||||||
protected updated(changedProps: PropertyValues): void {
|
const { oldIndex, newIndex } = ev.detail;
|
||||||
if (changedProps.has("value") || changedProps.has("selector")) {
|
this._move(oldIndex!, newIndex);
|
||||||
const sortableNeeded =
|
|
||||||
this.selector.select?.multiple &&
|
|
||||||
this.selector.select.reorder &&
|
|
||||||
this.value?.length;
|
|
||||||
if (!this._sortable && sortableNeeded) {
|
|
||||||
this._createSortable();
|
|
||||||
} else if (this._sortable && !sortableNeeded) {
|
|
||||||
this._destroySortable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _createSortable() {
|
|
||||||
const Sortable = (await import("../../resources/sortable")).default;
|
|
||||||
this._sortable = new Sortable(
|
|
||||||
this.shadowRoot!.querySelector("ha-chip-set")!,
|
|
||||||
{
|
|
||||||
animation: 150,
|
|
||||||
fallbackClass: "sortable-fallback",
|
|
||||||
draggable: "ha-input-chip",
|
|
||||||
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 _dragged(ev: SortableEvent): void {
|
|
||||||
if (ev.oldIndex === ev.newIndex) return;
|
|
||||||
this._move(ev.oldIndex!, ev.newIndex!);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _move(index: number, newIndex: number) {
|
private _move(index: number, newIndex: number) {
|
||||||
@ -99,11 +57,6 @@ export class HaSelectSelector extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _destroySortable() {
|
|
||||||
this._sortable?.destroy();
|
|
||||||
this._sortable = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _filter = "";
|
private _filter = "";
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
@ -195,37 +148,43 @@ export class HaSelectSelector extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
${value?.length
|
${value?.length
|
||||||
? html`
|
? html`
|
||||||
<ha-chip-set>
|
<ha-sortable
|
||||||
${repeat(
|
no-style
|
||||||
value,
|
.disabled=${!this.selector.select.reorder}
|
||||||
(item) => item,
|
@item-moved=${this._itemMoved}
|
||||||
(item, idx) => {
|
>
|
||||||
const label =
|
<ha-chip-set>
|
||||||
options.find((option) => option.value === item)?.label ||
|
${repeat(
|
||||||
item;
|
value,
|
||||||
return html`
|
(item) => item,
|
||||||
<ha-input-chip
|
(item, idx) => {
|
||||||
.idx=${idx}
|
const label =
|
||||||
@remove=${this._removeItem}
|
options.find((option) => option.value === item)
|
||||||
.label=${label}
|
?.label || item;
|
||||||
selected
|
return html`
|
||||||
>
|
<ha-input-chip
|
||||||
${this.selector.select?.reorder
|
.idx=${idx}
|
||||||
? html`
|
@remove=${this._removeItem}
|
||||||
<ha-svg-icon
|
.label=${label}
|
||||||
slot="icon"
|
selected
|
||||||
.path=${mdiDrag}
|
>
|
||||||
data-handle
|
${this.selector.select?.reorder
|
||||||
></ha-svg-icon>
|
? html`
|
||||||
`
|
<ha-svg-icon
|
||||||
: nothing}
|
slot="icon"
|
||||||
${options.find((option) => option.value === item)
|
.path=${mdiDrag}
|
||||||
?.label || item}
|
data-handle
|
||||||
</ha-input-chip>
|
></ha-svg-icon>
|
||||||
`;
|
`
|
||||||
}
|
: nothing}
|
||||||
)}
|
${options.find((option) => option.value === item)
|
||||||
</ha-chip-set>
|
?.label || item}
|
||||||
|
</ha-input-chip>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</ha-chip-set>
|
||||||
|
</ha-sortable>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
|
|
||||||
@ -419,25 +378,35 @@ export class HaSelectSelector extends LitElement {
|
|||||||
this.comboBox.filteredItems = filteredItems;
|
this.comboBox.filteredItems = filteredItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = [
|
static styles = css`
|
||||||
sortableStyles,
|
:host {
|
||||||
css`
|
position: relative;
|
||||||
:host {
|
}
|
||||||
position: relative;
|
ha-select,
|
||||||
}
|
mwc-formfield,
|
||||||
ha-select,
|
ha-formfield {
|
||||||
mwc-formfield,
|
display: block;
|
||||||
ha-formfield {
|
}
|
||||||
display: block;
|
mwc-list-item[disabled] {
|
||||||
}
|
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
||||||
mwc-list-item[disabled] {
|
}
|
||||||
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
ha-chip-set {
|
||||||
}
|
padding: 8px 0;
|
||||||
ha-chip-set {
|
}
|
||||||
padding: 8px 0;
|
|
||||||
}
|
.sortable-fallback {
|
||||||
`,
|
display: none;
|
||||||
];
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable-ghost {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable-drag {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -33,7 +33,6 @@ import {
|
|||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, eventOptions, property, state } from "lit/decorators";
|
import { customElement, eventOptions, property, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { guard } from "lit/directives/guard";
|
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { storage } from "../common/decorators/storage";
|
import { storage } from "../common/decorators/storage";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
@ -50,12 +49,12 @@ import { subscribeRepairsIssueRegistry } from "../data/repairs";
|
|||||||
import { UpdateEntity, updateCanInstall } from "../data/update";
|
import { UpdateEntity, updateCanInstall } from "../data/update";
|
||||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||||
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
|
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
|
||||||
import type { SortableInstance } from "../resources/sortable";
|
|
||||||
import { haStyleScrollbar } from "../resources/styles";
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
import type { HomeAssistant, PanelInfo, Route } from "../types";
|
import type { HomeAssistant, PanelInfo, Route } from "../types";
|
||||||
import "./ha-icon";
|
import "./ha-icon";
|
||||||
import "./ha-icon-button";
|
import "./ha-icon-button";
|
||||||
import "./ha-menu-button";
|
import "./ha-menu-button";
|
||||||
|
import "./ha-sortable";
|
||||||
import "./ha-svg-icon";
|
import "./ha-svg-icon";
|
||||||
import "./user/ha-user-badge";
|
import "./user/ha-user-badge";
|
||||||
|
|
||||||
@ -204,15 +203,13 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@state() private _issuesCount = 0;
|
@state() private _issuesCount = 0;
|
||||||
|
|
||||||
@state() private _renderEmptySortable = false;
|
|
||||||
|
|
||||||
private _mouseLeaveTimeout?: number;
|
private _mouseLeaveTimeout?: number;
|
||||||
|
|
||||||
private _tooltipHideTimeout?: number;
|
private _tooltipHideTimeout?: number;
|
||||||
|
|
||||||
private _recentKeydownActiveUntil = 0;
|
private _recentKeydownActiveUntil = 0;
|
||||||
|
|
||||||
private sortableStyleLoaded = false;
|
private _editStyleLoaded = false;
|
||||||
|
|
||||||
@storage({
|
@storage({
|
||||||
key: "sidebarPanelOrder",
|
key: "sidebarPanelOrder",
|
||||||
@ -228,8 +225,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
})
|
})
|
||||||
private _hiddenPanels: string[] = [];
|
private _hiddenPanels: string[] = [];
|
||||||
|
|
||||||
private _sortable?: SortableInstance;
|
|
||||||
|
|
||||||
public hassSubscribe(): UnsubscribeFunc[] {
|
public hassSubscribe(): UnsubscribeFunc[] {
|
||||||
return this.hass.user?.is_admin
|
return this.hass.user?.is_admin
|
||||||
? [
|
? [
|
||||||
@ -264,14 +259,13 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
changedProps.has("expanded") ||
|
changedProps.has("expanded") ||
|
||||||
changedProps.has("narrow") ||
|
changedProps.has("narrow") ||
|
||||||
changedProps.has("alwaysExpand") ||
|
changedProps.has("alwaysExpand") ||
|
||||||
|
changedProps.has("editMode") ||
|
||||||
changedProps.has("_externalConfig") ||
|
changedProps.has("_externalConfig") ||
|
||||||
changedProps.has("_updatesCount") ||
|
changedProps.has("_updatesCount") ||
|
||||||
changedProps.has("_issuesCount") ||
|
changedProps.has("_issuesCount") ||
|
||||||
changedProps.has("_notifications") ||
|
changedProps.has("_notifications") ||
|
||||||
changedProps.has("editMode") ||
|
|
||||||
changedProps.has("_renderEmptySortable") ||
|
|
||||||
changedProps.has("_hiddenPanels") ||
|
changedProps.has("_hiddenPanels") ||
|
||||||
(changedProps.has("_panelOrder") && !this.editMode)
|
changedProps.has("_panelOrder")
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -306,12 +300,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
if (changedProps.has("alwaysExpand")) {
|
if (changedProps.has("alwaysExpand")) {
|
||||||
toggleAttribute(this, "expanded", this.alwaysExpand);
|
toggleAttribute(this, "expanded", this.alwaysExpand);
|
||||||
}
|
}
|
||||||
if (changedProps.has("editMode")) {
|
if (changedProps.has("editMode") && this.editMode) {
|
||||||
if (this.editMode) {
|
this._editModeActivated();
|
||||||
this._activateEditMode();
|
|
||||||
} else {
|
|
||||||
this._deactivateEditMode();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!changedProps.has("hass")) {
|
if (!changedProps.has("hass")) {
|
||||||
return;
|
return;
|
||||||
@ -470,15 +460,36 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _panelMoved(ev: CustomEvent) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const { oldIndex, newIndex } = ev.detail;
|
||||||
|
|
||||||
|
const [beforeSpacer] = computePanels(
|
||||||
|
this.hass.panels,
|
||||||
|
this.hass.defaultPanel,
|
||||||
|
this._panelOrder,
|
||||||
|
this._hiddenPanels,
|
||||||
|
this.hass.locale
|
||||||
|
);
|
||||||
|
|
||||||
|
const panelOrder = beforeSpacer.map((panel) => panel.url_path);
|
||||||
|
const panel = panelOrder.splice(oldIndex, 1)[0];
|
||||||
|
panelOrder.splice(newIndex, 0, panel);
|
||||||
|
|
||||||
|
this._panelOrder = panelOrder;
|
||||||
|
}
|
||||||
|
|
||||||
private _renderPanelsEdit(beforeSpacer: PanelInfo[]) {
|
private _renderPanelsEdit(beforeSpacer: PanelInfo[]) {
|
||||||
// prettier-ignore
|
return html`
|
||||||
return html`<div id="sortable">
|
<ha-sortable
|
||||||
${guard([this._hiddenPanels, this._renderEmptySortable], () =>
|
handle-selector="paper-icon-item"
|
||||||
this._renderEmptySortable ? "" : this._renderPanels(beforeSpacer)
|
.disabled=${!this.editMode}
|
||||||
)}
|
@item-moved=${this._panelMoved}
|
||||||
</div>
|
>
|
||||||
${this._renderSpacer()}
|
<div class="reorder-list">${this._renderPanels(beforeSpacer)}</div>
|
||||||
${this._renderHiddenPanels()} `;
|
</ha-sortable>
|
||||||
|
${this._renderSpacer()}${this._renderHiddenPanels()}
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderHiddenPanels() {
|
private _renderHiddenPanels() {
|
||||||
@ -674,44 +685,22 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
fireEvent(this, "hass-edit-sidebar", { editMode: true });
|
fireEvent(this, "hass-edit-sidebar", { editMode: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _activateEditMode() {
|
private async _editModeActivated() {
|
||||||
await Promise.all([this._loadSortableStyle(), this._createSortable()]);
|
await this._loadEditStyle();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _loadSortableStyle() {
|
private async _loadEditStyle() {
|
||||||
if (this.sortableStyleLoaded) return;
|
if (this._editStyleLoaded) return;
|
||||||
|
|
||||||
const sortStylesImport = await import("../resources/ha-sortable-style");
|
const editStylesImport = await import("../resources/ha-sidebar-edit-style");
|
||||||
|
|
||||||
const style = document.createElement("style");
|
const style = document.createElement("style");
|
||||||
style.innerHTML = (sortStylesImport.sortableStyles as CSSResult).cssText;
|
style.innerHTML = (editStylesImport.sidebarEditStyle as CSSResult).cssText;
|
||||||
this.shadowRoot!.appendChild(style);
|
this.shadowRoot!.appendChild(style);
|
||||||
|
|
||||||
this.sortableStyleLoaded = true;
|
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _createSortable() {
|
|
||||||
const Sortable = (await import("../resources/sortable")).default;
|
|
||||||
this._sortable = new Sortable(
|
|
||||||
this.shadowRoot!.getElementById("sortable")!,
|
|
||||||
{
|
|
||||||
animation: 150,
|
|
||||||
fallbackClass: "sortable-fallback",
|
|
||||||
dataIdAttr: "data-panel",
|
|
||||||
handle: "paper-icon-item",
|
|
||||||
onSort: async () => {
|
|
||||||
this._panelOrder = this._sortable!.toArray();
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _deactivateEditMode() {
|
|
||||||
this._sortable?.destroy();
|
|
||||||
this._sortable = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _closeEditMode() {
|
private _closeEditMode() {
|
||||||
fireEvent(this, "hass-edit-sidebar", { editMode: false });
|
fireEvent(this, "hass-edit-sidebar", { editMode: false });
|
||||||
}
|
}
|
||||||
@ -724,13 +713,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
// Make a copy for Memoize
|
// Make a copy for Memoize
|
||||||
this._hiddenPanels = [...this._hiddenPanels, panel];
|
this._hiddenPanels = [...this._hiddenPanels, panel];
|
||||||
this._renderEmptySortable = true;
|
// Remove it from the panel order
|
||||||
await this.updateComplete;
|
this._panelOrder = this._panelOrder.filter((order) => order !== panel);
|
||||||
const container = this.shadowRoot!.getElementById("sortable")!;
|
|
||||||
while (container.lastElementChild) {
|
|
||||||
container.removeChild(container.lastElementChild);
|
|
||||||
}
|
|
||||||
this._renderEmptySortable = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _unhidePanel(ev: Event) {
|
private async _unhidePanel(ev: Event) {
|
||||||
@ -739,13 +723,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
this._hiddenPanels = this._hiddenPanels.filter(
|
this._hiddenPanels = this._hiddenPanels.filter(
|
||||||
(hidden) => hidden !== panel
|
(hidden) => hidden !== panel
|
||||||
);
|
);
|
||||||
this._renderEmptySortable = true;
|
|
||||||
await this.updateComplete;
|
|
||||||
const container = this.shadowRoot!.getElementById("sortable")!;
|
|
||||||
while (container.lastElementChild) {
|
|
||||||
container.removeChild(container.lastElementChild);
|
|
||||||
}
|
|
||||||
this._renderEmptySortable = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _itemMouseEnter(ev: MouseEvent) {
|
private _itemMouseEnter(ev: MouseEvent) {
|
||||||
@ -910,7 +887,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
.menu mwc-button {
|
.menu mwc-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
#sortable,
|
.reorder-list,
|
||||||
.hidden-panel {
|
.hidden-panel {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
153
src/components/ha-sortable.ts
Normal file
153
src/components/ha-sortable.ts
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
/* eslint-disable lit/prefer-static-styles */
|
||||||
|
import { html, LitElement, nothing, PropertyValues } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import type { SortableEvent } from "sortablejs";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import type { SortableInstance } from "../resources/sortable";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HASSDomEvents {
|
||||||
|
"item-moved": {
|
||||||
|
oldIndex: number;
|
||||||
|
newIndex: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("ha-sortable")
|
||||||
|
export class HaSortable extends LitElement {
|
||||||
|
private _sortable?: SortableInstance;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "no-style" })
|
||||||
|
public noStyle: boolean = false;
|
||||||
|
|
||||||
|
@property({ type: String, attribute: "draggable-selector" })
|
||||||
|
public draggableSelector?: string;
|
||||||
|
|
||||||
|
@property({ type: String, attribute: "handle-selector" })
|
||||||
|
public handleSelector?: string;
|
||||||
|
|
||||||
|
protected updated(changedProperties: PropertyValues<this>) {
|
||||||
|
if (changedProperties.has("disabled")) {
|
||||||
|
if (this.disabled) {
|
||||||
|
this._destroySortable();
|
||||||
|
} else {
|
||||||
|
this._createSortable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workaround for connectedCallback just after disconnectedCallback (when dragging sortable with sortable children)
|
||||||
|
private _shouldBeDestroy = false;
|
||||||
|
|
||||||
|
public disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._shouldBeDestroy = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this._shouldBeDestroy) {
|
||||||
|
this._destroySortable();
|
||||||
|
this._shouldBeDestroy = false;
|
||||||
|
}
|
||||||
|
}, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._shouldBeDestroy = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createRenderRoot() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (this.noStyle) return nothing;
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
.sortable-fallback {
|
||||||
|
display: none;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable-ghost {
|
||||||
|
border: 2px solid var(--primary-color);
|
||||||
|
background: rgba(var(--rgb-primary-color), 0.25);
|
||||||
|
border-radius: 4px;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable-drag {
|
||||||
|
border-radius: 4px;
|
||||||
|
opacity: 1;
|
||||||
|
background: var(--card-background-color);
|
||||||
|
box-shadow: 0px 4px 8px 3px #00000026;
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _createSortable() {
|
||||||
|
if (this._sortable) return;
|
||||||
|
const container = this.children[0] as HTMLElement | undefined;
|
||||||
|
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const Sortable = (await import("../resources/sortable")).default;
|
||||||
|
|
||||||
|
const options: SortableInstance.Options = {
|
||||||
|
animation: 150,
|
||||||
|
onChoose: this._handleChoose,
|
||||||
|
onEnd: this._handleEnd,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.draggableSelector) {
|
||||||
|
options.draggable = this.draggableSelector;
|
||||||
|
}
|
||||||
|
if (this.handleSelector) {
|
||||||
|
options.handle = this.handleSelector;
|
||||||
|
}
|
||||||
|
this._sortable = new Sortable(container, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleEnd = (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;
|
||||||
|
}
|
||||||
|
// if item was not moved, ignore
|
||||||
|
if (
|
||||||
|
evt.oldIndex === undefined ||
|
||||||
|
evt.newIndex === undefined ||
|
||||||
|
evt.oldIndex === evt.newIndex
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent(this, "item-moved", {
|
||||||
|
oldIndex: evt.oldIndex!,
|
||||||
|
newIndex: evt.newIndex!,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private _handleChoose = (evt: SortableEvent) => {
|
||||||
|
(evt.item as any).placeholder = document.createComment("sort-placeholder");
|
||||||
|
evt.item.after((evt.item as any).placeholder);
|
||||||
|
};
|
||||||
|
|
||||||
|
private _destroySortable() {
|
||||||
|
if (!this._sortable) return;
|
||||||
|
this._sortable.destroy();
|
||||||
|
this._sortable = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-sortable": HaSortable;
|
||||||
|
}
|
||||||
|
}
|
@ -4,15 +4,13 @@ import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
|||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import type { SortableEvent } from "sortablejs";
|
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import type { AreaFilterValue } from "../../components/ha-area-filter";
|
import type { AreaFilterValue } from "../../components/ha-area-filter";
|
||||||
import "../../components/ha-button";
|
import "../../components/ha-button";
|
||||||
import "../../components/ha-icon-button";
|
import "../../components/ha-icon-button";
|
||||||
import "../../components/ha-list-item";
|
import "../../components/ha-list-item";
|
||||||
|
import "../../components/ha-sortable";
|
||||||
import { areaCompare } from "../../data/area_registry";
|
import { areaCompare } from "../../data/area_registry";
|
||||||
import { sortableStyles } from "../../resources/ha-sortable-style";
|
|
||||||
import type { SortableInstance } from "../../resources/sortable";
|
|
||||||
import { haStyleDialog } from "../../resources/styles";
|
import { haStyleDialog } from "../../resources/styles";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import { HassDialog } from "../make-dialog-manager";
|
import { HassDialog } from "../make-dialog-manager";
|
||||||
@ -31,23 +29,18 @@ export class DialogAreaFilter
|
|||||||
|
|
||||||
@state() private _areas: string[] = [];
|
@state() private _areas: string[] = [];
|
||||||
|
|
||||||
private _sortable?: SortableInstance;
|
public showDialog(dialogParams: AreaFilterDialogParams): void {
|
||||||
|
|
||||||
public async showDialog(dialogParams: AreaFilterDialogParams): Promise<void> {
|
|
||||||
this._dialogParams = dialogParams;
|
this._dialogParams = dialogParams;
|
||||||
this._hidden = dialogParams.initialValue?.hidden ?? [];
|
this._hidden = dialogParams.initialValue?.hidden ?? [];
|
||||||
const order = dialogParams.initialValue?.order ?? [];
|
const order = dialogParams.initialValue?.order ?? [];
|
||||||
const allAreas = Object.keys(this.hass!.areas);
|
const allAreas = Object.keys(this.hass!.areas);
|
||||||
this._areas = allAreas.concat().sort(areaCompare(this.hass!.areas, order));
|
this._areas = allAreas.concat().sort(areaCompare(this.hass!.areas, order));
|
||||||
await this.updateComplete;
|
|
||||||
this._createSortable();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog(): void {
|
public closeDialog(): void {
|
||||||
this._dialogParams = undefined;
|
this._dialogParams = undefined;
|
||||||
this._hidden = [];
|
this._hidden = [];
|
||||||
this._areas = [];
|
this._areas = [];
|
||||||
this._destroySortable();
|
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,42 +59,14 @@ export class DialogAreaFilter
|
|||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _createSortable() {
|
private _areaMoved(ev: CustomEvent): void {
|
||||||
const Sortable = (await import("../../resources/sortable")).default;
|
ev.stopPropagation();
|
||||||
if (this._sortable) return;
|
const { oldIndex, newIndex } = ev.detail;
|
||||||
this._sortable = new Sortable(this.shadowRoot!.querySelector(".areas")!, {
|
|
||||||
animation: 150,
|
|
||||||
fallbackClass: "sortable-fallback",
|
|
||||||
handle: ".handle",
|
|
||||||
draggable: ".draggable",
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _dragged(ev: SortableEvent): void {
|
|
||||||
if (ev.oldIndex === ev.newIndex) return;
|
|
||||||
|
|
||||||
const areas = this._areas.concat();
|
const areas = this._areas.concat();
|
||||||
|
|
||||||
const option = areas.splice(ev.oldIndex!, 1)[0];
|
const option = areas.splice(oldIndex, 1)[0];
|
||||||
areas.splice(ev.newIndex!, 0, option);
|
areas.splice(newIndex, 0, option);
|
||||||
|
|
||||||
this._areas = areas;
|
this._areas = areas;
|
||||||
}
|
}
|
||||||
@ -120,50 +85,56 @@ export class DialogAreaFilter
|
|||||||
.heading=${this._dialogParams.title ??
|
.heading=${this._dialogParams.title ??
|
||||||
this.hass.localize("ui.components.area-filter.title")}
|
this.hass.localize("ui.components.area-filter.title")}
|
||||||
>
|
>
|
||||||
<mwc-list class="areas">
|
<ha-sortable
|
||||||
${repeat(
|
draggable-selector=".draggable"
|
||||||
allAreas,
|
handle-selector=".handle"
|
||||||
(area) => area,
|
@item-moved=${this._areaMoved}
|
||||||
(area, _idx) => {
|
>
|
||||||
const isVisible = !this._hidden.includes(area);
|
<mwc-list class="areas">
|
||||||
const name = this.hass!.areas[area]?.name || area;
|
${repeat(
|
||||||
return html`
|
allAreas,
|
||||||
<ha-list-item
|
(area) => area,
|
||||||
class=${classMap({
|
(area, _idx) => {
|
||||||
hidden: !isVisible,
|
const isVisible = !this._hidden.includes(area);
|
||||||
draggable: isVisible,
|
const name = this.hass!.areas[area]?.name || area;
|
||||||
})}
|
return html`
|
||||||
hasMeta
|
<ha-list-item
|
||||||
graphic="icon"
|
class=${classMap({
|
||||||
noninteractive
|
hidden: !isVisible,
|
||||||
>
|
draggable: isVisible,
|
||||||
${isVisible
|
})}
|
||||||
? html`<ha-svg-icon
|
hasMeta
|
||||||
class="handle"
|
graphic="icon"
|
||||||
.path=${mdiDrag}
|
noninteractive
|
||||||
slot="graphic"
|
>
|
||||||
></ha-svg-icon>`
|
${isVisible
|
||||||
: nothing}
|
? html`<ha-svg-icon
|
||||||
${name}
|
class="handle"
|
||||||
<ha-icon-button
|
.path=${mdiDrag}
|
||||||
tabindex="0"
|
slot="graphic"
|
||||||
class="action"
|
></ha-svg-icon>`
|
||||||
.path=${isVisible ? mdiEye : mdiEyeOff}
|
: nothing}
|
||||||
slot="meta"
|
${name}
|
||||||
.label=${this.hass!.localize(
|
<ha-icon-button
|
||||||
`ui.components.area-filter.${
|
tabindex="0"
|
||||||
isVisible ? "hide" : "show"
|
class="action"
|
||||||
}`,
|
.path=${isVisible ? mdiEye : mdiEyeOff}
|
||||||
{ area: name }
|
slot="meta"
|
||||||
)}
|
.label=${this.hass!.localize(
|
||||||
.area=${area}
|
`ui.components.area-filter.${
|
||||||
@click=${this._toggle}
|
isVisible ? "hide" : "show"
|
||||||
></ha-icon-button>
|
}`,
|
||||||
</ha-list-item>
|
{ area: name }
|
||||||
`;
|
)}
|
||||||
}
|
.area=${area}
|
||||||
)}
|
@click=${this._toggle}
|
||||||
</mwc-list>
|
></ha-icon-button>
|
||||||
|
</ha-list-item>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</mwc-list>
|
||||||
|
</ha-sortable>
|
||||||
<ha-button slot="secondaryAction" dialogAction="cancel">
|
<ha-button slot="secondaryAction" dialogAction="cancel">
|
||||||
${this.hass.localize("ui.common.cancel")}
|
${this.hass.localize("ui.common.cancel")}
|
||||||
</ha-button>
|
</ha-button>
|
||||||
@ -192,7 +163,6 @@ export class DialogAreaFilter
|
|||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
sortableStyles,
|
|
||||||
haStyleDialog,
|
haStyleDialog,
|
||||||
css`
|
css`
|
||||||
ha-dialog {
|
ha-dialog {
|
||||||
|
@ -10,9 +10,9 @@ import {
|
|||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import type { SortableEvent } from "sortablejs";
|
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import "../../../../components/ha-control-slider";
|
import "../../../../components/ha-control-slider";
|
||||||
|
import "../../../../components/ha-sortable";
|
||||||
import { UNAVAILABLE } from "../../../../data/entity";
|
import { UNAVAILABLE } from "../../../../data/entity";
|
||||||
import {
|
import {
|
||||||
ExtEntityRegistryEntry,
|
ExtEntityRegistryEntry,
|
||||||
@ -24,7 +24,6 @@ import {
|
|||||||
computeDefaultFavoriteColors,
|
computeDefaultFavoriteColors,
|
||||||
} from "../../../../data/light";
|
} from "../../../../data/light";
|
||||||
import { actionHandler } from "../../../../panels/lovelace/common/directives/action-handler-directive";
|
import { actionHandler } from "../../../../panels/lovelace/common/directives/action-handler-directive";
|
||||||
import type { SortableInstance } from "../../../../resources/sortable";
|
|
||||||
import { HomeAssistant } from "../../../../types";
|
import { HomeAssistant } from "../../../../types";
|
||||||
import { showConfirmationDialog } from "../../../generic/show-dialog-box";
|
import { showConfirmationDialog } from "../../../generic/show-dialog-box";
|
||||||
import "./ha-favorite-color-button";
|
import "./ha-favorite-color-button";
|
||||||
@ -48,16 +47,7 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
|
|||||||
|
|
||||||
@state() private _favoriteColors: LightColor[] = [];
|
@state() private _favoriteColors: LightColor[] = [];
|
||||||
|
|
||||||
private _sortable?: SortableInstance;
|
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues): void {
|
protected updated(changedProps: PropertyValues): void {
|
||||||
if (changedProps.has("editMode")) {
|
|
||||||
if (this.editMode) {
|
|
||||||
this._createSortable();
|
|
||||||
} else {
|
|
||||||
this._destroySortable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (changedProps.has("entry")) {
|
if (changedProps.has("entry")) {
|
||||||
if (this.entry) {
|
if (this.entry) {
|
||||||
if (this.entry.options?.light?.favorite_colors) {
|
if (this.entry.options?.light?.favorite_colors) {
|
||||||
@ -69,34 +59,10 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _createSortable() {
|
private _colorMoved(ev: CustomEvent): void {
|
||||||
const Sortable = (await import("../../../../resources/sortable")).default;
|
ev.stopPropagation();
|
||||||
this._sortable = new Sortable(
|
const { oldIndex, newIndex } = ev.detail;
|
||||||
this.shadowRoot!.querySelector(".container")!,
|
this._move(oldIndex, newIndex);
|
||||||
{
|
|
||||||
animation: 150,
|
|
||||||
fallbackClass: "sortable-fallback",
|
|
||||||
draggable: ".color",
|
|
||||||
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 _dragged(ev: SortableEvent): void {
|
|
||||||
if (ev.oldIndex === ev.newIndex) return;
|
|
||||||
this._move(ev.oldIndex!, ev.newIndex!);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _move(index: number, newIndex: number) {
|
private _move(index: number, newIndex: number) {
|
||||||
@ -107,11 +73,6 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
|
|||||||
this._save(favoriteColors);
|
this._save(favoriteColors);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _destroySortable() {
|
|
||||||
this._sortable?.destroy();
|
|
||||||
this._sortable = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _apply = (index: number) => {
|
private _apply = (index: number) => {
|
||||||
const favorite = this._favoriteColors[index];
|
const favorite = this._favoriteColors[index];
|
||||||
this.hass.callService("light", "turn_on", {
|
this.hass.callService("light", "turn_on", {
|
||||||
@ -223,72 +184,79 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
|
|||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="container">
|
<ha-sortable
|
||||||
${this._favoriteColors.map(
|
@item-moved=${this._colorMoved}
|
||||||
(color, index) => html`
|
item=".color"
|
||||||
<div class="color">
|
no-style
|
||||||
<div
|
.disabled=${!this.editMode}
|
||||||
class="color-bubble ${classMap({
|
>
|
||||||
shake: !!this.editMode,
|
<div class="container">
|
||||||
})}"
|
${this._favoriteColors.map(
|
||||||
>
|
(color, index) => html`
|
||||||
<ha-favorite-color-button
|
<div class="color">
|
||||||
.label=${this.hass.localize(
|
<div
|
||||||
`ui.dialogs.more_info_control.light.favorite_color.${
|
class="color-bubble ${classMap({
|
||||||
this.editMode ? "edit" : "set"
|
shake: !!this.editMode,
|
||||||
}`,
|
})}"
|
||||||
{ number: index }
|
|
||||||
)}
|
|
||||||
.disabled=${this.stateObj!.state === UNAVAILABLE}
|
|
||||||
.color=${color}
|
|
||||||
.index=${index}
|
|
||||||
.actionHandler=${actionHandler({
|
|
||||||
hasHold: !this.editMode && this.hass.user?.is_admin,
|
|
||||||
disabled: this.stateObj!.state === UNAVAILABLE,
|
|
||||||
})}
|
|
||||||
@action=${this._handleColorAction}
|
|
||||||
>
|
>
|
||||||
</ha-favorite-color-button>
|
<ha-favorite-color-button
|
||||||
${this.editMode
|
.label=${this.hass.localize(
|
||||||
? html`
|
`ui.dialogs.more_info_control.light.favorite_color.${
|
||||||
<button
|
this.editMode ? "edit" : "set"
|
||||||
@click=${this._handleDeleteButton}
|
}`,
|
||||||
class="delete"
|
{ number: index }
|
||||||
.index=${index}
|
)}
|
||||||
aria-label=${this.hass.localize(
|
.disabled=${this.stateObj!.state === UNAVAILABLE}
|
||||||
`ui.dialogs.more_info_control.light.favorite_color.delete`,
|
.color=${color}
|
||||||
{ number: index }
|
.index=${index}
|
||||||
)}
|
.actionHandler=${actionHandler({
|
||||||
.title=${this.hass.localize(
|
hasHold: !this.editMode && this.hass.user?.is_admin,
|
||||||
`ui.dialogs.more_info_control.light.favorite_color.delete`,
|
disabled: this.stateObj!.state === UNAVAILABLE,
|
||||||
{ number: index }
|
})}
|
||||||
)}
|
@action=${this._handleColorAction}
|
||||||
>
|
>
|
||||||
<ha-svg-icon .path=${mdiMinus}></ha-svg-icon>
|
</ha-favorite-color-button>
|
||||||
</button>
|
${this.editMode
|
||||||
`
|
? html`
|
||||||
: nothing}
|
<button
|
||||||
|
@click=${this._handleDeleteButton}
|
||||||
|
class="delete"
|
||||||
|
.index=${index}
|
||||||
|
aria-label=${this.hass.localize(
|
||||||
|
`ui.dialogs.more_info_control.light.favorite_color.delete`,
|
||||||
|
{ number: index }
|
||||||
|
)}
|
||||||
|
.title=${this.hass.localize(
|
||||||
|
`ui.dialogs.more_info_control.light.favorite_color.delete`,
|
||||||
|
{ number: index }
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ha-svg-icon .path=${mdiMinus}></ha-svg-icon>
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
${this.editMode
|
|
||||||
? html`
|
|
||||||
<ha-outlined-icon-button
|
|
||||||
class="button"
|
|
||||||
@click=${this._handleAddButton}
|
|
||||||
>
|
|
||||||
<ha-svg-icon .path=${mdiPlus}></ha-svg-icon>
|
|
||||||
</ha-outlined-icon-button>
|
|
||||||
<ha-outlined-icon-button
|
|
||||||
@click=${this._exitEditMode}
|
|
||||||
class="button"
|
|
||||||
>
|
|
||||||
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
|
|
||||||
</ha-outlined-icon-button>
|
|
||||||
`
|
`
|
||||||
: nothing}
|
)}
|
||||||
</div>
|
${this.editMode
|
||||||
|
? html`
|
||||||
|
<ha-outlined-icon-button
|
||||||
|
class="button"
|
||||||
|
@click=${this._handleAddButton}
|
||||||
|
>
|
||||||
|
<ha-svg-icon .path=${mdiPlus}></ha-svg-icon>
|
||||||
|
</ha-outlined-icon-button>
|
||||||
|
<ha-outlined-icon-button
|
||||||
|
@click=${this._exitEditMode}
|
||||||
|
class="button"
|
||||||
|
>
|
||||||
|
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
|
||||||
|
</ha-outlined-icon-button>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
</ha-sortable>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,19 +1,16 @@
|
|||||||
import "@material/mwc-button";
|
|
||||||
import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
|
import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
|
||||||
import deepClone from "deep-clone-simple";
|
import deepClone from "deep-clone-simple";
|
||||||
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
|
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import type { SortableEvent } from "sortablejs";
|
|
||||||
import { storage } from "../../../../common/decorators/storage";
|
import { storage } from "../../../../common/decorators/storage";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import "../../../../components/ha-button";
|
import "../../../../components/ha-button";
|
||||||
import "../../../../components/ha-svg-icon";
|
import "../../../../components/ha-svg-icon";
|
||||||
|
import "../../../../components/ha-sortable";
|
||||||
import { getService, isService } from "../../../../data/action";
|
import { getService, isService } from "../../../../data/action";
|
||||||
import type { AutomationClipboard } from "../../../../data/automation";
|
import type { AutomationClipboard } from "../../../../data/automation";
|
||||||
import { Action } from "../../../../data/script";
|
import { Action } from "../../../../data/script";
|
||||||
import { sortableStyles } from "../../../../resources/ha-sortable-style";
|
|
||||||
import type { SortableInstance } from "../../../../resources/sortable";
|
|
||||||
import { HomeAssistant } from "../../../../types";
|
import { HomeAssistant } from "../../../../types";
|
||||||
import {
|
import {
|
||||||
PASTE_VALUE,
|
PASTE_VALUE,
|
||||||
@ -48,8 +45,6 @@ export default class HaAutomationAction extends LitElement {
|
|||||||
|
|
||||||
private _actionKeys = new WeakMap<Action, string>();
|
private _actionKeys = new WeakMap<Action, string>();
|
||||||
|
|
||||||
private _sortable?: SortableInstance;
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
${this.reOrderMode && !this.nested
|
${this.reOrderMode && !this.nested
|
||||||
@ -63,62 +58,68 @@ export default class HaAutomationAction extends LitElement {
|
|||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.automation.editor.re_order_mode.description_actions"
|
"ui.panel.config.automation.editor.re_order_mode.description_actions"
|
||||||
)}
|
)}
|
||||||
<mwc-button slot="action" @click=${this._exitReOrderMode}>
|
<ha-button slot="action" @click=${this._exitReOrderMode}>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.automation.editor.re_order_mode.exit"
|
"ui.panel.config.automation.editor.re_order_mode.exit"
|
||||||
)}
|
)}
|
||||||
</mwc-button>
|
</ha-button>
|
||||||
</ha-alert>
|
</ha-alert>
|
||||||
`
|
`
|
||||||
: null}
|
: null}
|
||||||
<div class="actions">
|
<ha-sortable
|
||||||
${repeat(
|
handle-selector=".handle"
|
||||||
this.actions,
|
.disabled=${!this.reOrderMode}
|
||||||
(action) => this._getKey(action),
|
@item-moved=${this._actionMoved}
|
||||||
(action, idx) => html`
|
>
|
||||||
<ha-automation-action-row
|
<div class="actions">
|
||||||
.index=${idx}
|
${repeat(
|
||||||
.action=${action}
|
this.actions,
|
||||||
.narrow=${this.narrow}
|
(action) => this._getKey(action),
|
||||||
.disabled=${this.disabled}
|
(action, idx) => html`
|
||||||
.hideMenu=${this.reOrderMode}
|
<ha-automation-action-row
|
||||||
.reOrderMode=${this.reOrderMode}
|
.index=${idx}
|
||||||
@duplicate=${this._duplicateAction}
|
.action=${action}
|
||||||
@value-changed=${this._actionChanged}
|
.narrow=${this.narrow}
|
||||||
@re-order=${this._enterReOrderMode}
|
.disabled=${this.disabled}
|
||||||
.hass=${this.hass}
|
.hideMenu=${this.reOrderMode}
|
||||||
>
|
.reOrderMode=${this.reOrderMode}
|
||||||
${this.reOrderMode
|
@duplicate=${this._duplicateAction}
|
||||||
? html`
|
@value-changed=${this._actionChanged}
|
||||||
<ha-icon-button
|
@re-order=${this._enterReOrderMode}
|
||||||
.index=${idx}
|
.hass=${this.hass}
|
||||||
slot="icons"
|
>
|
||||||
.label=${this.hass.localize(
|
${this.reOrderMode
|
||||||
"ui.panel.config.automation.editor.move_up"
|
? html`
|
||||||
)}
|
<ha-icon-button
|
||||||
.path=${mdiArrowUp}
|
.index=${idx}
|
||||||
@click=${this._moveUp}
|
slot="icons"
|
||||||
.disabled=${idx === 0}
|
.label=${this.hass.localize(
|
||||||
></ha-icon-button>
|
"ui.panel.config.automation.editor.move_up"
|
||||||
<ha-icon-button
|
)}
|
||||||
.index=${idx}
|
.path=${mdiArrowUp}
|
||||||
slot="icons"
|
@click=${this._moveUp}
|
||||||
.label=${this.hass.localize(
|
.disabled=${idx === 0}
|
||||||
"ui.panel.config.automation.editor.move_down"
|
></ha-icon-button>
|
||||||
)}
|
<ha-icon-button
|
||||||
.path=${mdiArrowDown}
|
.index=${idx}
|
||||||
@click=${this._moveDown}
|
slot="icons"
|
||||||
.disabled=${idx === this.actions.length - 1}
|
.label=${this.hass.localize(
|
||||||
></ha-icon-button>
|
"ui.panel.config.automation.editor.move_down"
|
||||||
<div class="handle" slot="icons">
|
)}
|
||||||
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
.path=${mdiArrowDown}
|
||||||
</div>
|
@click=${this._moveDown}
|
||||||
`
|
.disabled=${idx === this.actions.length - 1}
|
||||||
: ""}
|
></ha-icon-button>
|
||||||
</ha-automation-action-row>
|
<div class="handle" slot="icons">
|
||||||
`
|
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
||||||
)}
|
</div>
|
||||||
</div>
|
`
|
||||||
|
: ""}
|
||||||
|
</ha-automation-action-row>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ha-sortable>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<ha-button
|
<ha-button
|
||||||
outlined
|
outlined
|
||||||
@ -146,13 +147,6 @@ export default class HaAutomationAction extends LitElement {
|
|||||||
protected updated(changedProps: PropertyValues) {
|
protected updated(changedProps: PropertyValues) {
|
||||||
super.updated(changedProps);
|
super.updated(changedProps);
|
||||||
|
|
||||||
if (changedProps.has("reOrderMode")) {
|
|
||||||
if (this.reOrderMode) {
|
|
||||||
this._createSortable();
|
|
||||||
} else {
|
|
||||||
this._destroySortable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (changedProps.has("actions") && this._focusLastActionOnChange) {
|
if (changedProps.has("actions") && this._focusLastActionOnChange) {
|
||||||
this._focusLastActionOnChange = false;
|
this._focusLastActionOnChange = false;
|
||||||
|
|
||||||
@ -215,33 +209,6 @@ export default class HaAutomationAction extends LitElement {
|
|||||||
this.reOrderMode = false;
|
this.reOrderMode = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _createSortable() {
|
|
||||||
const Sortable = (await import("../../../../resources/sortable")).default;
|
|
||||||
this._sortable = new Sortable(this.shadowRoot!.querySelector(".actions")!, {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getKey(action: Action) {
|
private _getKey(action: Action) {
|
||||||
if (!this._actionKeys.has(action)) {
|
if (!this._actionKeys.has(action)) {
|
||||||
this._actionKeys.set(action, Math.random().toString());
|
this._actionKeys.set(action, Math.random().toString());
|
||||||
@ -262,11 +229,6 @@ export default class HaAutomationAction extends LitElement {
|
|||||||
this._move(index, newIndex);
|
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) {
|
private _move(index: number, newIndex: number) {
|
||||||
const actions = this.actions.concat();
|
const actions = this.actions.concat();
|
||||||
const action = actions.splice(index, 1)[0];
|
const action = actions.splice(index, 1)[0];
|
||||||
@ -274,6 +236,12 @@ export default class HaAutomationAction extends LitElement {
|
|||||||
fireEvent(this, "value-changed", { value: actions });
|
fireEvent(this, "value-changed", { value: actions });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _actionMoved(ev: CustomEvent): void {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const { oldIndex, newIndex } = ev.detail;
|
||||||
|
this._move(oldIndex, newIndex);
|
||||||
|
}
|
||||||
|
|
||||||
private _actionChanged(ev: CustomEvent) {
|
private _actionChanged(ev: CustomEvent) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const actions = [...this.actions];
|
const actions = [...this.actions];
|
||||||
@ -302,39 +270,36 @@ export default class HaAutomationAction extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return css`
|
||||||
sortableStyles,
|
ha-automation-action-row {
|
||||||
css`
|
display: block;
|
||||||
ha-automation-action-row {
|
margin-bottom: 16px;
|
||||||
display: block;
|
scroll-margin-top: 48px;
|
||||||
margin-bottom: 16px;
|
}
|
||||||
scroll-margin-top: 48px;
|
ha-svg-icon {
|
||||||
}
|
height: 20px;
|
||||||
ha-svg-icon {
|
}
|
||||||
height: 20px;
|
ha-alert {
|
||||||
}
|
display: block;
|
||||||
ha-alert {
|
margin-bottom: 16px;
|
||||||
display: block;
|
border-radius: var(--ha-card-border-radius, 12px);
|
||||||
margin-bottom: 16px;
|
overflow: hidden;
|
||||||
border-radius: var(--ha-card-border-radius, 12px);
|
}
|
||||||
overflow: hidden;
|
.handle {
|
||||||
}
|
padding: 12px;
|
||||||
.handle {
|
cursor: move; /* fallback if grab cursor is unsupported */
|
||||||
cursor: move; /* fallback if grab cursor is unsupported */
|
cursor: grab;
|
||||||
cursor: grab;
|
}
|
||||||
padding: 12px;
|
.handle ha-svg-icon {
|
||||||
}
|
pointer-events: none;
|
||||||
.handle ha-svg-icon {
|
height: 24px;
|
||||||
pointer-events: none;
|
}
|
||||||
height: 24px;
|
.buttons {
|
||||||
}
|
display: flex;
|
||||||
.buttons {
|
flex-wrap: wrap;
|
||||||
display: flex;
|
gap: 8px;
|
||||||
flex-wrap: wrap;
|
}
|
||||||
gap: 8px;
|
`;
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,29 +1,31 @@
|
|||||||
import { consume } from "@lit-labs/context";
|
import { consume } from "@lit-labs/context";
|
||||||
import type { SortableEvent } from "sortablejs";
|
import type { ActionDetail } from "@material/mwc-list";
|
||||||
import {
|
import {
|
||||||
mdiDotsVertical,
|
mdiArrowDown,
|
||||||
mdiRenameBox,
|
mdiArrowUp,
|
||||||
mdiSort,
|
|
||||||
mdiContentDuplicate,
|
mdiContentDuplicate,
|
||||||
mdiDelete,
|
mdiDelete,
|
||||||
mdiPlus,
|
mdiDotsVertical,
|
||||||
mdiArrowUp,
|
|
||||||
mdiArrowDown,
|
|
||||||
mdiDrag,
|
mdiDrag,
|
||||||
|
mdiPlus,
|
||||||
|
mdiRenameBox,
|
||||||
|
mdiSort,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import deepClone from "deep-clone-simple";
|
import deepClone from "deep-clone-simple";
|
||||||
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
|
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import type { ActionDetail } from "@material/mwc-list";
|
|
||||||
import type { SortableInstance } from "../../../../../resources/sortable";
|
|
||||||
import { ensureArray } from "../../../../../common/array/ensure-array";
|
import { ensureArray } from "../../../../../common/array/ensure-array";
|
||||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||||
import { capitalizeFirstLetter } from "../../../../../common/string/capitalize-first-letter";
|
import { capitalizeFirstLetter } from "../../../../../common/string/capitalize-first-letter";
|
||||||
import "../../../../../components/ha-button";
|
import "../../../../../components/ha-button";
|
||||||
import "../../../../../components/ha-icon-button";
|
|
||||||
import "../../../../../components/ha-button-menu";
|
import "../../../../../components/ha-button-menu";
|
||||||
|
import "../../../../../components/ha-icon-button";
|
||||||
|
import "../../../../../components/ha-sortable";
|
||||||
import { Condition } from "../../../../../data/automation";
|
import { Condition } from "../../../../../data/automation";
|
||||||
|
import { describeCondition } from "../../../../../data/automation_i18n";
|
||||||
|
import { fullEntitiesContext } from "../../../../../data/context";
|
||||||
|
import { EntityRegistryEntry } from "../../../../../data/entity_registry";
|
||||||
import {
|
import {
|
||||||
Action,
|
Action,
|
||||||
ChooseAction,
|
ChooseAction,
|
||||||
@ -36,10 +38,6 @@ import {
|
|||||||
import { haStyle } from "../../../../../resources/styles";
|
import { haStyle } from "../../../../../resources/styles";
|
||||||
import { HomeAssistant } from "../../../../../types";
|
import { HomeAssistant } from "../../../../../types";
|
||||||
import { ActionElement } from "../ha-automation-action-row";
|
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";
|
|
||||||
|
|
||||||
const preventDefault = (ev) => ev.preventDefault();
|
const preventDefault = (ev) => ev.preventDefault();
|
||||||
|
|
||||||
@ -63,8 +61,6 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
|||||||
|
|
||||||
private _expandLast = false;
|
private _expandLast = false;
|
||||||
|
|
||||||
private _sortable?: SortableInstance;
|
|
||||||
|
|
||||||
public static get defaultConfig() {
|
public static get defaultConfig() {
|
||||||
return { choose: [{ conditions: [], sequence: [] }] };
|
return { choose: [{ conditions: [], sequence: [] }] };
|
||||||
}
|
}
|
||||||
@ -100,157 +96,166 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
|||||||
const action = this.action;
|
const action = this.action;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="options">
|
<ha-sortable
|
||||||
${repeat(
|
handle-selector=".handle"
|
||||||
action.choose ? ensureArray(action.choose) : [],
|
.disabled=${!this.reOrderMode}
|
||||||
(option) => option,
|
@item-moved=${this._optionMoved}
|
||||||
(option, idx) =>
|
>
|
||||||
html`<ha-card>
|
<div class="options">
|
||||||
<ha-expansion-panel
|
${repeat(
|
||||||
.index=${idx}
|
action.choose ? ensureArray(action.choose) : [],
|
||||||
leftChevron
|
(option) => option,
|
||||||
@expanded-changed=${this._expandedChanged}
|
(option, idx) => html`
|
||||||
>
|
<div class="option">
|
||||||
<h3 slot="header">
|
<ha-card>
|
||||||
${this.hass.localize(
|
<ha-expansion-panel
|
||||||
"ui.panel.config.automation.editor.actions.type.choose.option",
|
.index=${idx}
|
||||||
{ number: idx + 1 }
|
leftChevron
|
||||||
)}:
|
@expanded-changed=${this._expandedChanged}
|
||||||
${option.alias ||
|
>
|
||||||
(this._expandedStates[idx]
|
<h3 slot="header">
|
||||||
? ""
|
${this.hass.localize(
|
||||||
: this._getDescription(option))}
|
"ui.panel.config.automation.editor.actions.type.choose.option",
|
||||||
</h3>
|
{ number: idx + 1 }
|
||||||
${this.reOrderMode
|
)}:
|
||||||
? html`
|
${option.alias ||
|
||||||
<ha-icon-button
|
(this._expandedStates[idx]
|
||||||
.index=${idx}
|
? ""
|
||||||
slot="icons"
|
: this._getDescription(option))}
|
||||||
.label=${this.hass.localize(
|
</h3>
|
||||||
"ui.panel.config.automation.editor.move_up"
|
${this.reOrderMode
|
||||||
|
? html`
|
||||||
|
<ha-icon-button
|
||||||
|
.index=${idx}
|
||||||
|
slot="icons"
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.automation.editor.move_up"
|
||||||
|
)}
|
||||||
|
.path=${mdiArrowUp}
|
||||||
|
@click=${this._moveUp}
|
||||||
|
.disabled=${idx === 0}
|
||||||
|
></ha-icon-button>
|
||||||
|
<ha-icon-button
|
||||||
|
.index=${idx}
|
||||||
|
slot="icons"
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.automation.editor.move_down"
|
||||||
|
)}
|
||||||
|
.path=${mdiArrowDown}
|
||||||
|
@click=${this._moveDown}
|
||||||
|
.disabled=${idx ===
|
||||||
|
ensureArray(this.action.choose).length - 1}
|
||||||
|
></ha-icon-button>
|
||||||
|
<div class="handle" slot="icons">
|
||||||
|
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<ha-button-menu
|
||||||
|
slot="icons"
|
||||||
|
.idx=${idx}
|
||||||
|
@action=${this._handleAction}
|
||||||
|
@click=${preventDefault}
|
||||||
|
fixed
|
||||||
|
>
|
||||||
|
<ha-icon-button
|
||||||
|
slot="trigger"
|
||||||
|
.label=${this.hass.localize("ui.common.menu")}
|
||||||
|
.path=${mdiDotsVertical}
|
||||||
|
></ha-icon-button>
|
||||||
|
<mwc-list-item
|
||||||
|
graphic="icon"
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.automation.editor.actions.rename"
|
||||||
|
)}
|
||||||
|
<ha-svg-icon
|
||||||
|
slot="graphic"
|
||||||
|
.path=${mdiRenameBox}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</mwc-list-item>
|
||||||
|
<mwc-list-item
|
||||||
|
graphic="icon"
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.automation.editor.actions.re_order"
|
||||||
|
)}
|
||||||
|
<ha-svg-icon
|
||||||
|
slot="graphic"
|
||||||
|
.path=${mdiSort}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</mwc-list-item>
|
||||||
|
|
||||||
|
<mwc-list-item
|
||||||
|
graphic="icon"
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.automation.editor.actions.duplicate"
|
||||||
|
)}
|
||||||
|
<ha-svg-icon
|
||||||
|
slot="graphic"
|
||||||
|
.path=${mdiContentDuplicate}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</mwc-list-item>
|
||||||
|
|
||||||
|
<mwc-list-item
|
||||||
|
class="warning"
|
||||||
|
graphic="icon"
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
|
||||||
|
)}
|
||||||
|
<ha-svg-icon
|
||||||
|
class="warning"
|
||||||
|
slot="graphic"
|
||||||
|
.path=${mdiDelete}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</mwc-list-item>
|
||||||
|
</ha-button-menu>
|
||||||
|
`}
|
||||||
|
<div class="card-content">
|
||||||
|
<h4>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.automation.editor.actions.type.choose.conditions"
|
||||||
|
)}:
|
||||||
|
</h4>
|
||||||
|
<ha-automation-condition
|
||||||
|
nested
|
||||||
|
.conditions=${ensureArray<string | Condition>(
|
||||||
|
option.conditions
|
||||||
)}
|
)}
|
||||||
.path=${mdiArrowUp}
|
.reOrderMode=${this.reOrderMode}
|
||||||
@click=${this._moveUp}
|
.disabled=${this.disabled}
|
||||||
.disabled=${idx === 0}
|
.hass=${this.hass}
|
||||||
></ha-icon-button>
|
|
||||||
<ha-icon-button
|
|
||||||
.index=${idx}
|
|
||||||
slot="icons"
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.panel.config.automation.editor.move_down"
|
|
||||||
)}
|
|
||||||
.path=${mdiArrowDown}
|
|
||||||
@click=${this._moveDown}
|
|
||||||
.disabled=${idx ===
|
|
||||||
ensureArray(this.action.choose).length - 1}
|
|
||||||
></ha-icon-button>
|
|
||||||
<div class="handle" slot="icons">
|
|
||||||
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<ha-button-menu
|
|
||||||
slot="icons"
|
|
||||||
.idx=${idx}
|
.idx=${idx}
|
||||||
@action=${this._handleAction}
|
@value-changed=${this._conditionChanged}
|
||||||
@click=${preventDefault}
|
></ha-automation-condition>
|
||||||
fixed
|
<h4>
|
||||||
>
|
${this.hass.localize(
|
||||||
<ha-icon-button
|
"ui.panel.config.automation.editor.actions.type.choose.sequence"
|
||||||
slot="trigger"
|
)}:
|
||||||
.label=${this.hass.localize("ui.common.menu")}
|
</h4>
|
||||||
.path=${mdiDotsVertical}
|
<ha-automation-action
|
||||||
></ha-icon-button>
|
nested
|
||||||
<mwc-list-item
|
.actions=${ensureArray(option.sequence) || []}
|
||||||
graphic="icon"
|
.reOrderMode=${this.reOrderMode}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
>
|
.hass=${this.hass}
|
||||||
${this.hass.localize(
|
.idx=${idx}
|
||||||
"ui.panel.config.automation.editor.actions.rename"
|
@value-changed=${this._actionChanged}
|
||||||
)}
|
></ha-automation-action>
|
||||||
<ha-svg-icon
|
</div>
|
||||||
slot="graphic"
|
</ha-expansion-panel>
|
||||||
.path=${mdiRenameBox}
|
</ha-card>
|
||||||
></ha-svg-icon>
|
</div>
|
||||||
</mwc-list-item>
|
`
|
||||||
<mwc-list-item
|
)}
|
||||||
graphic="icon"
|
</div>
|
||||||
.disabled=${this.disabled}
|
</ha-sortable>
|
||||||
>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.automation.editor.actions.re_order"
|
|
||||||
)}
|
|
||||||
<ha-svg-icon
|
|
||||||
slot="graphic"
|
|
||||||
.path=${mdiSort}
|
|
||||||
></ha-svg-icon>
|
|
||||||
</mwc-list-item>
|
|
||||||
|
|
||||||
<mwc-list-item
|
|
||||||
graphic="icon"
|
|
||||||
.disabled=${this.disabled}
|
|
||||||
>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.automation.editor.actions.duplicate"
|
|
||||||
)}
|
|
||||||
<ha-svg-icon
|
|
||||||
slot="graphic"
|
|
||||||
.path=${mdiContentDuplicate}
|
|
||||||
></ha-svg-icon>
|
|
||||||
</mwc-list-item>
|
|
||||||
|
|
||||||
<mwc-list-item
|
|
||||||
class="warning"
|
|
||||||
graphic="icon"
|
|
||||||
.disabled=${this.disabled}
|
|
||||||
>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
|
|
||||||
)}
|
|
||||||
<ha-svg-icon
|
|
||||||
class="warning"
|
|
||||||
slot="graphic"
|
|
||||||
.path=${mdiDelete}
|
|
||||||
></ha-svg-icon>
|
|
||||||
</mwc-list-item>
|
|
||||||
</ha-button-menu>
|
|
||||||
`}
|
|
||||||
<div class="card-content">
|
|
||||||
<h4>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.automation.editor.actions.type.choose.conditions"
|
|
||||||
)}:
|
|
||||||
</h4>
|
|
||||||
<ha-automation-condition
|
|
||||||
nested
|
|
||||||
.conditions=${ensureArray<string | Condition>(
|
|
||||||
option.conditions
|
|
||||||
)}
|
|
||||||
.reOrderMode=${this.reOrderMode}
|
|
||||||
.disabled=${this.disabled}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.idx=${idx}
|
|
||||||
@value-changed=${this._conditionChanged}
|
|
||||||
></ha-automation-condition>
|
|
||||||
<h4>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.automation.editor.actions.type.choose.sequence"
|
|
||||||
)}:
|
|
||||||
</h4>
|
|
||||||
<ha-automation-action
|
|
||||||
nested
|
|
||||||
.actions=${ensureArray(option.sequence) || []}
|
|
||||||
.reOrderMode=${this.reOrderMode}
|
|
||||||
.disabled=${this.disabled}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.idx=${idx}
|
|
||||||
@value-changed=${this._actionChanged}
|
|
||||||
></ha-automation-action>
|
|
||||||
</div>
|
|
||||||
</ha-expansion-panel>
|
|
||||||
</ha-card>`
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<ha-button
|
<ha-button
|
||||||
outlined
|
outlined
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
@ -352,14 +357,6 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
|||||||
protected updated(changedProps: PropertyValues) {
|
protected updated(changedProps: PropertyValues) {
|
||||||
super.updated(changedProps);
|
super.updated(changedProps);
|
||||||
|
|
||||||
if (changedProps.has("reOrderMode")) {
|
|
||||||
if (this.reOrderMode) {
|
|
||||||
this._createSortable();
|
|
||||||
} else {
|
|
||||||
this._destroySortable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._expandLast) {
|
if (this._expandLast) {
|
||||||
const nodes = this.shadowRoot!.querySelectorAll("ha-expansion-panel");
|
const nodes = this.shadowRoot!.querySelectorAll("ha-expansion-panel");
|
||||||
nodes[nodes.length - 1].expanded = true;
|
nodes[nodes.length - 1].expanded = true;
|
||||||
@ -425,11 +422,6 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
|||||||
this._move(index, newIndex);
|
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) {
|
private _move(index: number, newIndex: number) {
|
||||||
const options = ensureArray(this.action.choose)!.concat();
|
const options = ensureArray(this.action.choose)!.concat();
|
||||||
const item = options.splice(index, 1)[0];
|
const item = options.splice(index, 1)[0];
|
||||||
@ -443,6 +435,12 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _optionMoved(ev: CustomEvent): void {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const { oldIndex, newIndex } = ev.detail;
|
||||||
|
this._move(oldIndex, newIndex);
|
||||||
|
}
|
||||||
|
|
||||||
private _removeOption(ev: CustomEvent) {
|
private _removeOption(ev: CustomEvent) {
|
||||||
const index = (ev.target as any).idx;
|
const index = (ev.target as any).idx;
|
||||||
showConfirmationDialog(this, {
|
showConfirmationDialog(this, {
|
||||||
@ -479,40 +477,11 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _createSortable() {
|
|
||||||
const Sortable = (await import("../../../../../resources/sortable"))
|
|
||||||
.default;
|
|
||||||
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 {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
sortableStyles,
|
|
||||||
css`
|
css`
|
||||||
ha-card {
|
.option {
|
||||||
margin: 0 0 16px 0;
|
margin: 0 0 16px 0;
|
||||||
}
|
}
|
||||||
.add-card mwc-button {
|
.add-card mwc-button {
|
||||||
@ -543,9 +512,9 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
|||||||
padding: 0 16px 16px 16px;
|
padding: 0 16px 16px 16px;
|
||||||
}
|
}
|
||||||
.handle {
|
.handle {
|
||||||
|
padding: 12px;
|
||||||
cursor: move; /* fallback if grab cursor is unsupported */
|
cursor: move; /* fallback if grab cursor is unsupported */
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
padding: 12px;
|
|
||||||
}
|
}
|
||||||
.handle ha-svg-icon {
|
.handle ha-svg-icon {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import "@material/mwc-button";
|
|
||||||
import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
|
import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
|
||||||
import deepClone from "deep-clone-simple";
|
import deepClone from "deep-clone-simple";
|
||||||
import {
|
import {
|
||||||
@ -11,18 +10,16 @@ import {
|
|||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import type { SortableEvent } from "sortablejs";
|
|
||||||
import { storage } from "../../../../common/decorators/storage";
|
import { storage } from "../../../../common/decorators/storage";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import "../../../../components/ha-button";
|
import "../../../../components/ha-button";
|
||||||
import "../../../../components/ha-button-menu";
|
import "../../../../components/ha-button-menu";
|
||||||
|
import "../../../../components/ha-sortable";
|
||||||
import "../../../../components/ha-svg-icon";
|
import "../../../../components/ha-svg-icon";
|
||||||
import type {
|
import type {
|
||||||
AutomationClipboard,
|
AutomationClipboard,
|
||||||
Condition,
|
Condition,
|
||||||
} from "../../../../data/automation";
|
} from "../../../../data/automation";
|
||||||
import { sortableStyles } from "../../../../resources/ha-sortable-style";
|
|
||||||
import type { SortableInstance } from "../../../../resources/sortable";
|
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import {
|
import {
|
||||||
PASTE_VALUE,
|
PASTE_VALUE,
|
||||||
@ -55,17 +52,7 @@ export default class HaAutomationCondition extends LitElement {
|
|||||||
|
|
||||||
private _conditionKeys = new WeakMap<Condition, string>();
|
private _conditionKeys = new WeakMap<Condition, string>();
|
||||||
|
|
||||||
private _sortable?: SortableInstance;
|
|
||||||
|
|
||||||
protected updated(changedProperties: PropertyValues) {
|
protected updated(changedProperties: PropertyValues) {
|
||||||
if (changedProperties.has("reOrderMode")) {
|
|
||||||
if (this.reOrderMode) {
|
|
||||||
this._createSortable();
|
|
||||||
} else {
|
|
||||||
this._destroySortable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!changedProperties.has("conditions")) {
|
if (!changedProperties.has("conditions")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -118,63 +105,70 @@ export default class HaAutomationCondition extends LitElement {
|
|||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.automation.editor.re_order_mode.description_conditions"
|
"ui.panel.config.automation.editor.re_order_mode.description_conditions"
|
||||||
)}
|
)}
|
||||||
<mwc-button slot="action" @click=${this._exitReOrderMode}>
|
<ha-button slot="action" @click=${this._exitReOrderMode}>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.automation.editor.re_order_mode.exit"
|
"ui.panel.config.automation.editor.re_order_mode.exit"
|
||||||
)}
|
)}
|
||||||
</mwc-button>
|
</ha-button>
|
||||||
</ha-alert>
|
</ha-alert>
|
||||||
`
|
`
|
||||||
: null}
|
: null}
|
||||||
<div class="conditions">
|
|
||||||
${repeat(
|
<ha-sortable
|
||||||
this.conditions.filter((c) => typeof c === "object"),
|
handle-selector=".handle"
|
||||||
(condition) => this._getKey(condition),
|
.disabled=${!this.reOrderMode}
|
||||||
(cond, idx) => html`
|
@item-moved=${this._conditionMoved}
|
||||||
<ha-automation-condition-row
|
>
|
||||||
.index=${idx}
|
<div class="conditions">
|
||||||
.totalConditions=${this.conditions.length}
|
${repeat(
|
||||||
.condition=${cond}
|
this.conditions.filter((c) => typeof c === "object"),
|
||||||
.hideMenu=${this.reOrderMode}
|
(condition) => this._getKey(condition),
|
||||||
.reOrderMode=${this.reOrderMode}
|
(cond, idx) => html`
|
||||||
.disabled=${this.disabled}
|
<ha-automation-condition-row
|
||||||
@duplicate=${this._duplicateCondition}
|
.index=${idx}
|
||||||
@move-condition=${this._move}
|
.totalConditions=${this.conditions.length}
|
||||||
@value-changed=${this._conditionChanged}
|
.condition=${cond}
|
||||||
@re-order=${this._enterReOrderMode}
|
.hideMenu=${this.reOrderMode}
|
||||||
.hass=${this.hass}
|
.reOrderMode=${this.reOrderMode}
|
||||||
>
|
.disabled=${this.disabled}
|
||||||
${this.reOrderMode
|
@duplicate=${this._duplicateCondition}
|
||||||
? html`
|
@move-condition=${this._move}
|
||||||
<ha-icon-button
|
@value-changed=${this._conditionChanged}
|
||||||
.index=${idx}
|
@re-order=${this._enterReOrderMode}
|
||||||
slot="icons"
|
.hass=${this.hass}
|
||||||
.label=${this.hass.localize(
|
>
|
||||||
"ui.panel.config.automation.editor.move_up"
|
${this.reOrderMode
|
||||||
)}
|
? html`
|
||||||
.path=${mdiArrowUp}
|
<ha-icon-button
|
||||||
@click=${this._moveUp}
|
.index=${idx}
|
||||||
.disabled=${idx === 0}
|
slot="icons"
|
||||||
></ha-icon-button>
|
.label=${this.hass.localize(
|
||||||
<ha-icon-button
|
"ui.panel.config.automation.editor.move_up"
|
||||||
.index=${idx}
|
)}
|
||||||
slot="icons"
|
.path=${mdiArrowUp}
|
||||||
.label=${this.hass.localize(
|
@click=${this._moveUp}
|
||||||
"ui.panel.config.automation.editor.move_down"
|
.disabled=${idx === 0}
|
||||||
)}
|
></ha-icon-button>
|
||||||
.path=${mdiArrowDown}
|
<ha-icon-button
|
||||||
@click=${this._moveDown}
|
.index=${idx}
|
||||||
.disabled=${idx === this.conditions.length - 1}
|
slot="icons"
|
||||||
></ha-icon-button>
|
.label=${this.hass.localize(
|
||||||
<div class="handle" slot="icons">
|
"ui.panel.config.automation.editor.move_down"
|
||||||
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
)}
|
||||||
</div>
|
.path=${mdiArrowDown}
|
||||||
`
|
@click=${this._moveDown}
|
||||||
: ""}
|
.disabled=${idx === this.conditions.length - 1}
|
||||||
</ha-automation-condition-row>
|
></ha-icon-button>
|
||||||
`
|
<div class="handle" slot="icons">
|
||||||
)}
|
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
||||||
</div>
|
</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</ha-automation-condition-row>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ha-sortable>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<ha-button
|
<ha-button
|
||||||
outlined
|
outlined
|
||||||
@ -248,36 +242,6 @@ export default class HaAutomationCondition extends LitElement {
|
|||||||
this.reOrderMode = false;
|
this.reOrderMode = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _createSortable() {
|
|
||||||
const Sortable = (await import("../../../../resources/sortable")).default;
|
|
||||||
this._sortable = new Sortable(
|
|
||||||
this.shadowRoot!.querySelector(".conditions")!,
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getKey(condition: Condition) {
|
private _getKey(condition: Condition) {
|
||||||
if (!this._conditionKeys.has(condition)) {
|
if (!this._conditionKeys.has(condition)) {
|
||||||
this._conditionKeys.set(condition, Math.random().toString());
|
this._conditionKeys.set(condition, Math.random().toString());
|
||||||
@ -298,11 +262,6 @@ export default class HaAutomationCondition extends LitElement {
|
|||||||
this._move(index, newIndex);
|
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) {
|
private _move(index: number, newIndex: number) {
|
||||||
const conditions = this.conditions.concat();
|
const conditions = this.conditions.concat();
|
||||||
const condition = conditions.splice(index, 1)[0];
|
const condition = conditions.splice(index, 1)[0];
|
||||||
@ -310,6 +269,12 @@ export default class HaAutomationCondition extends LitElement {
|
|||||||
fireEvent(this, "value-changed", { value: conditions });
|
fireEvent(this, "value-changed", { value: conditions });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _conditionMoved(ev: CustomEvent): void {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const { oldIndex, newIndex } = ev.detail;
|
||||||
|
this._move(oldIndex, newIndex);
|
||||||
|
}
|
||||||
|
|
||||||
private _conditionChanged(ev: CustomEvent) {
|
private _conditionChanged(ev: CustomEvent) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const conditions = [...this.conditions];
|
const conditions = [...this.conditions];
|
||||||
@ -340,39 +305,36 @@ export default class HaAutomationCondition extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return css`
|
||||||
sortableStyles,
|
ha-automation-condition-row {
|
||||||
css`
|
display: block;
|
||||||
ha-automation-condition-row {
|
margin-bottom: 16px;
|
||||||
display: block;
|
scroll-margin-top: 48px;
|
||||||
margin-bottom: 16px;
|
}
|
||||||
scroll-margin-top: 48px;
|
ha-svg-icon {
|
||||||
}
|
height: 20px;
|
||||||
ha-svg-icon {
|
}
|
||||||
height: 20px;
|
ha-alert {
|
||||||
}
|
display: block;
|
||||||
ha-alert {
|
margin-bottom: 16px;
|
||||||
display: block;
|
border-radius: var(--ha-card-border-radius, 12px);
|
||||||
margin-bottom: 16px;
|
overflow: hidden;
|
||||||
border-radius: var(--ha-card-border-radius, 12px);
|
}
|
||||||
overflow: hidden;
|
.handle {
|
||||||
}
|
padding: 12px;
|
||||||
.handle {
|
cursor: move; /* fallback if grab cursor is unsupported */
|
||||||
cursor: move; /* fallback if grab cursor is unsupported */
|
cursor: grab;
|
||||||
cursor: grab;
|
}
|
||||||
padding: 12px;
|
.handle ha-svg-icon {
|
||||||
}
|
pointer-events: none;
|
||||||
.handle ha-svg-icon {
|
height: 24px;
|
||||||
pointer-events: none;
|
}
|
||||||
height: 24px;
|
.buttons {
|
||||||
}
|
display: flex;
|
||||||
.buttons {
|
flex-wrap: wrap;
|
||||||
display: flex;
|
gap: 8px;
|
||||||
flex-wrap: wrap;
|
}
|
||||||
gap: 8px;
|
`;
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,25 +1,22 @@
|
|||||||
import "@material/mwc-button";
|
|
||||||
import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
|
import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
|
||||||
import deepClone from "deep-clone-simple";
|
import deepClone from "deep-clone-simple";
|
||||||
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
|
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import type { SortableEvent } from "sortablejs";
|
|
||||||
import { storage } from "../../../../common/decorators/storage";
|
import { storage } from "../../../../common/decorators/storage";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import "../../../../components/ha-button";
|
import "../../../../components/ha-button";
|
||||||
import "../../../../components/ha-button-menu";
|
import "../../../../components/ha-button-menu";
|
||||||
|
import "../../../../components/ha-sortable";
|
||||||
import "../../../../components/ha-svg-icon";
|
import "../../../../components/ha-svg-icon";
|
||||||
import { AutomationClipboard, Trigger } from "../../../../data/automation";
|
import { AutomationClipboard, Trigger } from "../../../../data/automation";
|
||||||
import { sortableStyles } from "../../../../resources/ha-sortable-style";
|
|
||||||
import type { SortableInstance } from "../../../../resources/sortable";
|
|
||||||
import { HomeAssistant } from "../../../../types";
|
import { HomeAssistant } from "../../../../types";
|
||||||
import "./ha-automation-trigger-row";
|
|
||||||
import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
|
|
||||||
import {
|
import {
|
||||||
PASTE_VALUE,
|
PASTE_VALUE,
|
||||||
showAddAutomationElementDialog,
|
showAddAutomationElementDialog,
|
||||||
} from "../show-add-automation-element-dialog";
|
} from "../show-add-automation-element-dialog";
|
||||||
|
import "./ha-automation-trigger-row";
|
||||||
|
import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
|
||||||
|
|
||||||
@customElement("ha-automation-trigger")
|
@customElement("ha-automation-trigger")
|
||||||
export default class HaAutomationTrigger extends LitElement {
|
export default class HaAutomationTrigger extends LitElement {
|
||||||
@ -45,8 +42,6 @@ export default class HaAutomationTrigger extends LitElement {
|
|||||||
|
|
||||||
private _triggerKeys = new WeakMap<Trigger, string>();
|
private _triggerKeys = new WeakMap<Trigger, string>();
|
||||||
|
|
||||||
private _sortable?: SortableInstance;
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
${this.reOrderMode && !this.nested
|
${this.reOrderMode && !this.nested
|
||||||
@ -60,70 +55,76 @@ export default class HaAutomationTrigger extends LitElement {
|
|||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.automation.editor.re_order_mode.description_triggers"
|
"ui.panel.config.automation.editor.re_order_mode.description_triggers"
|
||||||
)}
|
)}
|
||||||
<mwc-button slot="action" @click=${this._exitReOrderMode}>
|
<ha-button slot="action" @click=${this._exitReOrderMode}>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.automation.editor.re_order_mode.exit"
|
"ui.panel.config.automation.editor.re_order_mode.exit"
|
||||||
)}
|
)}
|
||||||
</mwc-button>
|
</ha-button>
|
||||||
</ha-alert>
|
</ha-alert>
|
||||||
`
|
`
|
||||||
: null}
|
: null}
|
||||||
<div class="triggers">
|
<ha-sortable
|
||||||
${repeat(
|
handle-selector=".handle"
|
||||||
this.triggers,
|
.disabled=${!this.reOrderMode}
|
||||||
(trigger) => this._getKey(trigger),
|
@item-moved=${this._triggerMoved}
|
||||||
(trg, idx) => html`
|
>
|
||||||
<ha-automation-trigger-row
|
<div class="triggers">
|
||||||
.index=${idx}
|
${repeat(
|
||||||
.trigger=${trg}
|
this.triggers,
|
||||||
.hideMenu=${this.reOrderMode}
|
(trigger) => this._getKey(trigger),
|
||||||
@duplicate=${this._duplicateTrigger}
|
(trg, idx) => html`
|
||||||
@value-changed=${this._triggerChanged}
|
<ha-automation-trigger-row
|
||||||
.hass=${this.hass}
|
.index=${idx}
|
||||||
.disabled=${this.disabled}
|
.trigger=${trg}
|
||||||
@re-order=${this._enterReOrderMode}
|
.hideMenu=${this.reOrderMode}
|
||||||
>
|
@duplicate=${this._duplicateTrigger}
|
||||||
${this.reOrderMode
|
@value-changed=${this._triggerChanged}
|
||||||
? html`
|
.hass=${this.hass}
|
||||||
<ha-icon-button
|
.disabled=${this.disabled}
|
||||||
.index=${idx}
|
@re-order=${this._enterReOrderMode}
|
||||||
slot="icons"
|
>
|
||||||
.label=${this.hass.localize(
|
${this.reOrderMode
|
||||||
"ui.panel.config.automation.editor.move_up"
|
? html`
|
||||||
)}
|
<ha-icon-button
|
||||||
.path=${mdiArrowUp}
|
.index=${idx}
|
||||||
@click=${this._moveUp}
|
slot="icons"
|
||||||
.disabled=${idx === 0}
|
.label=${this.hass.localize(
|
||||||
></ha-icon-button>
|
"ui.panel.config.automation.editor.move_up"
|
||||||
<ha-icon-button
|
)}
|
||||||
.index=${idx}
|
.path=${mdiArrowUp}
|
||||||
slot="icons"
|
@click=${this._moveUp}
|
||||||
.label=${this.hass.localize(
|
.disabled=${idx === 0}
|
||||||
"ui.panel.config.automation.editor.move_down"
|
></ha-icon-button>
|
||||||
)}
|
<ha-icon-button
|
||||||
.path=${mdiArrowDown}
|
.index=${idx}
|
||||||
@click=${this._moveDown}
|
slot="icons"
|
||||||
.disabled=${idx === this.triggers.length - 1}
|
.label=${this.hass.localize(
|
||||||
></ha-icon-button>
|
"ui.panel.config.automation.editor.move_down"
|
||||||
<div class="handle" slot="icons">
|
)}
|
||||||
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
.path=${mdiArrowDown}
|
||||||
</div>
|
@click=${this._moveDown}
|
||||||
`
|
.disabled=${idx === this.triggers.length - 1}
|
||||||
: ""}
|
></ha-icon-button>
|
||||||
</ha-automation-trigger-row>
|
<div class="handle" slot="icons">
|
||||||
`
|
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
||||||
)}
|
</div>
|
||||||
<ha-button
|
`
|
||||||
outlined
|
: ""}
|
||||||
.label=${this.hass.localize(
|
</ha-automation-trigger-row>
|
||||||
"ui.panel.config.automation.editor.triggers.add"
|
`
|
||||||
)}
|
)}
|
||||||
.disabled=${this.disabled}
|
</div>
|
||||||
@click=${this._addTriggerDialog}
|
</ha-sortable>
|
||||||
>
|
<ha-button
|
||||||
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
|
outlined
|
||||||
</ha-button>
|
.label=${this.hass.localize(
|
||||||
</div>
|
"ui.panel.config.automation.editor.triggers.add"
|
||||||
|
)}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
@click=${this._addTriggerDialog}
|
||||||
|
>
|
||||||
|
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
|
||||||
|
</ha-button>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,14 +159,6 @@ export default class HaAutomationTrigger extends LitElement {
|
|||||||
protected updated(changedProps: PropertyValues) {
|
protected updated(changedProps: PropertyValues) {
|
||||||
super.updated(changedProps);
|
super.updated(changedProps);
|
||||||
|
|
||||||
if (changedProps.has("reOrderMode")) {
|
|
||||||
if (this.reOrderMode) {
|
|
||||||
this._createSortable();
|
|
||||||
} else {
|
|
||||||
this._destroySortable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changedProps.has("triggers") && this._focusLastTriggerOnChange) {
|
if (changedProps.has("triggers") && this._focusLastTriggerOnChange) {
|
||||||
this._focusLastTriggerOnChange = false;
|
this._focusLastTriggerOnChange = false;
|
||||||
|
|
||||||
@ -190,36 +183,6 @@ export default class HaAutomationTrigger extends LitElement {
|
|||||||
this.reOrderMode = false;
|
this.reOrderMode = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _createSortable() {
|
|
||||||
const Sortable = (await import("../../../../resources/sortable")).default;
|
|
||||||
this._sortable = new Sortable(
|
|
||||||
this.shadowRoot!.querySelector(".triggers")!,
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getKey(action: Trigger) {
|
private _getKey(action: Trigger) {
|
||||||
if (!this._triggerKeys.has(action)) {
|
if (!this._triggerKeys.has(action)) {
|
||||||
this._triggerKeys.set(action, Math.random().toString());
|
this._triggerKeys.set(action, Math.random().toString());
|
||||||
@ -240,11 +203,6 @@ export default class HaAutomationTrigger extends LitElement {
|
|||||||
this._move(index, newIndex);
|
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) {
|
private _move(index: number, newIndex: number) {
|
||||||
const triggers = this.triggers.concat();
|
const triggers = this.triggers.concat();
|
||||||
const trigger = triggers.splice(index, 1)[0];
|
const trigger = triggers.splice(index, 1)[0];
|
||||||
@ -252,6 +210,12 @@ export default class HaAutomationTrigger extends LitElement {
|
|||||||
fireEvent(this, "value-changed", { value: triggers });
|
fireEvent(this, "value-changed", { value: triggers });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _triggerMoved(ev: CustomEvent): void {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const { oldIndex, newIndex } = ev.detail;
|
||||||
|
this._move(oldIndex, newIndex);
|
||||||
|
}
|
||||||
|
|
||||||
private _triggerChanged(ev: CustomEvent) {
|
private _triggerChanged(ev: CustomEvent) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const triggers = [...this.triggers];
|
const triggers = [...this.triggers];
|
||||||
@ -280,34 +244,31 @@ export default class HaAutomationTrigger extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return css`
|
||||||
sortableStyles,
|
ha-automation-trigger-row {
|
||||||
css`
|
display: block;
|
||||||
ha-automation-trigger-row {
|
margin-bottom: 16px;
|
||||||
display: block;
|
scroll-margin-top: 48px;
|
||||||
margin-bottom: 16px;
|
}
|
||||||
scroll-margin-top: 48px;
|
ha-svg-icon {
|
||||||
}
|
height: 20px;
|
||||||
ha-svg-icon {
|
}
|
||||||
height: 20px;
|
ha-alert {
|
||||||
}
|
display: block;
|
||||||
ha-alert {
|
margin-bottom: 16px;
|
||||||
display: block;
|
border-radius: var(--ha-card-border-radius, 16px);
|
||||||
margin-bottom: 16px;
|
overflow: hidden;
|
||||||
border-radius: var(--ha-card-border-radius, 16px);
|
}
|
||||||
overflow: hidden;
|
.handle {
|
||||||
}
|
padding: 12px;
|
||||||
.handle {
|
cursor: move; /* fallback if grab cursor is unsupported */
|
||||||
cursor: move; /* fallback if grab cursor is unsupported */
|
cursor: grab;
|
||||||
cursor: grab;
|
}
|
||||||
padding: 12px;
|
.handle ha-svg-icon {
|
||||||
}
|
pointer-events: none;
|
||||||
.handle ha-svg-icon {
|
height: 24px;
|
||||||
pointer-events: none;
|
}
|
||||||
height: 24px;
|
`;
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,22 +1,20 @@
|
|||||||
import "@material/mwc-list/mwc-list";
|
import "@material/mwc-list/mwc-list";
|
||||||
import { mdiDelete, mdiDrag } from "@mdi/js";
|
import { mdiDelete, mdiDrag } from "@mdi/js";
|
||||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import type { SortableEvent } from "sortablejs";
|
|
||||||
import { sortableStyles } from "../../../../resources/ha-sortable-style";
|
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import "../../../../components/ha-button";
|
import "../../../../components/ha-button";
|
||||||
import "../../../../components/ha-icon-button";
|
import "../../../../components/ha-icon-button";
|
||||||
import "../../../../components/ha-list-item";
|
|
||||||
import "../../../../components/ha-icon-picker";
|
import "../../../../components/ha-icon-picker";
|
||||||
|
import "../../../../components/ha-list-item";
|
||||||
|
import "../../../../components/ha-sortable";
|
||||||
import "../../../../components/ha-textfield";
|
import "../../../../components/ha-textfield";
|
||||||
import type { HaTextField } from "../../../../components/ha-textfield";
|
import type { HaTextField } from "../../../../components/ha-textfield";
|
||||||
import type { InputSelect } from "../../../../data/input_select";
|
import type { InputSelect } from "../../../../data/input_select";
|
||||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||||
import { haStyle } from "../../../../resources/styles";
|
import { haStyle } from "../../../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import type { SortableInstance } from "../../../../resources/sortable";
|
|
||||||
|
|
||||||
@customElement("ha-input_select-form")
|
@customElement("ha-input_select-form")
|
||||||
class HaInputSelectForm extends LitElement {
|
class HaInputSelectForm extends LitElement {
|
||||||
@ -32,59 +30,20 @@ class HaInputSelectForm extends LitElement {
|
|||||||
|
|
||||||
@state() private _options: string[] = [];
|
@state() private _options: string[] = [];
|
||||||
|
|
||||||
private _sortable?: SortableInstance;
|
|
||||||
|
|
||||||
@query("#option_input", true) private _optionInput?: HaTextField;
|
@query("#option_input", true) private _optionInput?: HaTextField;
|
||||||
|
|
||||||
public connectedCallback() {
|
private _optionMoved(ev: CustomEvent): void {
|
||||||
super.connectedCallback();
|
ev.stopPropagation();
|
||||||
this._createSortable();
|
const { oldIndex, newIndex } = ev.detail;
|
||||||
}
|
|
||||||
|
|
||||||
public disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
this._destroySortable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _createSortable() {
|
|
||||||
const Sortable = (await import("../../../../resources/sortable")).default;
|
|
||||||
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 _dragged(ev: SortableEvent): void {
|
|
||||||
if (ev.oldIndex === ev.newIndex) return;
|
|
||||||
|
|
||||||
const options = this._options.concat();
|
const options = this._options.concat();
|
||||||
const option = options.splice(ev.oldIndex!, 1)[0];
|
const option = options.splice(oldIndex, 1)[0];
|
||||||
options.splice(ev.newIndex!, 0, option);
|
options.splice(newIndex, 0, option);
|
||||||
|
|
||||||
fireEvent(this, "value-changed", {
|
fireEvent(this, "value-changed", {
|
||||||
value: { ...this._item, options },
|
value: { ...this._item, options },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _destroySortable() {
|
|
||||||
this._sortable?.destroy();
|
|
||||||
this._sortable = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
set item(item: InputSelect) {
|
set item(item: InputSelect) {
|
||||||
this._item = item;
|
this._item = item;
|
||||||
if (item) {
|
if (item) {
|
||||||
@ -142,39 +101,41 @@ class HaInputSelectForm extends LitElement {
|
|||||||
"ui.dialogs.helper_settings.input_select.options"
|
"ui.dialogs.helper_settings.input_select.options"
|
||||||
)}:
|
)}:
|
||||||
</div>
|
</div>
|
||||||
<mwc-list class="options">
|
<ha-sortable @item-moved=${this._optionMoved} handle-selector=".handle">
|
||||||
${this._options.length
|
<mwc-list class="options">
|
||||||
? repeat(
|
${this._options.length
|
||||||
this._options,
|
? repeat(
|
||||||
(option) => option,
|
this._options,
|
||||||
(option, index) => html`
|
(option) => option,
|
||||||
<ha-list-item class="option" hasMeta>
|
(option, index) => html`
|
||||||
<div class="optioncontent">
|
<ha-list-item class="option" hasMeta>
|
||||||
<div class="handle">
|
<div class="optioncontent">
|
||||||
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
<div class="handle">
|
||||||
|
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
||||||
|
</div>
|
||||||
|
${option}
|
||||||
</div>
|
</div>
|
||||||
${option}
|
<ha-icon-button
|
||||||
</div>
|
slot="meta"
|
||||||
<ha-icon-button
|
.index=${index}
|
||||||
slot="meta"
|
.label=${this.hass.localize(
|
||||||
.index=${index}
|
"ui.dialogs.helper_settings.input_select.remove_option"
|
||||||
.label=${this.hass.localize(
|
)}
|
||||||
"ui.dialogs.helper_settings.input_select.remove_option"
|
@click=${this._removeOption}
|
||||||
)}
|
.path=${mdiDelete}
|
||||||
@click=${this._removeOption}
|
></ha-icon-button>
|
||||||
.path=${mdiDelete}
|
</ha-list-item>
|
||||||
></ha-icon-button>
|
`
|
||||||
|
)
|
||||||
|
: html`
|
||||||
|
<ha-list-item noninteractive>
|
||||||
|
${this.hass!.localize(
|
||||||
|
"ui.dialogs.helper_settings.input_select.no_options"
|
||||||
|
)}
|
||||||
</ha-list-item>
|
</ha-list-item>
|
||||||
`
|
`}
|
||||||
)
|
</mwc-list>
|
||||||
: html`
|
</ha-sortable>
|
||||||
<ha-list-item noninteractive>
|
|
||||||
${this.hass!.localize(
|
|
||||||
"ui.dialogs.helper_settings.input_select.no_options"
|
|
||||||
)}
|
|
||||||
</ha-list-item>
|
|
||||||
`}
|
|
||||||
</mwc-list>
|
|
||||||
<div class="layout horizontal center">
|
<div class="layout horizontal center">
|
||||||
<ha-textfield
|
<ha-textfield
|
||||||
class="flex-auto"
|
class="flex-auto"
|
||||||
@ -255,7 +216,6 @@ class HaInputSelectForm extends LitElement {
|
|||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
sortableStyles,
|
|
||||||
css`
|
css`
|
||||||
.form {
|
.form {
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
|
@ -14,7 +14,6 @@ import "../../../components/ha-button";
|
|||||||
import "../../../components/ha-button-menu";
|
import "../../../components/ha-button-menu";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import { Fields } from "../../../data/script";
|
import { Fields } from "../../../data/script";
|
||||||
import { sortableStyles } from "../../../resources/ha-sortable-style";
|
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import "./ha-script-field-row";
|
import "./ha-script-field-row";
|
||||||
import type HaScriptFieldRow from "./ha-script-field-row";
|
import type HaScriptFieldRow from "./ha-script-field-row";
|
||||||
@ -142,19 +141,16 @@ export default class HaScriptFields extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return css`
|
||||||
sortableStyles,
|
ha-script-field-row {
|
||||||
css`
|
display: block;
|
||||||
ha-script-field-row {
|
margin-bottom: 16px;
|
||||||
display: block;
|
scroll-margin-top: 48px;
|
||||||
margin-bottom: 16px;
|
}
|
||||||
scroll-margin-top: 48px;
|
ha-svg-icon {
|
||||||
}
|
height: 20px;
|
||||||
ha-svg-icon {
|
}
|
||||||
height: 20px;
|
`;
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,11 +20,10 @@ import {
|
|||||||
html,
|
html,
|
||||||
nothing,
|
nothing,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import type { SortableEvent } from "sortablejs";
|
|
||||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
@ -35,6 +34,7 @@ import "../../../components/ha-list-item";
|
|||||||
import "../../../components/ha-markdown-element";
|
import "../../../components/ha-markdown-element";
|
||||||
import "../../../components/ha-relative-time";
|
import "../../../components/ha-relative-time";
|
||||||
import "../../../components/ha-select";
|
import "../../../components/ha-select";
|
||||||
|
import "../../../components/ha-sortable";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import "../../../components/ha-textfield";
|
import "../../../components/ha-textfield";
|
||||||
import type { HaTextField } from "../../../components/ha-textfield";
|
import type { HaTextField } from "../../../components/ha-textfield";
|
||||||
@ -50,14 +50,12 @@ import {
|
|||||||
updateItem,
|
updateItem,
|
||||||
} from "../../../data/todo";
|
} from "../../../data/todo";
|
||||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||||
import type { SortableInstance } from "../../../resources/sortable";
|
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { showTodoItemEditDialog } from "../../todo/show-dialog-todo-item-editor";
|
import { showTodoItemEditDialog } from "../../todo/show-dialog-todo-item-editor";
|
||||||
import { findEntities } from "../common/find-entities";
|
import { findEntities } from "../common/find-entities";
|
||||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||||
import { TodoListCardConfig } from "./types";
|
import { TodoListCardConfig } from "./types";
|
||||||
import { sortableStyles } from "../../../resources/ha-sortable-style";
|
|
||||||
|
|
||||||
@customElement("hui-todo-list-card")
|
@customElement("hui-todo-list-card")
|
||||||
export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||||
@ -96,10 +94,6 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
|||||||
|
|
||||||
private _unsubItems?: Promise<UnsubscribeFunc>;
|
private _unsubItems?: Promise<UnsubscribeFunc>;
|
||||||
|
|
||||||
private _sortable?: SortableInstance;
|
|
||||||
|
|
||||||
@query("#unchecked") private _uncheckedContainer?: HTMLElement;
|
|
||||||
|
|
||||||
connectedCallback(): void {
|
connectedCallback(): void {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
if (this.hasUpdated) {
|
if (this.hasUpdated) {
|
||||||
@ -264,9 +258,15 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
|||||||
</ha-button-menu>`
|
</ha-button-menu>`
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
<mwc-list id="unchecked">
|
<ha-sortable
|
||||||
${this._renderItems(uncheckedItems, unavailable)}
|
handle-selector="ha-svg-icon"
|
||||||
</mwc-list>`
|
.disabled=${!this._reordering}
|
||||||
|
@item-moved=${this._itemMoved}
|
||||||
|
>
|
||||||
|
<mwc-list id="unchecked">
|
||||||
|
${this._renderItems(uncheckedItems, unavailable)}
|
||||||
|
</mwc-list>
|
||||||
|
</ha-sortable>`
|
||||||
: html`<p class="empty">
|
: html`<p class="empty">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.lovelace.cards.todo-list.no_unchecked_items"
|
"ui.panel.lovelace.cards.todo-list.no_unchecked_items"
|
||||||
@ -553,43 +553,12 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
|||||||
|
|
||||||
private async _toggleReorder() {
|
private async _toggleReorder() {
|
||||||
this._reordering = !this._reordering;
|
this._reordering = !this._reordering;
|
||||||
await this.updateComplete;
|
|
||||||
if (this._reordering) {
|
|
||||||
this._createSortable();
|
|
||||||
} else {
|
|
||||||
this._sortable?.destroy();
|
|
||||||
this._sortable = undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _createSortable() {
|
private async _itemMoved(ev: CustomEvent) {
|
||||||
const Sortable = (await import("../../../resources/sortable")).default;
|
ev.stopPropagation();
|
||||||
this._sortable = new Sortable(this._uncheckedContainer!, {
|
const { oldIndex, newIndex } = ev.detail;
|
||||||
animation: 150,
|
this._moveItem(oldIndex, newIndex);
|
||||||
fallbackClass: "sortable-fallback",
|
|
||||||
dataIdAttr: "item-id",
|
|
||||||
handle: "ha-svg-icon",
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
if (evt.newIndex === undefined || evt.oldIndex === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Since this is `onEnd` event, it's possible that
|
|
||||||
// an item was dragged away and was put back to its original position.
|
|
||||||
if (evt.oldIndex !== evt.newIndex) {
|
|
||||||
this._moveItem(evt.oldIndex, evt.newIndex);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _moveItem(oldIndex: number, newIndex: number) {
|
private async _moveItem(oldIndex: number, newIndex: number) {
|
||||||
@ -621,165 +590,162 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return css`
|
||||||
sortableStyles,
|
ha-card {
|
||||||
css`
|
height: 100%;
|
||||||
ha-card {
|
box-sizing: border-box;
|
||||||
height: 100%;
|
}
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.has-header {
|
.has-header {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.addRow {
|
.addRow {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.addRow ha-icon-button {
|
.addRow ha-icon-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 16px;
|
right: 16px;
|
||||||
inset-inline-start: initial;
|
inset-inline-start: initial;
|
||||||
inset-inline-end: 16px;
|
inset-inline-end: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.addRow,
|
.addRow,
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
padding-left: 30px;
|
padding-left: 30px;
|
||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
padding-inline-start: 30px;
|
padding-inline-start: 30px;
|
||||||
padding-inline-end: 16px;
|
padding-inline-end: 16px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
direction: var(--direction);
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header span {
|
.header span {
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
padding: 16px 32px;
|
padding: 16px 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-check-list-item {
|
ha-check-list-item {
|
||||||
--mdc-list-item-meta-size: 56px;
|
--mdc-list-item-meta-size: 56px;
|
||||||
min-height: 56px;
|
min-height: 56px;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-check-list-item.multiline {
|
ha-check-list-item.multiline {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
--check-list-item-graphic-margin-top: 8px;
|
--check-list-item-graphic-margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.multiline .column {
|
.multiline .column {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.completed .summary {
|
.completed .summary {
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description,
|
.description,
|
||||||
.due {
|
.due {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
white-space: initial;
|
white-space: initial;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 3;
|
-webkit-line-clamp: 3;
|
||||||
line-clamp: 3;
|
line-clamp: 3;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description p {
|
.description p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description a {
|
.description a {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.due {
|
.due {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.due ha-svg-icon {
|
.due ha-svg-icon {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
--mdc-icon-size: 14px;
|
--mdc-icon-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.due.overdue {
|
.due.overdue {
|
||||||
color: var(--warning-color);
|
color: var(--warning-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.completed .due.overdue {
|
.completed .due.overdue {
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.handle {
|
.handle {
|
||||||
cursor: move; /* fallback if grab cursor is unsupported */
|
cursor: move; /* fallback if grab cursor is unsupported */
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
padding: 16px 4px;
|
padding: 16px 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.deleteItemButton {
|
.deleteItemButton {
|
||||||
position: relative;
|
position: relative;
|
||||||
left: 8px;
|
left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-textfield {
|
ha-textfield {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background-color: var(--divider-color);
|
background-color: var(--divider-color);
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clearall {
|
.clearall {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.todoList {
|
.todoList {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.warning {
|
.warning {
|
||||||
color: var(--error-color);
|
color: var(--error-color);
|
||||||
}
|
}
|
||||||
`,
|
`;
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@ import { mdiDrag } from "@mdi/js";
|
|||||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import type { SortableEvent } from "sortablejs";
|
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import "../../../components/entity/ha-entity-picker";
|
import "../../../components/entity/ha-entity-picker";
|
||||||
import type {
|
import type {
|
||||||
@ -10,8 +9,7 @@ import type {
|
|||||||
HaEntityPickerEntityFilterFunc,
|
HaEntityPickerEntityFilterFunc,
|
||||||
} from "../../../components/entity/ha-entity-picker";
|
} from "../../../components/entity/ha-entity-picker";
|
||||||
import "../../../components/ha-icon-button";
|
import "../../../components/ha-icon-button";
|
||||||
import { sortableStyles } from "../../../resources/ha-sortable-style";
|
import "../../../components/ha-sortable";
|
||||||
import type { SortableInstance } from "../../../resources/sortable";
|
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { EntityConfig } from "../entity-rows/types";
|
import { EntityConfig } from "../entity-rows/types";
|
||||||
|
|
||||||
@ -27,13 +25,6 @@ export class HuiEntityEditor extends LitElement {
|
|||||||
|
|
||||||
private _entityKeys = new WeakMap<EntityConfig, string>();
|
private _entityKeys = new WeakMap<EntityConfig, string>();
|
||||||
|
|
||||||
private _sortable?: SortableInstance;
|
|
||||||
|
|
||||||
public disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
this._destroySortable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getKey(action: EntityConfig) {
|
private _getKey(action: EntityConfig) {
|
||||||
if (!this._entityKeys.has(action)) {
|
if (!this._entityKeys.has(action)) {
|
||||||
this._entityKeys.set(action, Math.random().toString());
|
this._entityKeys.set(action, Math.random().toString());
|
||||||
@ -55,27 +46,29 @@ export class HuiEntityEditor extends LitElement {
|
|||||||
this.hass!.localize("ui.panel.lovelace.editor.card.config.required") +
|
this.hass!.localize("ui.panel.lovelace.editor.card.config.required") +
|
||||||
")"}
|
")"}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="entities">
|
<ha-sortable handle-selector=".handle" @item-moved=${this._entityMoved}>
|
||||||
${repeat(
|
<div class="entities">
|
||||||
this.entities,
|
${repeat(
|
||||||
(entityConf) => this._getKey(entityConf),
|
this.entities,
|
||||||
(entityConf, index) => html`
|
(entityConf) => this._getKey(entityConf),
|
||||||
<div class="entity" data-entity-id=${entityConf.entity}>
|
(entityConf, index) => html`
|
||||||
<div class="handle">
|
<div class="entity" data-entity-id=${entityConf.entity}>
|
||||||
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
<div class="handle">
|
||||||
|
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
||||||
|
</div>
|
||||||
|
<ha-entity-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.value=${entityConf.entity}
|
||||||
|
.index=${index}
|
||||||
|
.entityFilter=${this.entityFilter}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
allow-custom-entity
|
||||||
|
></ha-entity-picker>
|
||||||
</div>
|
</div>
|
||||||
<ha-entity-picker
|
`
|
||||||
.hass=${this.hass}
|
)}
|
||||||
.value=${entityConf.entity}
|
</div>
|
||||||
.index=${index}
|
</ha-sortable>
|
||||||
.entityFilter=${this.entityFilter}
|
|
||||||
@value-changed=${this._valueChanged}
|
|
||||||
allow-custom-entity
|
|
||||||
></ha-entity-picker>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<ha-entity-picker
|
<ha-entity-picker
|
||||||
class="add-entity"
|
class="add-entity"
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@ -85,41 +78,6 @@ export class HuiEntityEditor extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated(): void {
|
|
||||||
this._createSortable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _createSortable() {
|
|
||||||
const Sortable = (await import("../../../resources/sortable")).default;
|
|
||||||
this._sortable = new Sortable(
|
|
||||||
this.shadowRoot!.querySelector(".entities")!,
|
|
||||||
{
|
|
||||||
animation: 150,
|
|
||||||
fallbackClass: "sortable-fallback",
|
|
||||||
handle: ".handle",
|
|
||||||
dataIdAttr: "data-entity-id",
|
|
||||||
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._entityMoved(evt);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _destroySortable() {
|
|
||||||
this._sortable?.destroy();
|
|
||||||
this._sortable = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _addEntity(ev: CustomEvent): Promise<void> {
|
private async _addEntity(ev: CustomEvent): Promise<void> {
|
||||||
const value = ev.detail.value;
|
const value = ev.detail.value;
|
||||||
if (value === "") {
|
if (value === "") {
|
||||||
@ -132,14 +90,13 @@ export class HuiEntityEditor extends LitElement {
|
|||||||
fireEvent(this, "entities-changed", { entities: newConfigEntities });
|
fireEvent(this, "entities-changed", { entities: newConfigEntities });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _entityMoved(ev: SortableEvent): void {
|
private _entityMoved(ev: CustomEvent): void {
|
||||||
if (ev.oldIndex === ev.newIndex) {
|
ev.stopPropagation();
|
||||||
return;
|
const { oldIndex, newIndex } = ev.detail;
|
||||||
}
|
|
||||||
|
|
||||||
const newEntities = this.entities!.concat();
|
const newEntities = this.entities!.concat();
|
||||||
|
|
||||||
newEntities.splice(ev.newIndex!, 0, newEntities.splice(ev.oldIndex!, 1)[0]);
|
newEntities.splice(newIndex, 0, newEntities.splice(oldIndex, 1)[0]);
|
||||||
|
|
||||||
fireEvent(this, "entities-changed", { entities: newEntities });
|
fireEvent(this, "entities-changed", { entities: newEntities });
|
||||||
}
|
}
|
||||||
@ -162,39 +119,36 @@ export class HuiEntityEditor extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return css`
|
||||||
sortableStyles,
|
ha-entity-picker {
|
||||||
css`
|
margin-top: 8px;
|
||||||
ha-entity-picker {
|
}
|
||||||
margin-top: 8px;
|
.add-entity {
|
||||||
}
|
display: block;
|
||||||
.add-entity {
|
margin-left: 31px;
|
||||||
display: block;
|
margin-inline-start: 31px;
|
||||||
margin-left: 31px;
|
margin-inline-end: initial;
|
||||||
margin-inline-start: 31px;
|
direction: var(--direction);
|
||||||
margin-inline-end: initial;
|
}
|
||||||
direction: var(--direction);
|
.entity {
|
||||||
}
|
display: flex;
|
||||||
.entity {
|
align-items: center;
|
||||||
display: flex;
|
}
|
||||||
align-items: center;
|
.entity .handle {
|
||||||
}
|
padding-right: 8px;
|
||||||
.entity .handle {
|
cursor: move; /* fallback if grab cursor is unsupported */
|
||||||
padding-right: 8px;
|
cursor: grab;
|
||||||
cursor: move; /* fallback if grab cursor is unsupported */
|
padding-inline-end: 8px;
|
||||||
cursor: grab;
|
padding-inline-start: initial;
|
||||||
padding-inline-end: 8px;
|
direction: var(--direction);
|
||||||
padding-inline-start: initial;
|
}
|
||||||
direction: var(--direction);
|
.entity .handle > * {
|
||||||
}
|
pointer-events: none;
|
||||||
.entity .handle > * {
|
}
|
||||||
pointer-events: none;
|
.entity ha-entity-picker {
|
||||||
}
|
flex-grow: 1;
|
||||||
.entity ha-entity-picker {
|
}
|
||||||
flex-grow: 1;
|
`;
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,13 +3,13 @@ import { HassEntity } from "home-assistant-js-websocket";
|
|||||||
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import type { SortableEvent } from "sortablejs";
|
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||||
import "../../../../components/entity/ha-entity-picker";
|
import "../../../../components/entity/ha-entity-picker";
|
||||||
import "../../../../components/ha-button";
|
import "../../../../components/ha-button";
|
||||||
import "../../../../components/ha-icon-button";
|
import "../../../../components/ha-icon-button";
|
||||||
import "../../../../components/ha-list-item";
|
import "../../../../components/ha-list-item";
|
||||||
|
import "../../../../components/ha-sortable";
|
||||||
import "../../../../components/ha-svg-icon";
|
import "../../../../components/ha-svg-icon";
|
||||||
import {
|
import {
|
||||||
CUSTOM_TYPE_PREFIX,
|
CUSTOM_TYPE_PREFIX,
|
||||||
@ -18,8 +18,6 @@ import {
|
|||||||
isCustomType,
|
isCustomType,
|
||||||
stripCustomPrefix,
|
stripCustomPrefix,
|
||||||
} from "../../../../data/lovelace_custom_cards";
|
} from "../../../../data/lovelace_custom_cards";
|
||||||
import { sortableStyles } from "../../../../resources/ha-sortable-style";
|
|
||||||
import type { SortableInstance } from "../../../../resources/sortable";
|
|
||||||
import { HomeAssistant } from "../../../../types";
|
import { HomeAssistant } from "../../../../types";
|
||||||
import { supportsAlarmModesCardFeature } from "../../card-features/hui-alarm-modes-card-feature";
|
import { supportsAlarmModesCardFeature } from "../../card-features/hui-alarm-modes-card-feature";
|
||||||
import { supportsClimateFanModesCardFeature } from "../../card-features/hui-climate-fan-modes-card-feature";
|
import { supportsClimateFanModesCardFeature } from "../../card-features/hui-climate-fan-modes-card-feature";
|
||||||
@ -39,11 +37,11 @@ import { supportsNumericInputCardFeature } from "../../card-features/hui-numeric
|
|||||||
import { supportsSelectOptionsCardFeature } from "../../card-features/hui-select-options-card-feature";
|
import { supportsSelectOptionsCardFeature } from "../../card-features/hui-select-options-card-feature";
|
||||||
import { supportsTargetHumidityCardFeature } from "../../card-features/hui-target-humidity-card-feature";
|
import { supportsTargetHumidityCardFeature } from "../../card-features/hui-target-humidity-card-feature";
|
||||||
import { supportsTargetTemperatureCardFeature } from "../../card-features/hui-target-temperature-card-feature";
|
import { supportsTargetTemperatureCardFeature } from "../../card-features/hui-target-temperature-card-feature";
|
||||||
|
import { supportsUpdateActionsCardFeature } from "../../card-features/hui-update-actions-card-feature";
|
||||||
import { supportsVacuumCommandsCardFeature } from "../../card-features/hui-vacuum-commands-card-feature";
|
import { supportsVacuumCommandsCardFeature } from "../../card-features/hui-vacuum-commands-card-feature";
|
||||||
import { supportsWaterHeaterOperationModesCardFeature } from "../../card-features/hui-water-heater-operation-modes-card-feature";
|
import { supportsWaterHeaterOperationModesCardFeature } from "../../card-features/hui-water-heater-operation-modes-card-feature";
|
||||||
import { LovelaceCardFeatureConfig } from "../../card-features/types";
|
import { LovelaceCardFeatureConfig } from "../../card-features/types";
|
||||||
import { getCardFeatureElementClass } from "../../create-element/create-card-feature-element";
|
import { getCardFeatureElementClass } from "../../create-element/create-card-feature-element";
|
||||||
import { supportsUpdateActionsCardFeature } from "../../card-features/hui-update-actions-card-feature";
|
|
||||||
|
|
||||||
export type FeatureType = LovelaceCardFeatureConfig["type"];
|
export type FeatureType = LovelaceCardFeatureConfig["type"];
|
||||||
type SupportsFeature = (stateObj: HassEntity) => boolean;
|
type SupportsFeature = (stateObj: HassEntity) => boolean;
|
||||||
@ -149,13 +147,6 @@ export class HuiCardFeaturesEditor extends LitElement {
|
|||||||
|
|
||||||
private _featuresKeys = new WeakMap<LovelaceCardFeatureConfig, string>();
|
private _featuresKeys = new WeakMap<LovelaceCardFeatureConfig, string>();
|
||||||
|
|
||||||
private _sortable?: SortableInstance;
|
|
||||||
|
|
||||||
public disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
this._destroySortable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _supportsFeatureType(type: string): boolean {
|
private _supportsFeatureType(type: string): boolean {
|
||||||
if (!this.stateObj) return false;
|
if (!this.stateObj) return false;
|
||||||
|
|
||||||
@ -205,10 +196,6 @@ export class HuiCardFeaturesEditor extends LitElement {
|
|||||||
return this._featuresKeys.get(feature)!;
|
return this._featuresKeys.get(feature)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated() {
|
|
||||||
this._createSortable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getSupportedFeaturesType() {
|
private _getSupportedFeaturesType() {
|
||||||
const featuresTypes = UI_FEATURE_TYPES.filter(
|
const featuresTypes = UI_FEATURE_TYPES.filter(
|
||||||
(type) => !this.featuresTypes || this.featuresTypes.includes(type)
|
(type) => !this.featuresTypes || this.featuresTypes.includes(type)
|
||||||
@ -249,61 +236,66 @@ export class HuiCardFeaturesEditor extends LitElement {
|
|||||||
</ha-alert>
|
</ha-alert>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
<div class="features">
|
<ha-sortable
|
||||||
${repeat(
|
handle-selector=".handle"
|
||||||
this.features,
|
@item-moved=${this._featureMoved}
|
||||||
(featureConf) => this._getKey(featureConf),
|
>
|
||||||
(featureConf, index) => {
|
<div class="features">
|
||||||
const type = featureConf.type;
|
${repeat(
|
||||||
const supported = this._supportsFeatureType(type);
|
this.features,
|
||||||
const editable = this._isFeatureTypeEditable(type);
|
(featureConf) => this._getKey(featureConf),
|
||||||
return html`
|
(featureConf, index) => {
|
||||||
<div class="feature">
|
const type = featureConf.type;
|
||||||
<div class="handle">
|
const supported = this._supportsFeatureType(type);
|
||||||
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
const editable = this._isFeatureTypeEditable(type);
|
||||||
</div>
|
return html`
|
||||||
<div class="feature-content">
|
<div class="feature">
|
||||||
<div>
|
<div class="handle">
|
||||||
<span> ${this._getFeatureTypeLabel(type)} </span>
|
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
||||||
${this.stateObj && !supported
|
|
||||||
? html`
|
|
||||||
<span class="secondary">
|
|
||||||
${this.hass!.localize(
|
|
||||||
"ui.panel.lovelace.editor.features.not_compatible"
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="feature-content">
|
||||||
|
<div>
|
||||||
|
<span> ${this._getFeatureTypeLabel(type)} </span>
|
||||||
|
${this.stateObj && !supported
|
||||||
|
? html`
|
||||||
|
<span class="secondary">
|
||||||
|
${this.hass!.localize(
|
||||||
|
"ui.panel.lovelace.editor.features.not_compatible"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${editable
|
||||||
|
? html`
|
||||||
|
<ha-icon-button
|
||||||
|
.label=${this.hass!.localize(
|
||||||
|
`ui.panel.lovelace.editor.features.edit`
|
||||||
|
)}
|
||||||
|
.path=${mdiPencil}
|
||||||
|
class="edit-icon"
|
||||||
|
.index=${index}
|
||||||
|
@click=${this._editFeature}
|
||||||
|
.disabled=${!supported}
|
||||||
|
></ha-icon-button>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
<ha-icon-button
|
||||||
|
.label=${this.hass!.localize(
|
||||||
|
`ui.panel.lovelace.editor.features.remove`
|
||||||
|
)}
|
||||||
|
.path=${mdiDelete}
|
||||||
|
class="remove-icon"
|
||||||
|
.index=${index}
|
||||||
|
@click=${this._removeFeature}
|
||||||
|
></ha-icon-button>
|
||||||
</div>
|
</div>
|
||||||
${editable
|
`;
|
||||||
? html`
|
}
|
||||||
<ha-icon-button
|
)}
|
||||||
.label=${this.hass!.localize(
|
</div>
|
||||||
`ui.panel.lovelace.editor.features.edit`
|
</ha-sortable>
|
||||||
)}
|
|
||||||
.path=${mdiPencil}
|
|
||||||
class="edit-icon"
|
|
||||||
.index=${index}
|
|
||||||
@click=${this._editFeature}
|
|
||||||
.disabled=${!supported}
|
|
||||||
></ha-icon-button>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
<ha-icon-button
|
|
||||||
.label=${this.hass!.localize(
|
|
||||||
`ui.panel.lovelace.editor.features.remove`
|
|
||||||
)}
|
|
||||||
.path=${mdiDelete}
|
|
||||||
class="remove-icon"
|
|
||||||
.index=${index}
|
|
||||||
@click=${this._removeFeature}
|
|
||||||
></ha-icon-button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
${supportedFeaturesType.length > 0
|
${supportedFeaturesType.length > 0
|
||||||
? html`
|
? html`
|
||||||
<ha-button-menu
|
<ha-button-menu
|
||||||
@ -345,36 +337,6 @@ export class HuiCardFeaturesEditor extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _createSortable() {
|
|
||||||
const Sortable = (await import("../../../../resources/sortable")).default;
|
|
||||||
this._sortable = new Sortable(
|
|
||||||
this.shadowRoot!.querySelector(".features")!,
|
|
||||||
{
|
|
||||||
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._rowMoved(evt);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _destroySortable() {
|
|
||||||
this._sortable?.destroy();
|
|
||||||
this._sortable = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _addFeature(ev: CustomEvent): Promise<void> {
|
private async _addFeature(ev: CustomEvent): Promise<void> {
|
||||||
const index = ev.detail.index as number;
|
const index = ev.detail.index as number;
|
||||||
|
|
||||||
@ -395,14 +357,13 @@ export class HuiCardFeaturesEditor extends LitElement {
|
|||||||
fireEvent(this, "features-changed", { features: newConfigFeature });
|
fireEvent(this, "features-changed", { features: newConfigFeature });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _rowMoved(ev: SortableEvent): void {
|
private _featureMoved(ev: CustomEvent): void {
|
||||||
if (ev.oldIndex === ev.newIndex) {
|
ev.stopPropagation();
|
||||||
return;
|
const { oldIndex, newIndex } = ev.detail;
|
||||||
}
|
|
||||||
|
|
||||||
const newFeatures = this.features!.concat();
|
const newFeatures = this.features!.concat();
|
||||||
|
|
||||||
newFeatures.splice(ev.newIndex!, 0, newFeatures.splice(ev.oldIndex!, 1)[0]);
|
newFeatures.splice(newIndex, 0, newFeatures.splice(oldIndex, 1)[0]);
|
||||||
|
|
||||||
fireEvent(this, "features-changed", { features: newFeatures });
|
fireEvent(this, "features-changed", { features: newFeatures });
|
||||||
}
|
}
|
||||||
@ -428,79 +389,76 @@ export class HuiCardFeaturesEditor extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return css`
|
||||||
sortableStyles,
|
:host {
|
||||||
css`
|
display: flex !important;
|
||||||
:host {
|
flex-direction: column;
|
||||||
display: flex !important;
|
}
|
||||||
flex-direction: column;
|
.content {
|
||||||
}
|
padding: 12px;
|
||||||
.content {
|
}
|
||||||
padding: 12px;
|
ha-expansion-panel {
|
||||||
}
|
display: block;
|
||||||
ha-expansion-panel {
|
--expansion-panel-content-padding: 0;
|
||||||
display: block;
|
border-radius: 6px;
|
||||||
--expansion-panel-content-padding: 0;
|
}
|
||||||
border-radius: 6px;
|
h3 {
|
||||||
}
|
margin: 0;
|
||||||
h3 {
|
font-size: inherit;
|
||||||
margin: 0;
|
font-weight: inherit;
|
||||||
font-size: inherit;
|
}
|
||||||
font-weight: inherit;
|
ha-svg-icon,
|
||||||
}
|
ha-icon {
|
||||||
ha-svg-icon,
|
color: var(--secondary-text-color);
|
||||||
ha-icon {
|
}
|
||||||
color: var(--secondary-text-color);
|
ha-button-menu {
|
||||||
}
|
margin-top: 8px;
|
||||||
ha-button-menu {
|
}
|
||||||
margin-top: 8px;
|
.feature {
|
||||||
}
|
display: flex;
|
||||||
.feature {
|
align-items: center;
|
||||||
display: flex;
|
}
|
||||||
align-items: center;
|
.feature .handle {
|
||||||
}
|
cursor: move; /* fallback if grab cursor is unsupported */
|
||||||
.feature .handle {
|
cursor: grab;
|
||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
cursor: move; /* fallback if grab cursor is unsupported */
|
padding-inline-end: 8px;
|
||||||
cursor: grab;
|
padding-inline-start: initial;
|
||||||
padding-inline-end: 8px;
|
direction: var(--direction);
|
||||||
padding-inline-start: initial;
|
}
|
||||||
direction: var(--direction);
|
.feature .handle > * {
|
||||||
}
|
pointer-events: none;
|
||||||
.feature .handle > * {
|
}
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-content {
|
.feature-content {
|
||||||
height: 60px;
|
height: 60px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-content div {
|
.feature-content div {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.remove-icon,
|
.remove-icon,
|
||||||
.edit-icon {
|
.edit-icon {
|
||||||
--mdc-icon-button-size: 36px;
|
--mdc-icon-button-size: 36px;
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary {
|
.secondary {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
li[divider] {
|
li[divider] {
|
||||||
border-bottom-color: var(--divider-color);
|
border-bottom-color: var(--divider-color);
|
||||||
}
|
}
|
||||||
`,
|
`;
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
import { mdiClose, mdiDrag, mdiPencil } from "@mdi/js";
|
import { mdiClose, mdiDrag, mdiPencil } from "@mdi/js";
|
||||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import type { SortableEvent } from "sortablejs";
|
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import "../../../components/entity/ha-entity-picker";
|
import "../../../components/entity/ha-entity-picker";
|
||||||
import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker";
|
import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker";
|
||||||
import "../../../components/ha-icon-button";
|
import "../../../components/ha-icon-button";
|
||||||
|
import "../../../components/ha-sortable";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import { sortableStyles } from "../../../resources/ha-sortable-style";
|
|
||||||
import type { SortableInstance } from "../../../resources/sortable";
|
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { EntityConfig, LovelaceRowConfig } from "../entity-rows/types";
|
import { EntityConfig, LovelaceRowConfig } from "../entity-rows/types";
|
||||||
|
|
||||||
@ -31,13 +29,6 @@ export class HuiEntitiesCardRowEditor extends LitElement {
|
|||||||
|
|
||||||
private _entityKeys = new WeakMap<LovelaceRowConfig, string>();
|
private _entityKeys = new WeakMap<LovelaceRowConfig, string>();
|
||||||
|
|
||||||
private _sortable?: SortableInstance;
|
|
||||||
|
|
||||||
public disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
this._destroySortable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getKey(action: LovelaceRowConfig) {
|
private _getKey(action: LovelaceRowConfig) {
|
||||||
if (!this._entityKeys.has(action)) {
|
if (!this._entityKeys.has(action)) {
|
||||||
this._entityKeys.set(action, Math.random().toString());
|
this._entityKeys.set(action, Math.random().toString());
|
||||||
@ -60,64 +51,66 @@ export class HuiEntitiesCardRowEditor extends LitElement {
|
|||||||
"ui.panel.lovelace.editor.card.config.required"
|
"ui.panel.lovelace.editor.card.config.required"
|
||||||
)})`}
|
)})`}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="entities">
|
<ha-sortable handle-selector=".handle" @item-moved=${this._rowMoved}>
|
||||||
${repeat(
|
<div class="entities">
|
||||||
this.entities,
|
${repeat(
|
||||||
(entityConf) => this._getKey(entityConf),
|
this.entities,
|
||||||
(entityConf, index) => html`
|
(entityConf) => this._getKey(entityConf),
|
||||||
<div class="entity">
|
(entityConf, index) => html`
|
||||||
<div class="handle">
|
<div class="entity">
|
||||||
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
<div class="handle">
|
||||||
</div>
|
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
||||||
${entityConf.type
|
</div>
|
||||||
? html`
|
${entityConf.type
|
||||||
<div class="special-row">
|
? html`
|
||||||
<div>
|
<div class="special-row">
|
||||||
<span>
|
<div>
|
||||||
${this.hass!.localize(
|
<span>
|
||||||
`ui.panel.lovelace.editor.card.entities.entity_row.${entityConf.type}`
|
${this.hass!.localize(
|
||||||
)}
|
`ui.panel.lovelace.editor.card.entities.entity_row.${entityConf.type}`
|
||||||
</span>
|
)}
|
||||||
<span class="secondary"
|
</span>
|
||||||
>${this.hass!.localize(
|
<span class="secondary"
|
||||||
"ui.panel.lovelace.editor.card.entities.edit_special_row"
|
>${this.hass!.localize(
|
||||||
)}</span
|
"ui.panel.lovelace.editor.card.entities.edit_special_row"
|
||||||
>
|
)}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
`
|
||||||
`
|
: html`
|
||||||
: html`
|
<ha-entity-picker
|
||||||
<ha-entity-picker
|
allow-custom-entity
|
||||||
allow-custom-entity
|
hideClearIcon
|
||||||
hideClearIcon
|
.hass=${this.hass}
|
||||||
.hass=${this.hass}
|
.value=${(entityConf as EntityConfig).entity}
|
||||||
.value=${(entityConf as EntityConfig).entity}
|
.index=${index}
|
||||||
.index=${index}
|
@value-changed=${this._valueChanged}
|
||||||
@value-changed=${this._valueChanged}
|
></ha-entity-picker>
|
||||||
></ha-entity-picker>
|
`}
|
||||||
`}
|
<ha-icon-button
|
||||||
<ha-icon-button
|
.label=${this.hass!.localize(
|
||||||
.label=${this.hass!.localize(
|
"ui.components.entity.entity-picker.clear"
|
||||||
"ui.components.entity.entity-picker.clear"
|
)}
|
||||||
)}
|
.path=${mdiClose}
|
||||||
.path=${mdiClose}
|
class="remove-icon"
|
||||||
class="remove-icon"
|
.index=${index}
|
||||||
.index=${index}
|
@click=${this._removeRow}
|
||||||
@click=${this._removeRow}
|
></ha-icon-button>
|
||||||
></ha-icon-button>
|
<ha-icon-button
|
||||||
<ha-icon-button
|
.label=${this.hass!.localize(
|
||||||
.label=${this.hass!.localize(
|
"ui.components.entity.entity-picker.edit"
|
||||||
"ui.components.entity.entity-picker.edit"
|
)}
|
||||||
)}
|
.path=${mdiPencil}
|
||||||
.path=${mdiPencil}
|
class="edit-icon"
|
||||||
class="edit-icon"
|
.index=${index}
|
||||||
.index=${index}
|
@click=${this._editRow}
|
||||||
@click=${this._editRow}
|
></ha-icon-button>
|
||||||
></ha-icon-button>
|
</div>
|
||||||
</div>
|
`
|
||||||
`
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</ha-sortable>
|
||||||
<ha-entity-picker
|
<ha-entity-picker
|
||||||
class="add-entity"
|
class="add-entity"
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@ -126,40 +119,6 @@ export class HuiEntitiesCardRowEditor extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated(): void {
|
|
||||||
this._createSortable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _createSortable() {
|
|
||||||
const Sortable = (await import("../../../resources/sortable")).default;
|
|
||||||
this._sortable = new Sortable(
|
|
||||||
this.shadowRoot!.querySelector(".entities")!,
|
|
||||||
{
|
|
||||||
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._rowMoved(evt);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _destroySortable() {
|
|
||||||
this._sortable?.destroy();
|
|
||||||
this._sortable = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _addEntity(ev: CustomEvent): Promise<void> {
|
private async _addEntity(ev: CustomEvent): Promise<void> {
|
||||||
const value = ev.detail.value;
|
const value = ev.detail.value;
|
||||||
if (value === "") {
|
if (value === "") {
|
||||||
@ -172,14 +131,13 @@ export class HuiEntitiesCardRowEditor extends LitElement {
|
|||||||
fireEvent(this, "entities-changed", { entities: newConfigEntities });
|
fireEvent(this, "entities-changed", { entities: newConfigEntities });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _rowMoved(ev: SortableEvent): void {
|
private _rowMoved(ev: CustomEvent): void {
|
||||||
if (ev.oldIndex === ev.newIndex) {
|
ev.stopPropagation();
|
||||||
return;
|
const { oldIndex, newIndex } = ev.detail;
|
||||||
}
|
|
||||||
|
|
||||||
const newEntities = this.entities!.concat();
|
const newEntities = this.entities!.concat();
|
||||||
|
|
||||||
newEntities.splice(ev.newIndex!, 0, newEntities.splice(ev.oldIndex!, 1)[0]);
|
newEntities.splice(newIndex, 0, newEntities.splice(oldIndex, 1)[0]);
|
||||||
|
|
||||||
fireEvent(this, "entities-changed", { entities: newEntities });
|
fireEvent(this, "entities-changed", { entities: newEntities });
|
||||||
}
|
}
|
||||||
@ -222,67 +180,64 @@ export class HuiEntitiesCardRowEditor extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return css`
|
||||||
sortableStyles,
|
ha-entity-picker {
|
||||||
css`
|
margin-top: 8px;
|
||||||
ha-entity-picker {
|
}
|
||||||
margin-top: 8px;
|
.add-entity {
|
||||||
}
|
display: block;
|
||||||
.add-entity {
|
margin-left: 31px;
|
||||||
display: block;
|
margin-right: 71px;
|
||||||
margin-left: 31px;
|
margin-inline-start: 31px;
|
||||||
margin-right: 71px;
|
margin-inline-end: 71px;
|
||||||
margin-inline-start: 31px;
|
direction: var(--direction);
|
||||||
margin-inline-end: 71px;
|
}
|
||||||
direction: var(--direction);
|
.entity {
|
||||||
}
|
display: flex;
|
||||||
.entity {
|
align-items: center;
|
||||||
display: flex;
|
}
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entity .handle {
|
.entity .handle {
|
||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
cursor: move; /* fallback if grab cursor is unsupported */
|
cursor: move; /* fallback if grab cursor is unsupported */
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
padding-inline-end: 8px;
|
padding-inline-end: 8px;
|
||||||
padding-inline-start: initial;
|
padding-inline-start: initial;
|
||||||
direction: var(--direction);
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
.entity .handle > * {
|
.entity .handle > * {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entity ha-entity-picker {
|
.entity ha-entity-picker {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.special-row {
|
.special-row {
|
||||||
height: 60px;
|
height: 60px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.special-row div {
|
.special-row div {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.remove-icon,
|
.remove-icon,
|
||||||
.edit-icon {
|
.edit-icon {
|
||||||
--mdc-icon-button-size: 36px;
|
--mdc-icon-button-size: 36px;
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary {
|
.secondary {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
`,
|
`;
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { css } from "lit";
|
import { css } from "lit";
|
||||||
|
|
||||||
export const sortableStyles = css`
|
export const sidebarEditStyle = css`
|
||||||
#sortable a:nth-of-type(2n) paper-icon-item {
|
.reorder-list a:nth-of-type(2n) paper-icon-item {
|
||||||
animation-name: keyframes1;
|
animation-name: keyframes1;
|
||||||
animation-iteration-count: infinite;
|
animation-iteration-count: infinite;
|
||||||
transform-origin: 50% 10%;
|
transform-origin: 50% 10%;
|
||||||
@ -9,7 +9,7 @@ export const sortableStyles = css`
|
|||||||
animation-duration: 0.25s;
|
animation-duration: 0.25s;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sortable a:nth-of-type(2n-1) paper-icon-item {
|
.reorder-list a:nth-of-type(2n-1) paper-icon-item {
|
||||||
animation-name: keyframes2;
|
animation-name: keyframes2;
|
||||||
animation-iteration-count: infinite;
|
animation-iteration-count: infinite;
|
||||||
animation-direction: alternate;
|
animation-direction: alternate;
|
||||||
@ -18,12 +18,12 @@ export const sortableStyles = css`
|
|||||||
animation-duration: 0.33s;
|
animation-duration: 0.33s;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sortable a {
|
.reorder-list a {
|
||||||
height: 48px;
|
height: 48px;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sortable {
|
.reorder-list {
|
||||||
outline: none;
|
outline: none;
|
||||||
display: block !important;
|
display: block !important;
|
||||||
}
|
}
|
||||||
@ -32,26 +32,6 @@ export const sortableStyles = css`
|
|||||||
display: flex !important;
|
display: flex !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sortable-fallback {
|
|
||||||
display: none;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sortable-ghost {
|
|
||||||
border: 2px solid var(--primary-color);
|
|
||||||
background: rgba(var(--rgb-primary-color), 0.25);
|
|
||||||
border-radius: 4px;
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sortable-drag {
|
|
||||||
border-radius: 4px;
|
|
||||||
opacity: 1;
|
|
||||||
background: var(--card-background-color);
|
|
||||||
box-shadow: 0px 4px 8px 3px #00000026;
|
|
||||||
cursor: grabbing;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes keyframes1 {
|
@keyframes keyframes1 {
|
||||||
0% {
|
0% {
|
||||||
transform: rotate(-1deg);
|
transform: rotate(-1deg);
|
Loading…
x
Reference in New Issue
Block a user