Use Sortable to move entities in entities editor (#6810)

This commit is contained in:
Zack Barett 2020-09-07 06:47:24 -05:00 committed by GitHub
parent d5bc498373
commit bb2462483e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 204 additions and 145 deletions

View File

@ -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",

View File

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

View File

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

View File

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

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

View File

@ -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"