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 { 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 { repeat } from "lit/directives/repeat";
import { SortableEvent } from "sortablejs";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
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 "../chips/ha-chip-set";
import "../chips/ha-input-chip";
@ -21,6 +18,7 @@ import "../ha-formfield";
import "../ha-input-helper-text";
import "../ha-radio";
import "../ha-select";
import "../ha-sortable";
@customElement("ha-selector-select")
export class HaSelectSelector extends LitElement {
@ -42,50 +40,10 @@ export class HaSelectSelector extends LitElement {
@query("ha-combo-box", true) private comboBox!: HaComboBox;
private _sortable?: SortableInstance;
protected updated(changedProps: PropertyValues): void {
if (changedProps.has("value") || changedProps.has("selector")) {
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 _itemMoved(ev: CustomEvent): void {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
this._move(oldIndex!, newIndex);
}
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 = "";
protected render() {
@ -195,37 +148,43 @@ export class HaSelectSelector extends LitElement {
return html`
${value?.length
? html`
<ha-chip-set>
${repeat(
value,
(item) => item,
(item, idx) => {
const label =
options.find((option) => option.value === item)?.label ||
item;
return html`
<ha-input-chip
.idx=${idx}
@remove=${this._removeItem}
.label=${label}
selected
>
${this.selector.select?.reorder
? html`
<ha-svg-icon
slot="icon"
.path=${mdiDrag}
data-handle
></ha-svg-icon>
`
: nothing}
${options.find((option) => option.value === item)
?.label || item}
</ha-input-chip>
`;
}
)}
</ha-chip-set>
<ha-sortable
no-style
.disabled=${!this.selector.select.reorder}
@item-moved=${this._itemMoved}
>
<ha-chip-set>
${repeat(
value,
(item) => item,
(item, idx) => {
const label =
options.find((option) => option.value === item)
?.label || item;
return html`
<ha-input-chip
.idx=${idx}
@remove=${this._removeItem}
.label=${label}
selected
>
${this.selector.select?.reorder
? html`
<ha-svg-icon
slot="icon"
.path=${mdiDrag}
data-handle
></ha-svg-icon>
`
: nothing}
${options.find((option) => option.value === item)
?.label || item}
</ha-input-chip>
`;
}
)}
</ha-chip-set>
</ha-sortable>
`
: nothing}
@ -419,25 +378,35 @@ export class HaSelectSelector extends LitElement {
this.comboBox.filteredItems = filteredItems;
}
static styles = [
sortableStyles,
css`
:host {
position: relative;
}
ha-select,
mwc-formfield,
ha-formfield {
display: block;
}
mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
ha-chip-set {
padding: 8px 0;
}
`,
];
static styles = css`
:host {
position: relative;
}
ha-select,
mwc-formfield,
ha-formfield {
display: block;
}
mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
ha-chip-set {
padding: 8px 0;
}
.sortable-fallback {
display: none;
opacity: 0;
}
.sortable-ghost {
opacity: 0.4;
}
.sortable-drag {
cursor: grabbing;
}
`;
}
declare global {

View File

@ -33,7 +33,6 @@ import {
} from "lit";
import { customElement, eventOptions, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { guard } from "lit/directives/guard";
import memoizeOne from "memoize-one";
import { storage } from "../common/decorators/storage";
import { fireEvent } from "../common/dom/fire_event";
@ -50,12 +49,12 @@ import { subscribeRepairsIssueRegistry } from "../data/repairs";
import { UpdateEntity, updateCanInstall } from "../data/update";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
import type { SortableInstance } from "../resources/sortable";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, PanelInfo, Route } from "../types";
import "./ha-icon";
import "./ha-icon-button";
import "./ha-menu-button";
import "./ha-sortable";
import "./ha-svg-icon";
import "./user/ha-user-badge";
@ -204,15 +203,13 @@ class HaSidebar extends SubscribeMixin(LitElement) {
@state() private _issuesCount = 0;
@state() private _renderEmptySortable = false;
private _mouseLeaveTimeout?: number;
private _tooltipHideTimeout?: number;
private _recentKeydownActiveUntil = 0;
private sortableStyleLoaded = false;
private _editStyleLoaded = false;
@storage({
key: "sidebarPanelOrder",
@ -228,8 +225,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
})
private _hiddenPanels: string[] = [];
private _sortable?: SortableInstance;
public hassSubscribe(): UnsubscribeFunc[] {
return this.hass.user?.is_admin
? [
@ -264,14 +259,13 @@ class HaSidebar extends SubscribeMixin(LitElement) {
changedProps.has("expanded") ||
changedProps.has("narrow") ||
changedProps.has("alwaysExpand") ||
changedProps.has("editMode") ||
changedProps.has("_externalConfig") ||
changedProps.has("_updatesCount") ||
changedProps.has("_issuesCount") ||
changedProps.has("_notifications") ||
changedProps.has("editMode") ||
changedProps.has("_renderEmptySortable") ||
changedProps.has("_hiddenPanels") ||
(changedProps.has("_panelOrder") && !this.editMode)
changedProps.has("_panelOrder")
) {
return true;
}
@ -306,12 +300,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
if (changedProps.has("alwaysExpand")) {
toggleAttribute(this, "expanded", this.alwaysExpand);
}
if (changedProps.has("editMode")) {
if (this.editMode) {
this._activateEditMode();
} else {
this._deactivateEditMode();
}
if (changedProps.has("editMode") && this.editMode) {
this._editModeActivated();
}
if (!changedProps.has("hass")) {
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[]) {
// prettier-ignore
return html`<div id="sortable">
${guard([this._hiddenPanels, this._renderEmptySortable], () =>
this._renderEmptySortable ? "" : this._renderPanels(beforeSpacer)
)}
</div>
${this._renderSpacer()}
${this._renderHiddenPanels()} `;
return html`
<ha-sortable
handle-selector="paper-icon-item"
.disabled=${!this.editMode}
@item-moved=${this._panelMoved}
>
<div class="reorder-list">${this._renderPanels(beforeSpacer)}</div>
</ha-sortable>
${this._renderSpacer()}${this._renderHiddenPanels()}
`;
}
private _renderHiddenPanels() {
@ -674,44 +685,22 @@ class HaSidebar extends SubscribeMixin(LitElement) {
fireEvent(this, "hass-edit-sidebar", { editMode: true });
}
private async _activateEditMode() {
await Promise.all([this._loadSortableStyle(), this._createSortable()]);
private async _editModeActivated() {
await this._loadEditStyle();
}
private async _loadSortableStyle() {
if (this.sortableStyleLoaded) return;
private async _loadEditStyle() {
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");
style.innerHTML = (sortStylesImport.sortableStyles as CSSResult).cssText;
style.innerHTML = (editStylesImport.sidebarEditStyle as CSSResult).cssText;
this.shadowRoot!.appendChild(style);
this.sortableStyleLoaded = true;
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() {
fireEvent(this, "hass-edit-sidebar", { editMode: false });
}
@ -724,13 +713,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
}
// Make a copy for Memoize
this._hiddenPanels = [...this._hiddenPanels, panel];
this._renderEmptySortable = true;
await this.updateComplete;
const container = this.shadowRoot!.getElementById("sortable")!;
while (container.lastElementChild) {
container.removeChild(container.lastElementChild);
}
this._renderEmptySortable = false;
// Remove it from the panel order
this._panelOrder = this._panelOrder.filter((order) => order !== panel);
}
private async _unhidePanel(ev: Event) {
@ -739,13 +723,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
this._hiddenPanels = this._hiddenPanels.filter(
(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) {
@ -910,7 +887,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
.menu mwc-button {
width: 100%;
}
#sortable,
.reorder-list,
.hidden-panel {
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 { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
import type { SortableEvent } from "sortablejs";
import { fireEvent } from "../../common/dom/fire_event";
import type { AreaFilterValue } from "../../components/ha-area-filter";
import "../../components/ha-button";
import "../../components/ha-icon-button";
import "../../components/ha-list-item";
import "../../components/ha-sortable";
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 { HomeAssistant } from "../../types";
import { HassDialog } from "../make-dialog-manager";
@ -31,23 +29,18 @@ export class DialogAreaFilter
@state() private _areas: string[] = [];
private _sortable?: SortableInstance;
public async showDialog(dialogParams: AreaFilterDialogParams): Promise<void> {
public showDialog(dialogParams: AreaFilterDialogParams): void {
this._dialogParams = dialogParams;
this._hidden = dialogParams.initialValue?.hidden ?? [];
const order = dialogParams.initialValue?.order ?? [];
const allAreas = Object.keys(this.hass!.areas);
this._areas = allAreas.concat().sort(areaCompare(this.hass!.areas, order));
await this.updateComplete;
this._createSortable();
}
public closeDialog(): void {
this._dialogParams = undefined;
this._hidden = [];
this._areas = [];
this._destroySortable();
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@ -66,42 +59,14 @@ export class DialogAreaFilter
this.closeDialog();
}
private async _createSortable() {
const Sortable = (await import("../../resources/sortable")).default;
if (this._sortable) return;
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;
private _areaMoved(ev: CustomEvent): void {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
const areas = this._areas.concat();
const option = areas.splice(ev.oldIndex!, 1)[0];
areas.splice(ev.newIndex!, 0, option);
const option = areas.splice(oldIndex, 1)[0];
areas.splice(newIndex, 0, option);
this._areas = areas;
}
@ -120,50 +85,56 @@ export class DialogAreaFilter
.heading=${this._dialogParams.title ??
this.hass.localize("ui.components.area-filter.title")}
>
<mwc-list class="areas">
${repeat(
allAreas,
(area) => area,
(area, _idx) => {
const isVisible = !this._hidden.includes(area);
const name = this.hass!.areas[area]?.name || area;
return html`
<ha-list-item
class=${classMap({
hidden: !isVisible,
draggable: isVisible,
})}
hasMeta
graphic="icon"
noninteractive
>
${isVisible
? html`<ha-svg-icon
class="handle"
.path=${mdiDrag}
slot="graphic"
></ha-svg-icon>`
: nothing}
${name}
<ha-icon-button
tabindex="0"
class="action"
.path=${isVisible ? mdiEye : mdiEyeOff}
slot="meta"
.label=${this.hass!.localize(
`ui.components.area-filter.${
isVisible ? "hide" : "show"
}`,
{ area: name }
)}
.area=${area}
@click=${this._toggle}
></ha-icon-button>
</ha-list-item>
`;
}
)}
</mwc-list>
<ha-sortable
draggable-selector=".draggable"
handle-selector=".handle"
@item-moved=${this._areaMoved}
>
<mwc-list class="areas">
${repeat(
allAreas,
(area) => area,
(area, _idx) => {
const isVisible = !this._hidden.includes(area);
const name = this.hass!.areas[area]?.name || area;
return html`
<ha-list-item
class=${classMap({
hidden: !isVisible,
draggable: isVisible,
})}
hasMeta
graphic="icon"
noninteractive
>
${isVisible
? html`<ha-svg-icon
class="handle"
.path=${mdiDrag}
slot="graphic"
></ha-svg-icon>`
: nothing}
${name}
<ha-icon-button
tabindex="0"
class="action"
.path=${isVisible ? mdiEye : mdiEyeOff}
slot="meta"
.label=${this.hass!.localize(
`ui.components.area-filter.${
isVisible ? "hide" : "show"
}`,
{ area: name }
)}
.area=${area}
@click=${this._toggle}
></ha-icon-button>
</ha-list-item>
`;
}
)}
</mwc-list>
</ha-sortable>
<ha-button slot="secondaryAction" dialogAction="cancel">
${this.hass.localize("ui.common.cancel")}
</ha-button>
@ -192,7 +163,6 @@ export class DialogAreaFilter
static get styles(): CSSResultGroup {
return [
sortableStyles,
haStyleDialog,
css`
ha-dialog {

View File

@ -10,9 +10,9 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import type { SortableEvent } from "sortablejs";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-control-slider";
import "../../../../components/ha-sortable";
import { UNAVAILABLE } from "../../../../data/entity";
import {
ExtEntityRegistryEntry,
@ -24,7 +24,6 @@ import {
computeDefaultFavoriteColors,
} from "../../../../data/light";
import { actionHandler } from "../../../../panels/lovelace/common/directives/action-handler-directive";
import type { SortableInstance } from "../../../../resources/sortable";
import { HomeAssistant } from "../../../../types";
import { showConfirmationDialog } from "../../../generic/show-dialog-box";
import "./ha-favorite-color-button";
@ -48,16 +47,7 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
@state() private _favoriteColors: LightColor[] = [];
private _sortable?: SortableInstance;
protected updated(changedProps: PropertyValues): void {
if (changedProps.has("editMode")) {
if (this.editMode) {
this._createSortable();
} else {
this._destroySortable();
}
}
if (changedProps.has("entry")) {
if (this.entry) {
if (this.entry.options?.light?.favorite_colors) {
@ -69,34 +59,10 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
}
}
private async _createSortable() {
const Sortable = (await import("../../../../resources/sortable")).default;
this._sortable = new Sortable(
this.shadowRoot!.querySelector(".container")!,
{
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 _colorMoved(ev: CustomEvent): void {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
this._move(oldIndex, newIndex);
}
private _move(index: number, newIndex: number) {
@ -107,11 +73,6 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
this._save(favoriteColors);
}
private _destroySortable() {
this._sortable?.destroy();
this._sortable = undefined;
}
private _apply = (index: number) => {
const favorite = this._favoriteColors[index];
this.hass.callService("light", "turn_on", {
@ -223,72 +184,79 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
protected render(): TemplateResult {
return html`
<div class="container">
${this._favoriteColors.map(
(color, index) => html`
<div class="color">
<div
class="color-bubble ${classMap({
shake: !!this.editMode,
})}"
>
<ha-favorite-color-button
.label=${this.hass.localize(
`ui.dialogs.more_info_control.light.favorite_color.${
this.editMode ? "edit" : "set"
}`,
{ 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-sortable
@item-moved=${this._colorMoved}
item=".color"
no-style
.disabled=${!this.editMode}
>
<div class="container">
${this._favoriteColors.map(
(color, index) => html`
<div class="color">
<div
class="color-bubble ${classMap({
shake: !!this.editMode,
})}"
>
</ha-favorite-color-button>
${this.editMode
? html`
<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}
<ha-favorite-color-button
.label=${this.hass.localize(
`ui.dialogs.more_info_control.light.favorite_color.${
this.editMode ? "edit" : "set"
}`,
{ 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>
${this.editMode
? html`
<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>
`
)}
${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 deepClone from "deep-clone-simple";
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import type { SortableEvent } from "sortablejs";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-button";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-sortable";
import { getService, isService } from "../../../../data/action";
import type { AutomationClipboard } from "../../../../data/automation";
import { Action } from "../../../../data/script";
import { sortableStyles } from "../../../../resources/ha-sortable-style";
import type { SortableInstance } from "../../../../resources/sortable";
import { HomeAssistant } from "../../../../types";
import {
PASTE_VALUE,
@ -48,8 +45,6 @@ export default class HaAutomationAction extends LitElement {
private _actionKeys = new WeakMap<Action, string>();
private _sortable?: SortableInstance;
protected render() {
return html`
${this.reOrderMode && !this.nested
@ -63,62 +58,68 @@ export default class HaAutomationAction extends LitElement {
${this.hass.localize(
"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(
"ui.panel.config.automation.editor.re_order_mode.exit"
)}
</mwc-button>
</ha-button>
</ha-alert>
`
: null}
<div class="actions">
${repeat(
this.actions,
(action) => this._getKey(action),
(action, idx) => html`
<ha-automation-action-row
.index=${idx}
.action=${action}
.narrow=${this.narrow}
.disabled=${this.disabled}
.hideMenu=${this.reOrderMode}
.reOrderMode=${this.reOrderMode}
@duplicate=${this._duplicateAction}
@value-changed=${this._actionChanged}
@re-order=${this._enterReOrderMode}
.hass=${this.hass}
>
${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 === this.actions.length - 1}
></ha-icon-button>
<div class="handle" slot="icons">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</div>
`
: ""}
</ha-automation-action-row>
`
)}
</div>
<ha-sortable
handle-selector=".handle"
.disabled=${!this.reOrderMode}
@item-moved=${this._actionMoved}
>
<div class="actions">
${repeat(
this.actions,
(action) => this._getKey(action),
(action, idx) => html`
<ha-automation-action-row
.index=${idx}
.action=${action}
.narrow=${this.narrow}
.disabled=${this.disabled}
.hideMenu=${this.reOrderMode}
.reOrderMode=${this.reOrderMode}
@duplicate=${this._duplicateAction}
@value-changed=${this._actionChanged}
@re-order=${this._enterReOrderMode}
.hass=${this.hass}
>
${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 === this.actions.length - 1}
></ha-icon-button>
<div class="handle" slot="icons">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</div>
`
: ""}
</ha-automation-action-row>
`
)}
</div>
</ha-sortable>
<div class="buttons">
<ha-button
outlined
@ -146,13 +147,6 @@ export default class HaAutomationAction extends LitElement {
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("reOrderMode")) {
if (this.reOrderMode) {
this._createSortable();
} else {
this._destroySortable();
}
}
if (changedProps.has("actions") && this._focusLastActionOnChange) {
this._focusLastActionOnChange = false;
@ -215,33 +209,6 @@ export default class HaAutomationAction extends LitElement {
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) {
if (!this._actionKeys.has(action)) {
this._actionKeys.set(action, Math.random().toString());
@ -262,11 +229,6 @@ export default class HaAutomationAction extends LitElement {
this._move(index, newIndex);
}
private _dragged(ev: SortableEvent): void {
if (ev.oldIndex === ev.newIndex) return;
this._move(ev.oldIndex!, ev.newIndex!);
}
private _move(index: number, newIndex: number) {
const actions = this.actions.concat();
const action = actions.splice(index, 1)[0];
@ -274,6 +236,12 @@ export default class HaAutomationAction extends LitElement {
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) {
ev.stopPropagation();
const actions = [...this.actions];
@ -302,39 +270,36 @@ export default class HaAutomationAction extends LitElement {
}
static get styles(): CSSResultGroup {
return [
sortableStyles,
css`
ha-automation-action-row {
display: block;
margin-bottom: 16px;
scroll-margin-top: 48px;
}
ha-svg-icon {
height: 20px;
}
ha-alert {
display: block;
margin-bottom: 16px;
border-radius: var(--ha-card-border-radius, 12px);
overflow: hidden;
}
.handle {
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
padding: 12px;
}
.handle ha-svg-icon {
pointer-events: none;
height: 24px;
}
.buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
`,
];
return css`
ha-automation-action-row {
display: block;
margin-bottom: 16px;
scroll-margin-top: 48px;
}
ha-svg-icon {
height: 20px;
}
ha-alert {
display: block;
margin-bottom: 16px;
border-radius: var(--ha-card-border-radius, 12px);
overflow: hidden;
}
.handle {
padding: 12px;
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
}
.handle ha-svg-icon {
pointer-events: none;
height: 24px;
}
.buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
`;
}
}

View File

@ -1,29 +1,31 @@
import { consume } from "@lit-labs/context";
import type { SortableEvent } from "sortablejs";
import type { ActionDetail } from "@material/mwc-list";
import {
mdiDotsVertical,
mdiRenameBox,
mdiSort,
mdiArrowDown,
mdiArrowUp,
mdiContentDuplicate,
mdiDelete,
mdiPlus,
mdiArrowUp,
mdiArrowDown,
mdiDotsVertical,
mdiDrag,
mdiPlus,
mdiRenameBox,
mdiSort,
} from "@mdi/js";
import deepClone from "deep-clone-simple";
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
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 { fireEvent } from "../../../../../common/dom/fire_event";
import { capitalizeFirstLetter } from "../../../../../common/string/capitalize-first-letter";
import "../../../../../components/ha-button";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-button-menu";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-sortable";
import { Condition } from "../../../../../data/automation";
import { describeCondition } from "../../../../../data/automation_i18n";
import { fullEntitiesContext } from "../../../../../data/context";
import { EntityRegistryEntry } from "../../../../../data/entity_registry";
import {
Action,
ChooseAction,
@ -36,10 +38,6 @@ import {
import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
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();
@ -63,8 +61,6 @@ export class HaChooseAction extends LitElement implements ActionElement {
private _expandLast = false;
private _sortable?: SortableInstance;
public static get defaultConfig() {
return { choose: [{ conditions: [], sequence: [] }] };
}
@ -100,157 +96,166 @@ export class HaChooseAction extends LitElement implements ActionElement {
const action = this.action;
return html`
<div class="options">
${repeat(
action.choose ? ensureArray(action.choose) : [],
(option) => option,
(option, idx) =>
html`<ha-card>
<ha-expansion-panel
.index=${idx}
leftChevron
@expanded-changed=${this._expandedChanged}
>
<h3 slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.option",
{ number: idx + 1 }
)}:
${option.alias ||
(this._expandedStates[idx]
? ""
: this._getDescription(option))}
</h3>
${this.reOrderMode
? html`
<ha-icon-button
.index=${idx}
slot="icons"
.label=${this.hass.localize(
"ui.panel.config.automation.editor.move_up"
<ha-sortable
handle-selector=".handle"
.disabled=${!this.reOrderMode}
@item-moved=${this._optionMoved}
>
<div class="options">
${repeat(
action.choose ? ensureArray(action.choose) : [],
(option) => option,
(option, idx) => html`
<div class="option">
<ha-card>
<ha-expansion-panel
.index=${idx}
leftChevron
@expanded-changed=${this._expandedChanged}
>
<h3 slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.option",
{ number: idx + 1 }
)}:
${option.alias ||
(this._expandedStates[idx]
? ""
: this._getDescription(option))}
</h3>
${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}
@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"
.reOrderMode=${this.reOrderMode}
.disabled=${this.disabled}
.hass=${this.hass}
.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
)}
.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>
@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>
`
)}
</div>
</ha-sortable>
<ha-button
outlined
.label=${this.hass.localize(
@ -352,14 +357,6 @@ export class HaChooseAction extends LitElement implements ActionElement {
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("reOrderMode")) {
if (this.reOrderMode) {
this._createSortable();
} else {
this._destroySortable();
}
}
if (this._expandLast) {
const nodes = this.shadowRoot!.querySelectorAll("ha-expansion-panel");
nodes[nodes.length - 1].expanded = true;
@ -425,11 +422,6 @@ export class HaChooseAction extends LitElement implements ActionElement {
this._move(index, newIndex);
}
private _dragged(ev: SortableEvent): void {
if (ev.oldIndex === ev.newIndex) return;
this._move(ev.oldIndex!, ev.newIndex!);
}
private _move(index: number, newIndex: number) {
const options = ensureArray(this.action.choose)!.concat();
const item = options.splice(index, 1)[0];
@ -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) {
const index = (ev.target as any).idx;
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 {
return [
haStyle,
sortableStyles,
css`
ha-card {
.option {
margin: 0 0 16px 0;
}
.add-card mwc-button {
@ -543,9 +512,9 @@ export class HaChooseAction extends LitElement implements ActionElement {
padding: 0 16px 16px 16px;
}
.handle {
padding: 12px;
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
padding: 12px;
}
.handle ha-svg-icon {
pointer-events: none;

View File

@ -1,4 +1,3 @@
import "@material/mwc-button";
import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple";
import {
@ -11,18 +10,16 @@ import {
} from "lit";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import type { SortableEvent } from "sortablejs";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-button";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-sortable";
import "../../../../components/ha-svg-icon";
import type {
AutomationClipboard,
Condition,
} from "../../../../data/automation";
import { sortableStyles } from "../../../../resources/ha-sortable-style";
import type { SortableInstance } from "../../../../resources/sortable";
import type { HomeAssistant } from "../../../../types";
import {
PASTE_VALUE,
@ -55,17 +52,7 @@ export default class HaAutomationCondition extends LitElement {
private _conditionKeys = new WeakMap<Condition, string>();
private _sortable?: SortableInstance;
protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("reOrderMode")) {
if (this.reOrderMode) {
this._createSortable();
} else {
this._destroySortable();
}
}
if (!changedProperties.has("conditions")) {
return;
}
@ -118,63 +105,70 @@ export default class HaAutomationCondition extends LitElement {
${this.hass.localize(
"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(
"ui.panel.config.automation.editor.re_order_mode.exit"
)}
</mwc-button>
</ha-button>
</ha-alert>
`
: null}
<div class="conditions">
${repeat(
this.conditions.filter((c) => typeof c === "object"),
(condition) => this._getKey(condition),
(cond, idx) => html`
<ha-automation-condition-row
.index=${idx}
.totalConditions=${this.conditions.length}
.condition=${cond}
.hideMenu=${this.reOrderMode}
.reOrderMode=${this.reOrderMode}
.disabled=${this.disabled}
@duplicate=${this._duplicateCondition}
@move-condition=${this._move}
@value-changed=${this._conditionChanged}
@re-order=${this._enterReOrderMode}
.hass=${this.hass}
>
${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 === this.conditions.length - 1}
></ha-icon-button>
<div class="handle" slot="icons">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</div>
`
: ""}
</ha-automation-condition-row>
`
)}
</div>
<ha-sortable
handle-selector=".handle"
.disabled=${!this.reOrderMode}
@item-moved=${this._conditionMoved}
>
<div class="conditions">
${repeat(
this.conditions.filter((c) => typeof c === "object"),
(condition) => this._getKey(condition),
(cond, idx) => html`
<ha-automation-condition-row
.index=${idx}
.totalConditions=${this.conditions.length}
.condition=${cond}
.hideMenu=${this.reOrderMode}
.reOrderMode=${this.reOrderMode}
.disabled=${this.disabled}
@duplicate=${this._duplicateCondition}
@move-condition=${this._move}
@value-changed=${this._conditionChanged}
@re-order=${this._enterReOrderMode}
.hass=${this.hass}
>
${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 === this.conditions.length - 1}
></ha-icon-button>
<div class="handle" slot="icons">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</div>
`
: ""}
</ha-automation-condition-row>
`
)}
</div>
</ha-sortable>
<div class="buttons">
<ha-button
outlined
@ -248,36 +242,6 @@ export default class HaAutomationCondition extends LitElement {
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) {
if (!this._conditionKeys.has(condition)) {
this._conditionKeys.set(condition, Math.random().toString());
@ -298,11 +262,6 @@ export default class HaAutomationCondition extends LitElement {
this._move(index, newIndex);
}
private _dragged(ev: SortableEvent): void {
if (ev.oldIndex === ev.newIndex) return;
this._move(ev.oldIndex!, ev.newIndex!);
}
private _move(index: number, newIndex: number) {
const conditions = this.conditions.concat();
const condition = conditions.splice(index, 1)[0];
@ -310,6 +269,12 @@ export default class HaAutomationCondition extends LitElement {
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) {
ev.stopPropagation();
const conditions = [...this.conditions];
@ -340,39 +305,36 @@ export default class HaAutomationCondition extends LitElement {
}
static get styles(): CSSResultGroup {
return [
sortableStyles,
css`
ha-automation-condition-row {
display: block;
margin-bottom: 16px;
scroll-margin-top: 48px;
}
ha-svg-icon {
height: 20px;
}
ha-alert {
display: block;
margin-bottom: 16px;
border-radius: var(--ha-card-border-radius, 12px);
overflow: hidden;
}
.handle {
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
padding: 12px;
}
.handle ha-svg-icon {
pointer-events: none;
height: 24px;
}
.buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
`,
];
return css`
ha-automation-condition-row {
display: block;
margin-bottom: 16px;
scroll-margin-top: 48px;
}
ha-svg-icon {
height: 20px;
}
ha-alert {
display: block;
margin-bottom: 16px;
border-radius: var(--ha-card-border-radius, 12px);
overflow: hidden;
}
.handle {
padding: 12px;
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
}
.handle ha-svg-icon {
pointer-events: none;
height: 24px;
}
.buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
`;
}
}

View File

@ -1,25 +1,22 @@
import "@material/mwc-button";
import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple";
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import type { SortableEvent } from "sortablejs";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-button";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-sortable";
import "../../../../components/ha-svg-icon";
import { AutomationClipboard, Trigger } from "../../../../data/automation";
import { sortableStyles } from "../../../../resources/ha-sortable-style";
import type { SortableInstance } from "../../../../resources/sortable";
import { HomeAssistant } from "../../../../types";
import "./ha-automation-trigger-row";
import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
import {
PASTE_VALUE,
showAddAutomationElementDialog,
} from "../show-add-automation-element-dialog";
import "./ha-automation-trigger-row";
import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
@customElement("ha-automation-trigger")
export default class HaAutomationTrigger extends LitElement {
@ -45,8 +42,6 @@ export default class HaAutomationTrigger extends LitElement {
private _triggerKeys = new WeakMap<Trigger, string>();
private _sortable?: SortableInstance;
protected render() {
return html`
${this.reOrderMode && !this.nested
@ -60,70 +55,76 @@ export default class HaAutomationTrigger extends LitElement {
${this.hass.localize(
"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(
"ui.panel.config.automation.editor.re_order_mode.exit"
)}
</mwc-button>
</ha-button>
</ha-alert>
`
: null}
<div class="triggers">
${repeat(
this.triggers,
(trigger) => this._getKey(trigger),
(trg, idx) => html`
<ha-automation-trigger-row
.index=${idx}
.trigger=${trg}
.hideMenu=${this.reOrderMode}
@duplicate=${this._duplicateTrigger}
@value-changed=${this._triggerChanged}
.hass=${this.hass}
.disabled=${this.disabled}
@re-order=${this._enterReOrderMode}
>
${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 === this.triggers.length - 1}
></ha-icon-button>
<div class="handle" slot="icons">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</div>
`
: ""}
</ha-automation-trigger-row>
`
)}
<ha-button
outlined
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.add"
<ha-sortable
handle-selector=".handle"
.disabled=${!this.reOrderMode}
@item-moved=${this._triggerMoved}
>
<div class="triggers">
${repeat(
this.triggers,
(trigger) => this._getKey(trigger),
(trg, idx) => html`
<ha-automation-trigger-row
.index=${idx}
.trigger=${trg}
.hideMenu=${this.reOrderMode}
@duplicate=${this._duplicateTrigger}
@value-changed=${this._triggerChanged}
.hass=${this.hass}
.disabled=${this.disabled}
@re-order=${this._enterReOrderMode}
>
${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 === this.triggers.length - 1}
></ha-icon-button>
<div class="handle" slot="icons">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</div>
`
: ""}
</ha-automation-trigger-row>
`
)}
.disabled=${this.disabled}
@click=${this._addTriggerDialog}
>
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button>
</div>
</div>
</ha-sortable>
<ha-button
outlined
.label=${this.hass.localize(
"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) {
super.updated(changedProps);
if (changedProps.has("reOrderMode")) {
if (this.reOrderMode) {
this._createSortable();
} else {
this._destroySortable();
}
}
if (changedProps.has("triggers") && this._focusLastTriggerOnChange) {
this._focusLastTriggerOnChange = false;
@ -190,36 +183,6 @@ export default class HaAutomationTrigger extends LitElement {
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) {
if (!this._triggerKeys.has(action)) {
this._triggerKeys.set(action, Math.random().toString());
@ -240,11 +203,6 @@ export default class HaAutomationTrigger extends LitElement {
this._move(index, newIndex);
}
private _dragged(ev: SortableEvent): void {
if (ev.oldIndex === ev.newIndex) return;
this._move(ev.oldIndex!, ev.newIndex!);
}
private _move(index: number, newIndex: number) {
const triggers = this.triggers.concat();
const trigger = triggers.splice(index, 1)[0];
@ -252,6 +210,12 @@ export default class HaAutomationTrigger extends LitElement {
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) {
ev.stopPropagation();
const triggers = [...this.triggers];
@ -280,34 +244,31 @@ export default class HaAutomationTrigger extends LitElement {
}
static get styles(): CSSResultGroup {
return [
sortableStyles,
css`
ha-automation-trigger-row {
display: block;
margin-bottom: 16px;
scroll-margin-top: 48px;
}
ha-svg-icon {
height: 20px;
}
ha-alert {
display: block;
margin-bottom: 16px;
border-radius: var(--ha-card-border-radius, 16px);
overflow: hidden;
}
.handle {
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
padding: 12px;
}
.handle ha-svg-icon {
pointer-events: none;
height: 24px;
}
`,
];
return css`
ha-automation-trigger-row {
display: block;
margin-bottom: 16px;
scroll-margin-top: 48px;
}
ha-svg-icon {
height: 20px;
}
ha-alert {
display: block;
margin-bottom: 16px;
border-radius: var(--ha-card-border-radius, 16px);
overflow: hidden;
}
.handle {
padding: 12px;
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
}
.handle ha-svg-icon {
pointer-events: none;
height: 24px;
}
`;
}
}

View File

@ -1,22 +1,20 @@
import "@material/mwc-list/mwc-list";
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 { 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 "../../../../components/ha-button";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-list-item";
import "../../../../components/ha-icon-picker";
import "../../../../components/ha-list-item";
import "../../../../components/ha-sortable";
import "../../../../components/ha-textfield";
import type { HaTextField } from "../../../../components/ha-textfield";
import type { InputSelect } from "../../../../data/input_select";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import type { SortableInstance } from "../../../../resources/sortable";
@customElement("ha-input_select-form")
class HaInputSelectForm extends LitElement {
@ -32,59 +30,20 @@ class HaInputSelectForm extends LitElement {
@state() private _options: string[] = [];
private _sortable?: SortableInstance;
@query("#option_input", true) private _optionInput?: HaTextField;
public connectedCallback() {
super.connectedCallback();
this._createSortable();
}
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;
private _optionMoved(ev: CustomEvent): void {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
const options = this._options.concat();
const option = options.splice(ev.oldIndex!, 1)[0];
options.splice(ev.newIndex!, 0, option);
const option = options.splice(oldIndex, 1)[0];
options.splice(newIndex, 0, option);
fireEvent(this, "value-changed", {
value: { ...this._item, options },
});
}
private _destroySortable() {
this._sortable?.destroy();
this._sortable = undefined;
}
set item(item: InputSelect) {
this._item = item;
if (item) {
@ -142,39 +101,41 @@ class HaInputSelectForm extends LitElement {
"ui.dialogs.helper_settings.input_select.options"
)}:
</div>
<mwc-list class="options">
${this._options.length
? repeat(
this._options,
(option) => option,
(option, index) => html`
<ha-list-item class="option" hasMeta>
<div class="optioncontent">
<div class="handle">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
<ha-sortable @item-moved=${this._optionMoved} handle-selector=".handle">
<mwc-list class="options">
${this._options.length
? repeat(
this._options,
(option) => option,
(option, index) => html`
<ha-list-item class="option" hasMeta>
<div class="optioncontent">
<div class="handle">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</div>
${option}
</div>
${option}
</div>
<ha-icon-button
slot="meta"
.index=${index}
.label=${this.hass.localize(
"ui.dialogs.helper_settings.input_select.remove_option"
)}
@click=${this._removeOption}
.path=${mdiDelete}
></ha-icon-button>
<ha-icon-button
slot="meta"
.index=${index}
.label=${this.hass.localize(
"ui.dialogs.helper_settings.input_select.remove_option"
)}
@click=${this._removeOption}
.path=${mdiDelete}
></ha-icon-button>
</ha-list-item>
`
)
: html`
<ha-list-item noninteractive>
${this.hass!.localize(
"ui.dialogs.helper_settings.input_select.no_options"
)}
</ha-list-item>
`
)
: html`
<ha-list-item noninteractive>
${this.hass!.localize(
"ui.dialogs.helper_settings.input_select.no_options"
)}
</ha-list-item>
`}
</mwc-list>
`}
</mwc-list>
</ha-sortable>
<div class="layout horizontal center">
<ha-textfield
class="flex-auto"
@ -255,7 +216,6 @@ class HaInputSelectForm extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyle,
sortableStyles,
css`
.form {
color: var(--primary-text-color);

View File

@ -14,7 +14,6 @@ import "../../../components/ha-button";
import "../../../components/ha-button-menu";
import "../../../components/ha-svg-icon";
import { Fields } from "../../../data/script";
import { sortableStyles } from "../../../resources/ha-sortable-style";
import { HomeAssistant } from "../../../types";
import "./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 {
return [
sortableStyles,
css`
ha-script-field-row {
display: block;
margin-bottom: 16px;
scroll-margin-top: 48px;
}
ha-svg-icon {
height: 20px;
}
`,
];
return css`
ha-script-field-row {
display: block;
margin-bottom: 16px;
scroll-margin-top: 48px;
}
ha-svg-icon {
height: 20px;
}
`;
}
}

View File

@ -20,11 +20,10 @@ import {
html,
nothing,
} 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 { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import type { SortableEvent } from "sortablejs";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-card";
@ -35,6 +34,7 @@ import "../../../components/ha-list-item";
import "../../../components/ha-markdown-element";
import "../../../components/ha-relative-time";
import "../../../components/ha-select";
import "../../../components/ha-sortable";
import "../../../components/ha-svg-icon";
import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield";
@ -50,14 +50,12 @@ import {
updateItem,
} from "../../../data/todo";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import type { SortableInstance } from "../../../resources/sortable";
import { HomeAssistant } from "../../../types";
import { showTodoItemEditDialog } from "../../todo/show-dialog-todo-item-editor";
import { findEntities } from "../common/find-entities";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { TodoListCardConfig } from "./types";
import { sortableStyles } from "../../../resources/ha-sortable-style";
@customElement("hui-todo-list-card")
export class HuiTodoListCard extends LitElement implements LovelaceCard {
@ -96,10 +94,6 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
private _unsubItems?: Promise<UnsubscribeFunc>;
private _sortable?: SortableInstance;
@query("#unchecked") private _uncheckedContainer?: HTMLElement;
connectedCallback(): void {
super.connectedCallback();
if (this.hasUpdated) {
@ -264,9 +258,15 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
</ha-button-menu>`
: nothing}
</div>
<mwc-list id="unchecked">
${this._renderItems(uncheckedItems, unavailable)}
</mwc-list>`
<ha-sortable
handle-selector="ha-svg-icon"
.disabled=${!this._reordering}
@item-moved=${this._itemMoved}
>
<mwc-list id="unchecked">
${this._renderItems(uncheckedItems, unavailable)}
</mwc-list>
</ha-sortable>`
: html`<p class="empty">
${this.hass.localize(
"ui.panel.lovelace.cards.todo-list.no_unchecked_items"
@ -553,43 +553,12 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
private async _toggleReorder() {
this._reordering = !this._reordering;
await this.updateComplete;
if (this._reordering) {
this._createSortable();
} else {
this._sortable?.destroy();
this._sortable = undefined;
}
}
private async _createSortable() {
const Sortable = (await import("../../../resources/sortable")).default;
this._sortable = new Sortable(this._uncheckedContainer!, {
animation: 150,
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 _itemMoved(ev: CustomEvent) {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
this._moveItem(oldIndex, newIndex);
}
private async _moveItem(oldIndex: number, newIndex: number) {
@ -621,165 +590,162 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
}
static get styles(): CSSResultGroup {
return [
sortableStyles,
css`
ha-card {
height: 100%;
box-sizing: border-box;
}
return css`
ha-card {
height: 100%;
box-sizing: border-box;
}
.has-header {
padding-top: 0;
}
.has-header {
padding-top: 0;
}
.addRow {
padding: 16px;
padding-bottom: 0;
position: relative;
}
.addRow {
padding: 16px;
padding-bottom: 0;
position: relative;
}
.addRow ha-icon-button {
position: absolute;
right: 16px;
inset-inline-start: initial;
inset-inline-end: 16px;
}
.addRow ha-icon-button {
position: absolute;
right: 16px;
inset-inline-start: initial;
inset-inline-end: 16px;
}
.addRow,
.header {
display: flex;
flex-direction: row;
align-items: center;
}
.addRow,
.header {
display: flex;
flex-direction: row;
align-items: center;
}
.header {
padding-left: 30px;
padding-right: 16px;
padding-inline-start: 30px;
padding-inline-end: 16px;
margin-top: 8px;
justify-content: space-between;
direction: var(--direction);
}
.header {
padding-left: 30px;
padding-right: 16px;
padding-inline-start: 30px;
padding-inline-end: 16px;
margin-top: 8px;
justify-content: space-between;
direction: var(--direction);
}
.header span {
color: var(--primary-text-color);
font-weight: 500;
}
.header span {
color: var(--primary-text-color);
font-weight: 500;
}
.empty {
padding: 16px 32px;
}
.empty {
padding: 16px 32px;
}
.item {
margin-top: 8px;
}
.item {
margin-top: 8px;
}
ha-check-list-item {
--mdc-list-item-meta-size: 56px;
min-height: 56px;
height: auto;
}
ha-check-list-item {
--mdc-list-item-meta-size: 56px;
min-height: 56px;
height: auto;
}
ha-check-list-item.multiline {
align-items: flex-start;
--check-list-item-graphic-margin-top: 8px;
}
ha-check-list-item.multiline {
align-items: flex-start;
--check-list-item-graphic-margin-top: 8px;
}
.row {
display: flex;
justify-content: space-between;
}
.row {
display: flex;
justify-content: space-between;
}
.multiline .column {
display: flex;
flex-direction: column;
margin-top: 18px;
margin-bottom: 12px;
}
.multiline .column {
display: flex;
flex-direction: column;
margin-top: 18px;
margin-bottom: 12px;
}
.completed .summary {
text-decoration: line-through;
}
.completed .summary {
text-decoration: line-through;
}
.description,
.due {
font-size: 12px;
color: var(--secondary-text-color);
}
.description,
.due {
font-size: 12px;
color: var(--secondary-text-color);
}
.description {
white-space: initial;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 3;
line-clamp: 3;
-webkit-box-orient: vertical;
}
.description {
white-space: initial;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 3;
line-clamp: 3;
-webkit-box-orient: vertical;
}
.description p {
margin: 0;
}
.description p {
margin: 0;
}
.description a {
color: var(--primary-color);
}
.description a {
color: var(--primary-color);
}
.due {
display: flex;
align-items: center;
}
.due {
display: flex;
align-items: center;
}
.due ha-svg-icon {
margin-right: 4px;
--mdc-icon-size: 14px;
}
.due ha-svg-icon {
margin-right: 4px;
--mdc-icon-size: 14px;
}
.due.overdue {
color: var(--warning-color);
}
.due.overdue {
color: var(--warning-color);
}
.completed .due.overdue {
color: var(--secondary-text-color);
}
.completed .due.overdue {
color: var(--secondary-text-color);
}
.handle {
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
height: 24px;
padding: 16px 4px;
}
.handle {
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
height: 24px;
padding: 16px 4px;
}
.deleteItemButton {
position: relative;
left: 8px;
}
.deleteItemButton {
position: relative;
left: 8px;
}
ha-textfield {
flex-grow: 1;
}
ha-textfield {
flex-grow: 1;
}
.divider {
height: 1px;
background-color: var(--divider-color);
margin: 10px 0;
}
.divider {
height: 1px;
background-color: var(--divider-color);
margin: 10px 0;
}
.clearall {
cursor: pointer;
}
.clearall {
cursor: pointer;
}
.todoList {
display: block;
padding: 8px;
}
.todoList {
display: block;
padding: 8px;
}
.warning {
color: var(--error-color);
}
`,
];
.warning {
color: var(--error-color);
}
`;
}
}

View File

@ -2,7 +2,6 @@ import { mdiDrag } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import type { SortableEvent } from "sortablejs";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/entity/ha-entity-picker";
import type {
@ -10,8 +9,7 @@ import type {
HaEntityPickerEntityFilterFunc,
} from "../../../components/entity/ha-entity-picker";
import "../../../components/ha-icon-button";
import { sortableStyles } from "../../../resources/ha-sortable-style";
import type { SortableInstance } from "../../../resources/sortable";
import "../../../components/ha-sortable";
import { HomeAssistant } from "../../../types";
import { EntityConfig } from "../entity-rows/types";
@ -27,13 +25,6 @@ export class HuiEntityEditor extends LitElement {
private _entityKeys = new WeakMap<EntityConfig, string>();
private _sortable?: SortableInstance;
public disconnectedCallback() {
super.disconnectedCallback();
this._destroySortable();
}
private _getKey(action: EntityConfig) {
if (!this._entityKeys.has(action)) {
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") +
")"}
</h3>
<div class="entities">
${repeat(
this.entities,
(entityConf) => this._getKey(entityConf),
(entityConf, index) => html`
<div class="entity" data-entity-id=${entityConf.entity}>
<div class="handle">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
<ha-sortable handle-selector=".handle" @item-moved=${this._entityMoved}>
<div class="entities">
${repeat(
this.entities,
(entityConf) => this._getKey(entityConf),
(entityConf, index) => html`
<div class="entity" data-entity-id=${entityConf.entity}>
<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>
<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>
`
)}
</div>
</ha-sortable>
<ha-entity-picker
class="add-entity"
.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> {
const value = ev.detail.value;
if (value === "") {
@ -132,14 +90,13 @@ export class HuiEntityEditor extends LitElement {
fireEvent(this, "entities-changed", { entities: newConfigEntities });
}
private _entityMoved(ev: SortableEvent): void {
if (ev.oldIndex === ev.newIndex) {
return;
}
private _entityMoved(ev: CustomEvent): void {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
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 });
}
@ -162,39 +119,36 @@ export class HuiEntityEditor extends LitElement {
}
static get styles(): CSSResultGroup {
return [
sortableStyles,
css`
ha-entity-picker {
margin-top: 8px;
}
.add-entity {
display: block;
margin-left: 31px;
margin-inline-start: 31px;
margin-inline-end: initial;
direction: var(--direction);
}
.entity {
display: flex;
align-items: center;
}
.entity .handle {
padding-right: 8px;
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
padding-inline-end: 8px;
padding-inline-start: initial;
direction: var(--direction);
}
.entity .handle > * {
pointer-events: none;
}
.entity ha-entity-picker {
flex-grow: 1;
}
`,
];
return css`
ha-entity-picker {
margin-top: 8px;
}
.add-entity {
display: block;
margin-left: 31px;
margin-inline-start: 31px;
margin-inline-end: initial;
direction: var(--direction);
}
.entity {
display: flex;
align-items: center;
}
.entity .handle {
padding-right: 8px;
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
padding-inline-end: 8px;
padding-inline-start: initial;
direction: var(--direction);
}
.entity .handle > * {
pointer-events: none;
}
.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 { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import type { SortableEvent } from "sortablejs";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/ha-button";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-list-item";
import "../../../../components/ha-sortable";
import "../../../../components/ha-svg-icon";
import {
CUSTOM_TYPE_PREFIX,
@ -18,8 +18,6 @@ import {
isCustomType,
stripCustomPrefix,
} from "../../../../data/lovelace_custom_cards";
import { sortableStyles } from "../../../../resources/ha-sortable-style";
import type { SortableInstance } from "../../../../resources/sortable";
import { HomeAssistant } from "../../../../types";
import { supportsAlarmModesCardFeature } from "../../card-features/hui-alarm-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 { supportsTargetHumidityCardFeature } from "../../card-features/hui-target-humidity-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 { supportsWaterHeaterOperationModesCardFeature } from "../../card-features/hui-water-heater-operation-modes-card-feature";
import { LovelaceCardFeatureConfig } from "../../card-features/types";
import { getCardFeatureElementClass } from "../../create-element/create-card-feature-element";
import { supportsUpdateActionsCardFeature } from "../../card-features/hui-update-actions-card-feature";
export type FeatureType = LovelaceCardFeatureConfig["type"];
type SupportsFeature = (stateObj: HassEntity) => boolean;
@ -149,13 +147,6 @@ export class HuiCardFeaturesEditor extends LitElement {
private _featuresKeys = new WeakMap<LovelaceCardFeatureConfig, string>();
private _sortable?: SortableInstance;
public disconnectedCallback() {
super.disconnectedCallback();
this._destroySortable();
}
private _supportsFeatureType(type: string): boolean {
if (!this.stateObj) return false;
@ -205,10 +196,6 @@ export class HuiCardFeaturesEditor extends LitElement {
return this._featuresKeys.get(feature)!;
}
protected firstUpdated() {
this._createSortable();
}
private _getSupportedFeaturesType() {
const featuresTypes = UI_FEATURE_TYPES.filter(
(type) => !this.featuresTypes || this.featuresTypes.includes(type)
@ -249,61 +236,66 @@ export class HuiCardFeaturesEditor extends LitElement {
</ha-alert>
`
: nothing}
<div class="features">
${repeat(
this.features,
(featureConf) => this._getKey(featureConf),
(featureConf, index) => {
const type = featureConf.type;
const supported = this._supportsFeatureType(type);
const editable = this._isFeatureTypeEditable(type);
return html`
<div class="feature">
<div class="handle">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</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}
<ha-sortable
handle-selector=".handle"
@item-moved=${this._featureMoved}
>
<div class="features">
${repeat(
this.features,
(featureConf) => this._getKey(featureConf),
(featureConf, index) => {
const type = featureConf.type;
const supported = this._supportsFeatureType(type);
const editable = this._isFeatureTypeEditable(type);
return html`
<div class="feature">
<div class="handle">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</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>
${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>
`;
}
)}
</div>
</ha-sortable>
${supportedFeaturesType.length > 0
? html`
<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> {
const index = ev.detail.index as number;
@ -395,14 +357,13 @@ export class HuiCardFeaturesEditor extends LitElement {
fireEvent(this, "features-changed", { features: newConfigFeature });
}
private _rowMoved(ev: SortableEvent): void {
if (ev.oldIndex === ev.newIndex) {
return;
}
private _featureMoved(ev: CustomEvent): void {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
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 });
}
@ -428,79 +389,76 @@ export class HuiCardFeaturesEditor extends LitElement {
}
static get styles(): CSSResultGroup {
return [
sortableStyles,
css`
:host {
display: flex !important;
flex-direction: column;
}
.content {
padding: 12px;
}
ha-expansion-panel {
display: block;
--expansion-panel-content-padding: 0;
border-radius: 6px;
}
h3 {
margin: 0;
font-size: inherit;
font-weight: inherit;
}
ha-svg-icon,
ha-icon {
color: var(--secondary-text-color);
}
ha-button-menu {
margin-top: 8px;
}
.feature {
display: flex;
align-items: center;
}
.feature .handle {
padding-right: 8px;
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
padding-inline-end: 8px;
padding-inline-start: initial;
direction: var(--direction);
}
.feature .handle > * {
pointer-events: none;
}
return css`
:host {
display: flex !important;
flex-direction: column;
}
.content {
padding: 12px;
}
ha-expansion-panel {
display: block;
--expansion-panel-content-padding: 0;
border-radius: 6px;
}
h3 {
margin: 0;
font-size: inherit;
font-weight: inherit;
}
ha-svg-icon,
ha-icon {
color: var(--secondary-text-color);
}
ha-button-menu {
margin-top: 8px;
}
.feature {
display: flex;
align-items: center;
}
.feature .handle {
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
padding-right: 8px;
padding-inline-end: 8px;
padding-inline-start: initial;
direction: var(--direction);
}
.feature .handle > * {
pointer-events: none;
}
.feature-content {
height: 60px;
font-size: 16px;
display: flex;
align-items: center;
justify-content: space-between;
flex-grow: 1;
}
.feature-content {
height: 60px;
font-size: 16px;
display: flex;
align-items: center;
justify-content: space-between;
flex-grow: 1;
}
.feature-content div {
display: flex;
flex-direction: column;
}
.feature-content div {
display: flex;
flex-direction: column;
}
.remove-icon,
.edit-icon {
--mdc-icon-button-size: 36px;
color: var(--secondary-text-color);
}
.remove-icon,
.edit-icon {
--mdc-icon-button-size: 36px;
color: var(--secondary-text-color);
}
.secondary {
font-size: 12px;
color: var(--secondary-text-color);
}
.secondary {
font-size: 12px;
color: var(--secondary-text-color);
}
li[divider] {
border-bottom-color: var(--divider-color);
}
`,
];
li[divider] {
border-bottom-color: var(--divider-color);
}
`;
}
}

View File

@ -1,15 +1,13 @@
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 { repeat } from "lit/directives/repeat";
import type { SortableEvent } from "sortablejs";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/entity/ha-entity-picker";
import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker";
import "../../../components/ha-icon-button";
import "../../../components/ha-sortable";
import "../../../components/ha-svg-icon";
import { sortableStyles } from "../../../resources/ha-sortable-style";
import type { SortableInstance } from "../../../resources/sortable";
import { HomeAssistant } from "../../../types";
import { EntityConfig, LovelaceRowConfig } from "../entity-rows/types";
@ -31,13 +29,6 @@ export class HuiEntitiesCardRowEditor extends LitElement {
private _entityKeys = new WeakMap<LovelaceRowConfig, string>();
private _sortable?: SortableInstance;
public disconnectedCallback() {
super.disconnectedCallback();
this._destroySortable();
}
private _getKey(action: LovelaceRowConfig) {
if (!this._entityKeys.has(action)) {
this._entityKeys.set(action, Math.random().toString());
@ -60,64 +51,66 @@ export class HuiEntitiesCardRowEditor extends LitElement {
"ui.panel.lovelace.editor.card.config.required"
)})`}
</h3>
<div class="entities">
${repeat(
this.entities,
(entityConf) => this._getKey(entityConf),
(entityConf, index) => html`
<div class="entity">
<div class="handle">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</div>
${entityConf.type
? html`
<div class="special-row">
<div>
<span>
${this.hass!.localize(
`ui.panel.lovelace.editor.card.entities.entity_row.${entityConf.type}`
)}
</span>
<span class="secondary"
>${this.hass!.localize(
"ui.panel.lovelace.editor.card.entities.edit_special_row"
)}</span
>
<ha-sortable handle-selector=".handle" @item-moved=${this._rowMoved}>
<div class="entities">
${repeat(
this.entities,
(entityConf) => this._getKey(entityConf),
(entityConf, index) => html`
<div class="entity">
<div class="handle">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</div>
${entityConf.type
? html`
<div class="special-row">
<div>
<span>
${this.hass!.localize(
`ui.panel.lovelace.editor.card.entities.entity_row.${entityConf.type}`
)}
</span>
<span class="secondary"
>${this.hass!.localize(
"ui.panel.lovelace.editor.card.entities.edit_special_row"
)}</span
>
</div>
</div>
</div>
`
: html`
<ha-entity-picker
allow-custom-entity
hideClearIcon
.hass=${this.hass}
.value=${(entityConf as EntityConfig).entity}
.index=${index}
@value-changed=${this._valueChanged}
></ha-entity-picker>
`}
<ha-icon-button
.label=${this.hass!.localize(
"ui.components.entity.entity-picker.clear"
)}
.path=${mdiClose}
class="remove-icon"
.index=${index}
@click=${this._removeRow}
></ha-icon-button>
<ha-icon-button
.label=${this.hass!.localize(
"ui.components.entity.entity-picker.edit"
)}
.path=${mdiPencil}
class="edit-icon"
.index=${index}
@click=${this._editRow}
></ha-icon-button>
</div>
`
)}
</div>
`
: html`
<ha-entity-picker
allow-custom-entity
hideClearIcon
.hass=${this.hass}
.value=${(entityConf as EntityConfig).entity}
.index=${index}
@value-changed=${this._valueChanged}
></ha-entity-picker>
`}
<ha-icon-button
.label=${this.hass!.localize(
"ui.components.entity.entity-picker.clear"
)}
.path=${mdiClose}
class="remove-icon"
.index=${index}
@click=${this._removeRow}
></ha-icon-button>
<ha-icon-button
.label=${this.hass!.localize(
"ui.components.entity.entity-picker.edit"
)}
.path=${mdiPencil}
class="edit-icon"
.index=${index}
@click=${this._editRow}
></ha-icon-button>
</div>
`
)}
</div>
</ha-sortable>
<ha-entity-picker
class="add-entity"
.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> {
const value = ev.detail.value;
if (value === "") {
@ -172,14 +131,13 @@ export class HuiEntitiesCardRowEditor extends LitElement {
fireEvent(this, "entities-changed", { entities: newConfigEntities });
}
private _rowMoved(ev: SortableEvent): void {
if (ev.oldIndex === ev.newIndex) {
return;
}
private _rowMoved(ev: CustomEvent): void {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
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 });
}
@ -222,67 +180,64 @@ export class HuiEntitiesCardRowEditor extends LitElement {
}
static get styles(): CSSResultGroup {
return [
sortableStyles,
css`
ha-entity-picker {
margin-top: 8px;
}
.add-entity {
display: block;
margin-left: 31px;
margin-right: 71px;
margin-inline-start: 31px;
margin-inline-end: 71px;
direction: var(--direction);
}
.entity {
display: flex;
align-items: center;
}
return css`
ha-entity-picker {
margin-top: 8px;
}
.add-entity {
display: block;
margin-left: 31px;
margin-right: 71px;
margin-inline-start: 31px;
margin-inline-end: 71px;
direction: var(--direction);
}
.entity {
display: flex;
align-items: center;
}
.entity .handle {
padding-right: 8px;
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
padding-inline-end: 8px;
padding-inline-start: initial;
direction: var(--direction);
}
.entity .handle > * {
pointer-events: none;
}
.entity .handle {
padding-right: 8px;
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
padding-inline-end: 8px;
padding-inline-start: initial;
direction: var(--direction);
}
.entity .handle > * {
pointer-events: none;
}
.entity ha-entity-picker {
flex-grow: 1;
}
.entity ha-entity-picker {
flex-grow: 1;
}
.special-row {
height: 60px;
font-size: 16px;
display: flex;
align-items: center;
justify-content: space-between;
flex-grow: 1;
}
.special-row {
height: 60px;
font-size: 16px;
display: flex;
align-items: center;
justify-content: space-between;
flex-grow: 1;
}
.special-row div {
display: flex;
flex-direction: column;
}
.special-row div {
display: flex;
flex-direction: column;
}
.remove-icon,
.edit-icon {
--mdc-icon-button-size: 36px;
color: var(--secondary-text-color);
}
.remove-icon,
.edit-icon {
--mdc-icon-button-size: 36px;
color: var(--secondary-text-color);
}
.secondary {
font-size: 12px;
color: var(--secondary-text-color);
}
`,
];
.secondary {
font-size: 12px;
color: var(--secondary-text-color);
}
`;
}
}

View File

@ -1,7 +1,7 @@
import { css } from "lit";
export const sortableStyles = css`
#sortable a:nth-of-type(2n) paper-icon-item {
export const sidebarEditStyle = css`
.reorder-list a:nth-of-type(2n) paper-icon-item {
animation-name: keyframes1;
animation-iteration-count: infinite;
transform-origin: 50% 10%;
@ -9,7 +9,7 @@ export const sortableStyles = css`
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-iteration-count: infinite;
animation-direction: alternate;
@ -18,12 +18,12 @@ export const sortableStyles = css`
animation-duration: 0.33s;
}
#sortable a {
.reorder-list a {
height: 48px;
display: flex;
}
#sortable {
.reorder-list {
outline: none;
display: block !important;
}
@ -32,26 +32,6 @@ export const sortableStyles = css`
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 {
0% {
transform: rotate(-1deg);