Add ha-sortable component (#19294)

This commit is contained in:
Paul Bottein 2024-01-08 14:02:41 +01:00 committed by GitHub
parent 17e62c10d4
commit 104aef3dec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1366 additions and 1703 deletions

View File

@ -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,14 +148,19 @@ export class HaSelectSelector extends LitElement {
return html` return html`
${value?.length ${value?.length
? html` ? html`
<ha-sortable
no-style
.disabled=${!this.selector.select.reorder}
@item-moved=${this._itemMoved}
>
<ha-chip-set> <ha-chip-set>
${repeat( ${repeat(
value, value,
(item) => item, (item) => item,
(item, idx) => { (item, idx) => {
const label = const label =
options.find((option) => option.value === item)?.label || options.find((option) => option.value === item)
item; ?.label || item;
return html` return html`
<ha-input-chip <ha-input-chip
.idx=${idx} .idx=${idx}
@ -226,6 +184,7 @@ export class HaSelectSelector extends LitElement {
} }
)} )}
</ha-chip-set> </ha-chip-set>
</ha-sortable>
` `
: nothing} : nothing}
@ -419,9 +378,7 @@ export class HaSelectSelector extends LitElement {
this.comboBox.filteredItems = filteredItems; this.comboBox.filteredItems = filteredItems;
} }
static styles = [ static styles = css`
sortableStyles,
css`
:host { :host {
position: relative; position: relative;
} }
@ -436,8 +393,20 @@ export class HaSelectSelector extends LitElement {
ha-chip-set { ha-chip-set {
padding: 8px 0; padding: 8px 0;
} }
`,
]; .sortable-fallback {
display: none;
opacity: 0;
}
.sortable-ghost {
opacity: 0.4;
}
.sortable-drag {
cursor: grabbing;
}
`;
} }
declare global { declare global {

View File

@ -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;
} }

View 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;
}
}

View File

@ -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;
} }
@ -119,6 +84,11 @@ export class DialogAreaFilter
@closed=${this._cancel} @closed=${this._cancel}
.heading=${this._dialogParams.title ?? .heading=${this._dialogParams.title ??
this.hass.localize("ui.components.area-filter.title")} this.hass.localize("ui.components.area-filter.title")}
>
<ha-sortable
draggable-selector=".draggable"
handle-selector=".handle"
@item-moved=${this._areaMoved}
> >
<mwc-list class="areas"> <mwc-list class="areas">
${repeat( ${repeat(
@ -164,6 +134,7 @@ export class DialogAreaFilter
} }
)} )}
</mwc-list> </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 {

View File

@ -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,6 +184,12 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<ha-sortable
@item-moved=${this._colorMoved}
item=".color"
no-style
.disabled=${!this.editMode}
>
<div class="container"> <div class="container">
${this._favoriteColors.map( ${this._favoriteColors.map(
(color, index) => html` (color, index) => html`
@ -289,6 +256,7 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
` `
: nothing} : nothing}
</div> </div>
</ha-sortable>
`; `;
} }

View File

@ -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,14 +58,19 @@ 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}
<ha-sortable
handle-selector=".handle"
.disabled=${!this.reOrderMode}
@item-moved=${this._actionMoved}
>
<div class="actions"> <div class="actions">
${repeat( ${repeat(
this.actions, this.actions,
@ -119,6 +119,7 @@ export default class HaAutomationAction extends LitElement {
` `
)} )}
</div> </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,9 +270,7 @@ export default class HaAutomationAction extends LitElement {
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return css`
sortableStyles,
css`
ha-automation-action-row { ha-automation-action-row {
display: block; display: block;
margin-bottom: 16px; margin-bottom: 16px;
@ -320,9 +286,9 @@ export default class HaAutomationAction extends LitElement {
overflow: hidden; overflow: hidden;
} }
.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;
@ -333,8 +299,7 @@ export default class HaAutomationAction extends LitElement {
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; gap: 8px;
} }
`, `;
];
} }
} }

View File

