mirror of
https://github.com/home-assistant/frontend.git
synced 2025-04-25 13:57:21 +00:00
Use Sortable to move entities in entities editor (#6810)
This commit is contained in:
parent
d5bc498373
commit
bb2462483e
@ -79,6 +79,7 @@
|
|||||||
"@polymer/polymer": "3.1.0",
|
"@polymer/polymer": "3.1.0",
|
||||||
"@thomasloven/round-slider": "0.5.0",
|
"@thomasloven/round-slider": "0.5.0",
|
||||||
"@types/chromecast-caf-sender": "^1.0.3",
|
"@types/chromecast-caf-sender": "^1.0.3",
|
||||||
|
"@types/sortablejs": "^1.10.6",
|
||||||
"@vaadin/vaadin-combo-box": "^5.0.10",
|
"@vaadin/vaadin-combo-box": "^5.0.10",
|
||||||
"@vaadin/vaadin-date-picker": "^4.0.7",
|
"@vaadin/vaadin-date-picker": "^4.0.7",
|
||||||
"@vue/web-component-wrapper": "^1.2.0",
|
"@vue/web-component-wrapper": "^1.2.0",
|
||||||
|
@ -1,77 +0,0 @@
|
|||||||
import { html } from "lit-element";
|
|
||||||
|
|
||||||
export const sortStyles = html`
|
|
||||||
<style>
|
|
||||||
#sortable a:nth-of-type(2n) paper-icon-item {
|
|
||||||
animation-name: keyframes1;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
transform-origin: 50% 10%;
|
|
||||||
animation-delay: -0.75s;
|
|
||||||
animation-duration: 0.25s;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sortable a:nth-of-type(2n-1) paper-icon-item {
|
|
||||||
animation-name: keyframes2;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
animation-direction: alternate;
|
|
||||||
transform-origin: 30% 5%;
|
|
||||||
animation-delay: -0.5s;
|
|
||||||
animation-duration: 0.33s;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sortable {
|
|
||||||
outline: none;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sortable-ghost {
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sortable-fallback {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes keyframes1 {
|
|
||||||
0% {
|
|
||||||
transform: rotate(-1deg);
|
|
||||||
animation-timing-function: ease-in;
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
transform: rotate(1.5deg);
|
|
||||||
animation-timing-function: ease-out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes keyframes2 {
|
|
||||||
0% {
|
|
||||||
transform: rotate(1deg);
|
|
||||||
animation-timing-function: ease-in;
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
transform: rotate(-1.5deg);
|
|
||||||
animation-timing-function: ease-out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hide-panel {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([expanded]) .hide-panel {
|
|
||||||
display: inline-flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
paper-icon-item.hidden-panel,
|
|
||||||
paper-icon-item.hidden-panel span,
|
|
||||||
paper-icon-item.hidden-panel ha-icon[slot="item-icon"] {
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
`;
|
|
@ -23,7 +23,6 @@ import {
|
|||||||
LitElement,
|
LitElement,
|
||||||
property,
|
property,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import { classMap } from "lit-html/directives/class-map";
|
import { classMap } from "lit-html/directives/class-map";
|
||||||
import { guard } from "lit-html/directives/guard";
|
import { guard } from "lit-html/directives/guard";
|
||||||
@ -161,7 +160,7 @@ const computePanels = memoizeOne(
|
|||||||
|
|
||||||
let Sortable;
|
let Sortable;
|
||||||
|
|
||||||
let sortStyles: TemplateResult;
|
let sortStyles: CSSResult;
|
||||||
|
|
||||||
@customElement("ha-sidebar")
|
@customElement("ha-sidebar")
|
||||||
class HaSidebar extends LitElement {
|
class HaSidebar extends LitElement {
|
||||||
@ -229,7 +228,13 @@ class HaSidebar extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${this._editMode ? sortStyles : ""}
|
${this._editMode
|
||||||
|
? html`
|
||||||
|
<style>
|
||||||
|
${sortStyles?.cssText}
|
||||||
|
</style>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
${!this.narrow
|
${!this.narrow
|
||||||
? html`
|
? html`
|
||||||
@ -481,10 +486,10 @@ class HaSidebar extends LitElement {
|
|||||||
if (!Sortable) {
|
if (!Sortable) {
|
||||||
const [sortableImport, sortStylesImport] = await Promise.all([
|
const [sortableImport, sortStylesImport] = await Promise.all([
|
||||||
import("sortablejs/modular/sortable.core.esm"),
|
import("sortablejs/modular/sortable.core.esm"),
|
||||||
import("./ha-sidebar-sort-styles"),
|
import("../resources/ha-sortable-style"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
sortStyles = sortStylesImport.sortStyles;
|
sortStyles = sortStylesImport.sortableStyles;
|
||||||
|
|
||||||
Sortable = sortableImport.Sortable;
|
Sortable = sortableImport.Sortable;
|
||||||
Sortable.mount(sortableImport.OnSpill);
|
Sortable.mount(sortableImport.OnSpill);
|
||||||
|
@ -1,27 +1,51 @@
|
|||||||
import "../../../components/ha-icon-button";
|
import { mdiDrag } from "@mdi/js";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResult,
|
CSSResult,
|
||||||
customElement,
|
customElement,
|
||||||
html,
|
html,
|
||||||
|
internalProperty,
|
||||||
LitElement,
|
LitElement,
|
||||||
property,
|
property,
|
||||||
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
|
import { guard } from "lit-html/directives/guard";
|
||||||
|
import type { SortableEvent } from "sortablejs";
|
||||||
|
import Sortable, {
|
||||||
|
AutoScroll,
|
||||||
|
OnSpill,
|
||||||
|
} from "sortablejs/modular/sortable.core.esm";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import "../../../components/entity/ha-entity-picker";
|
import "../../../components/entity/ha-entity-picker";
|
||||||
|
import "../../../components/ha-icon-button";
|
||||||
|
import { sortableStyles } from "../../../resources/ha-sortable-style";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { EditorTarget } from "../editor/types";
|
import { EditorTarget } from "../editor/types";
|
||||||
import { EntityConfig } from "../entity-rows/types";
|
import { EntityConfig } from "../entity-rows/types";
|
||||||
|
|
||||||
@customElement("hui-entity-editor")
|
@customElement("hui-entity-editor")
|
||||||
export class HuiEntityEditor extends LitElement {
|
export class HuiEntityEditor extends LitElement {
|
||||||
@property() protected hass?: HomeAssistant;
|
@property({ attribute: false }) protected hass?: HomeAssistant;
|
||||||
|
|
||||||
@property() protected entities?: EntityConfig[];
|
@property({ attribute: false }) protected entities?: EntityConfig[];
|
||||||
|
|
||||||
@property() protected label?: string;
|
@property() protected label?: string;
|
||||||
|
|
||||||
|
@internalProperty() private _attached = false;
|
||||||
|
|
||||||
|
private _sortable?;
|
||||||
|
|
||||||
|
public connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._attached = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._attached = false;
|
||||||
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this.entities) {
|
if (!this.entities) {
|
||||||
return html``;
|
return html``;
|
||||||
@ -36,42 +60,73 @@ export class HuiEntityEditor extends LitElement {
|
|||||||
")"}
|
")"}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="entities">
|
<div class="entities">
|
||||||
${this.entities.map((entityConf, index) => {
|
${guard([this.entities], () =>
|
||||||
return html`
|
this.entities!.map((entityConf, index) => {
|
||||||
<div class="entity">
|
return html`
|
||||||
<ha-entity-picker
|
<div class="entity" data-entity-id=${entityConf.entity}>
|
||||||
.hass=${this.hass}
|
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
||||||
.value="${entityConf.entity}"
|
<ha-entity-picker
|
||||||
.index="${index}"
|
.hass=${this.hass}
|
||||||
@change="${this._valueChanged}"
|
.value=${entityConf.entity}
|
||||||
allow-custom-entity
|
.index=${index}
|
||||||
></ha-entity-picker>
|
@change=${this._valueChanged}
|
||||||
<ha-icon-button
|
allow-custom-entity
|
||||||
title="Move entity down"
|
></ha-entity-picker>
|
||||||
icon="hass:arrow-down"
|
</div>
|
||||||
.index="${index}"
|
`;
|
||||||
@click="${this._entityDown}"
|
})
|
||||||
?disabled="${index === this.entities!.length - 1}"
|
)}
|
||||||
></ha-icon-button>
|
|
||||||
<ha-icon-button
|
|
||||||
title="Move entity up"
|
|
||||||
icon="hass:arrow-up"
|
|
||||||
.index="${index}"
|
|
||||||
@click="${this._entityUp}"
|
|
||||||
?disabled="${index === 0}"
|
|
||||||
></ha-icon-button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
})}
|
|
||||||
<ha-entity-picker
|
|
||||||
.hass=${this.hass}
|
|
||||||
@change="${this._addEntity}"
|
|
||||||
></ha-entity-picker>
|
|
||||||
</div>
|
</div>
|
||||||
|
<ha-entity-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
@change=${this._addEntity}
|
||||||
|
></ha-entity-picker>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _addEntity(ev: Event): void {
|
protected firstUpdated(): void {
|
||||||
|
Sortable.mount(OnSpill);
|
||||||
|
Sortable.mount(new AutoScroll());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues): void {
|
||||||
|
super.updated(changedProps);
|
||||||
|
|
||||||
|
const attachedChanged = changedProps.has("_attached");
|
||||||
|
const entitiesChanged = changedProps.has("entities");
|
||||||
|
|
||||||
|
if (!entitiesChanged && !attachedChanged) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attachedChanged && !this._attached) {
|
||||||
|
// Tear down sortable, if available
|
||||||
|
this._sortable?.destroy();
|
||||||
|
this._sortable = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._sortable && this.entities) {
|
||||||
|
this._createSortable();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entitiesChanged) {
|
||||||
|
this._sortable.sort(this.entities?.map((entity) => entity.entity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _createSortable() {
|
||||||
|
this._sortable = new Sortable(this.shadowRoot!.querySelector(".entities"), {
|
||||||
|
animation: 150,
|
||||||
|
fallbackClass: "sortable-fallback",
|
||||||
|
handle: "ha-svg-icon",
|
||||||
|
dataIdAttr: "data-entity-id",
|
||||||
|
onEnd: async (evt: SortableEvent) => this._entityMoved(evt),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _addEntity(ev: Event): Promise<void> {
|
||||||
const target = ev.target! as EditorTarget;
|
const target = ev.target! as EditorTarget;
|
||||||
if (target.value === "") {
|
if (target.value === "") {
|
||||||
return;
|
return;
|
||||||
@ -83,26 +138,14 @@ export class HuiEntityEditor extends LitElement {
|
|||||||
fireEvent(this, "entities-changed", { entities: newConfigEntities });
|
fireEvent(this, "entities-changed", { entities: newConfigEntities });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _entityUp(ev: Event): void {
|
private _entityMoved(ev: SortableEvent): void {
|
||||||
const target = ev.target! as EditorTarget;
|
if (ev.oldIndex === ev.newIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const newEntities = this.entities!.concat();
|
const newEntities = this.entities!.concat();
|
||||||
|
|
||||||
[newEntities[target.index! - 1], newEntities[target.index!]] = [
|
newEntities.splice(ev.newIndex!, 0, newEntities.splice(ev.oldIndex!, 1)[0]);
|
||||||
newEntities[target.index!],
|
|
||||||
newEntities[target.index! - 1],
|
|
||||||
];
|
|
||||||
|
|
||||||
fireEvent(this, "entities-changed", { entities: newEntities });
|
|
||||||
}
|
|
||||||
|
|
||||||
private _entityDown(ev: Event): void {
|
|
||||||
const target = ev.target! as EditorTarget;
|
|
||||||
const newEntities = this.entities!.concat();
|
|
||||||
|
|
||||||
[newEntities[target.index! + 1], newEntities[target.index!]] = [
|
|
||||||
newEntities[target.index!],
|
|
||||||
newEntities[target.index! + 1],
|
|
||||||
];
|
|
||||||
|
|
||||||
fireEvent(this, "entities-changed", { entities: newEntities });
|
fireEvent(this, "entities-changed", { entities: newEntities });
|
||||||
}
|
}
|
||||||
@ -123,16 +166,23 @@ export class HuiEntityEditor extends LitElement {
|
|||||||
fireEvent(this, "entities-changed", { entities: newConfigEntities });
|
fireEvent(this, "entities-changed", { entities: newConfigEntities });
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResult {
|
static get styles(): CSSResult[] {
|
||||||
return css`
|
return [
|
||||||
.entity {
|
sortableStyles,
|
||||||
display: flex;
|
css`
|
||||||
align-items: flex-end;
|
.entity {
|
||||||
}
|
display: flex;
|
||||||
.entity ha-entity-picker {
|
align-items: center;
|
||||||
flex-grow: 1;
|
}
|
||||||
}
|
.entity ha-svg-icon {
|
||||||
`;
|
padding-right: 8px;
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
.entity ha-entity-picker {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
75
src/resources/ha-sortable-style.ts
Normal file
75
src/resources/ha-sortable-style.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { css } from "lit-element";
|
||||||
|
|
||||||
|
export const sortableStyles = css`
|
||||||
|
#sortable a:nth-of-type(2n) paper-icon-item {
|
||||||
|
animation-name: keyframes1;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
transform-origin: 50% 10%;
|
||||||
|
animation-delay: -0.75s;
|
||||||
|
animation-duration: 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sortable a:nth-of-type(2n-1) paper-icon-item {
|
||||||
|
animation-name: keyframes2;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-direction: alternate;
|
||||||
|
transform-origin: 30% 5%;
|
||||||
|
animation-delay: -0.5s;
|
||||||
|
animation-duration: 0.33s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sortable {
|
||||||
|
outline: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable-ghost {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable-fallback {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes keyframes1 {
|
||||||
|
0% {
|
||||||
|
transform: rotate(-1deg);
|
||||||
|
animation-timing-function: ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: rotate(1.5deg);
|
||||||
|
animation-timing-function: ease-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes keyframes2 {
|
||||||
|
0% {
|
||||||
|
transform: rotate(1deg);
|
||||||
|
animation-timing-function: ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: rotate(-1.5deg);
|
||||||
|
animation-timing-function: ease-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-panel {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([expanded]) .hide-panel {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
paper-icon-item.hidden-panel,
|
||||||
|
paper-icon-item.hidden-panel span,
|
||||||
|
paper-icon-item.hidden-panel ha-icon[slot="item-icon"] {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
`;
|
@ -2710,6 +2710,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/sortablejs@^1.10.6":
|
||||||
|
version "1.10.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/sortablejs/-/sortablejs-1.10.6.tgz#98725ae08f1dfe28b8da0fdf302c417f5ff043c0"
|
||||||
|
integrity sha512-QRz8Z+uw2Y4Gwrtxw8hD782zzuxxugdcq8X/FkPsXUa1kfslhGzy13+4HugO9FXNo+jlWVcE6DYmmegniIQ30A==
|
||||||
|
|
||||||
"@types/tern@*":
|
"@types/tern@*":
|
||||||
version "0.23.3"
|
version "0.23.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/tern/-/tern-0.23.3.tgz#4b54538f04a88c9ff79de1f6f94f575a7f339460"
|
resolved "https://registry.yarnpkg.com/@types/tern/-/tern-0.23.3.tgz#4b54538f04a88c9ff79de1f6f94f575a7f339460"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user