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,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 {

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

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,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>
`; `;
} }

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,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; `;
}
`,
];
} }
} }

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,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;

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,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; `;
}
`,
];
} }
} }

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,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; `;
}
`,
];
} }
} }

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

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,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; `;
}
`,
];
} }
} }

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>
<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);
} }
`, `;
];
} }
} }

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,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; `;
}
`,
];
} }
} }

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,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);
} }
`, `;
];
} }
} }

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,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);
} }
`, `;
];
} }
} }

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