@ -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,12 +96,18 @@ export class HaChooseAction extends LitElement implements ActionElement {
const action = this.action; const action = this.action;
return html` return html`
<ha-sortable
handle-selector=".handle"
.disabled=${!this.reOrderMode}
@item-moved=${this._optionMoved}
>
<div class="options"> <div class="options">
${repeat( ${repeat(
action.choose ? ensureArray(action.choose) : [], action.choose ? ensureArray(action.choose) : [],
(option) => option, (option) => option,
(option, idx) => (option, idx) => html`
html`<ha-card> <div class="option">
<ha-card>
<ha-expansion-panel <ha-expansion-panel
.index=${idx} .index=${idx}
leftChevron leftChevron
@ -248,9 +250,12 @@ export class HaChooseAction extends LitElement implements ActionElement {
></ha-automation-action> ></ha-automation-action>
</div> </div>
</ha-expansion-panel> </ha-expansion-panel>
</ha-card>` </ha-card>
</div>
`
)} )}
</div> </div>
</ha-sortable>
<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;

View File

@ -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,14 +105,20 @@ 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}
<ha-sortable
handle-selector=".handle"
.disabled=${!this.reOrderMode}
@item-moved=${this._conditionMoved}
>
<div class="conditions"> <div class="conditions">
${repeat( ${repeat(
this.conditions.filter((c) => typeof c === "object"), this.conditions.filter((c) => typeof c === "object"),
@ -175,6 +168,7 @@ export default class HaAutomationCondition extends LitElement {
` `
)} )}
</div> </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,9 +305,7 @@ export default class HaAutomationCondition extends LitElement {
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return css`
sortableStyles,
css`
ha-automation-condition-row { ha-automation-condition-row {
display: block; display: block;
margin-bottom: 16px; margin-bottom: 16px;
@ -358,9 +321,9 @@ export default class HaAutomationCondition extends LitElement {
overflow: hidden; overflow: hidden;
} }
.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;
@ -371,8 +334,7 @@ export default class HaAutomationCondition extends LitElement {
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; gap: 8px;
} }
`, `;
];
} }
} }

View File

@ -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,14 +55,19 @@ 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}
<ha-sortable
handle-selector=".handle"
.disabled=${!this.reOrderMode}
@item-moved=${this._triggerMoved}
>
<div class="triggers"> <div class="triggers">
${repeat( ${repeat(
this.triggers, this.triggers,
@ -113,6 +113,8 @@ export default class HaAutomationTrigger extends LitElement {
</ha-automation-trigger-row> </ha-automation-trigger-row>
` `
)} )}
</div>
</ha-sortable>
<ha-button <ha-button
outlined outlined
.label=${this.hass.localize( .label=${this.hass.localize(
@ -123,7 +125,6 @@ export default class HaAutomationTrigger extends LitElement {
> >
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon> <ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button> </ha-button>
</div>
`; `;
} }
@ -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,9 +244,7 @@ export default class HaAutomationTrigger extends LitElement {
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return css`
sortableStyles,
css`
ha-automation-trigger-row { ha-automation-trigger-row {
display: block; display: block;
margin-bottom: 16px; margin-bottom: 16px;
@ -298,16 +260,15 @@ export default class HaAutomationTrigger extends LitElement {
overflow: hidden; overflow: hidden;
} }
.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;
height: 24px; height: 24px;
} }
`, `;
];
} }
} }

View File

@ -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,6 +101,7 @@ class HaInputSelectForm extends LitElement {
"ui.dialogs.helper_settings.input_select.options" "ui.dialogs.helper_settings.input_select.options"
)}: )}:
</div> </div>
<ha-sortable @item-moved=${this._optionMoved} handle-selector=".handle">
<mwc-list class="options"> <mwc-list class="options">
${this._options.length ${this._options.length
? repeat( ? repeat(
@ -175,6 +135,7 @@ class HaInputSelectForm extends LitElement {
</ha-list-item> </ha-list-item>
`} `}
</mwc-list> </mwc-list>
</ha-sortable>
<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);

View File

@ -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,9 +141,7 @@ export default class HaScriptFields extends LitElement {
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return css`
sortableStyles,
css`
ha-script-field-row { ha-script-field-row {
display: block; display: block;
margin-bottom: 16px; margin-bottom: 16px;
@ -153,8 +150,7 @@ export default class HaScriptFields extends LitElement {
ha-svg-icon { ha-svg-icon {
height: 20px; height: 20px;
} }
`, `;
];
} }
} }

View File

@ -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>
<ha-sortable
handle-selector="ha-svg-icon"
.disabled=${!this._reordering}
@item-moved=${this._itemMoved}
>
<mwc-list id="unchecked"> <mwc-list id="unchecked">
${this._renderItems(uncheckedItems, unavailable)} ${this._renderItems(uncheckedItems, unavailable)}
</mwc-list>` </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,9 +590,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return css`
sortableStyles,
css`
ha-card { ha-card {
height: 100%; height: 100%;
box-sizing: border-box; box-sizing: border-box;
@ -778,8 +745,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
.warning { .warning {
color: var(--error-color); color: var(--error-color);
} }
`, `;
];
} }
} }

View File

@ -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,6 +46,7 @@ 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>
<ha-sortable handle-selector=".handle" @item-moved=${this._entityMoved}>
<div class="entities"> <div class="entities">
${repeat( ${repeat(
this.entities, this.entities,
@ -76,6 +68,7 @@ export class HuiEntityEditor extends LitElement {
` `
)} )}
</div> </div>
</ha-sortable>
<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,9 +119,7 @@ export class HuiEntityEditor extends LitElement {
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return css`
sortableStyles,
css`
ha-entity-picker { ha-entity-picker {
margin-top: 8px; margin-top: 8px;
} }
@ -193,8 +148,7 @@ export class HuiEntityEditor extends LitElement {
.entity ha-entity-picker { .entity ha-entity-picker {
flex-grow: 1; flex-grow: 1;
} }
`, `;
];
} }
} }

View File

@ -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,6 +236,10 @@ export class HuiCardFeaturesEditor extends LitElement {
</ha-alert> </ha-alert>
` `
: nothing} : nothing}
<ha-sortable
handle-selector=".handle"
@item-moved=${this._featureMoved}
>
<div class="features"> <div class="features">
${repeat( ${repeat(
this.features, this.features,
@ -304,6 +295,7 @@ export class HuiCardFeaturesEditor extends LitElement {
} }
)} )}
</div> </div>
</ha-sortable>
${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,9 +389,7 @@ export class HuiCardFeaturesEditor extends LitElement {
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return css`
sortableStyles,
css`
:host { :host {
display: flex !important; display: flex !important;
flex-direction: column; flex-direction: column;
@ -460,9 +419,9 @@ export class HuiCardFeaturesEditor extends LitElement {
align-items: center; align-items: center;
} }
.feature .handle { .feature .handle {
padding-right: 8px;
cursor: move; /* fallback if grab cursor is unsupported */ cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab; cursor: grab;
padding-right: 8px;
padding-inline-end: 8px; padding-inline-end: 8px;
padding-inline-start: initial; padding-inline-start: initial;
direction: var(--direction); direction: var(--direction);
@ -499,8 +458,7 @@ export class HuiCardFeaturesEditor extends LitElement {
li[divider] { li[divider] {
border-bottom-color: var(--divider-color); border-bottom-color: var(--divider-color);
} }
`, `;
];
} }
} }

View File

@ -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,6 +51,7 @@ export class HuiEntitiesCardRowEditor extends LitElement {
"ui.panel.lovelace.editor.card.config.required" "ui.panel.lovelace.editor.card.config.required"
)})`} )})`}
</h3> </h3>
<ha-sortable handle-selector=".handle" @item-moved=${this._rowMoved}>
<div class="entities"> <div class="entities">
${repeat( ${repeat(
this.entities, this.entities,
@ -118,6 +110,7 @@ export class HuiEntitiesCardRowEditor extends LitElement {
` `
)} )}
</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,9 +180,7 @@ export class HuiEntitiesCardRowEditor extends LitElement {
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return css`
sortableStyles,
css`
ha-entity-picker { ha-entity-picker {
margin-top: 8px; margin-top: 8px;
} }
@ -281,8 +237,7 @@ export class HuiEntitiesCardRowEditor extends LitElement {
font-size: 12px; font-size: 12px;
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
`, `;
];
} }
} }

View File

@ -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);