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",
"@thomasloven/round-slider": "0.5.0",
"@types/chromecast-caf-sender": "^1.0.3",
"@types/sortablejs": "^1.10.6",
"@vaadin/vaadin-combo-box": "^5.0.10",
"@vaadin/vaadin-date-picker": "^4.0.7",
"@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,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { guard } from "lit-html/directives/guard";
@ -161,7 +160,7 @@ const computePanels = memoizeOne(
let Sortable;
let sortStyles: TemplateResult;
let sortStyles: CSSResult;
@customElement("ha-sidebar")
class HaSidebar extends LitElement {
@ -229,7 +228,13 @@ class HaSidebar extends LitElement {
}
return html`
${this._editMode ? sortStyles : ""}
${this._editMode
? html`
<style>
${sortStyles?.cssText}
</style>
`
: ""}
<div class="menu">
${!this.narrow
? html`
@ -481,10 +486,10 @@ class HaSidebar extends LitElement {
if (!Sortable) {
const [sortableImport, sortStylesImport] = await Promise.all([
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.mount(sortableImport.OnSpill);

View File

@ -1,27 +1,51 @@
import "../../../components/ha-icon-button";
import { mdiDrag } from "@mdi/js";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
TemplateResult,
} 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 "../../../components/entity/ha-entity-picker";
import "../../../components/ha-icon-button";
import { sortableStyles } from "../../../resources/ha-sortable-style";
import { HomeAssistant } from "../../../types";
import { EditorTarget } from "../editor/types";
import { EntityConfig } from "../entity-rows/types";
@customElement("hui-entity-editor")
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;
@internalProperty() private _attached = false;
private _sortable?;
public connectedCallback() {
super.connectedCallback();
this._attached = true;
}
public disconnectedCallback() {
super.disconnectedCallback();
this._attached = false;
}
protected render(): TemplateResult {
if (!this.entities) {
return html``;
@ -36,42 +60,73 @@ export class HuiEntityEditor extends LitElement {
")"}
</h3>
<div class="entities">
${this.entities.map((entityConf, index) => {
return html`
<div class="entity">
<ha-entity-picker
.hass=${this.hass}
.value="${entityConf.entity}"
.index="${index}"
@change="${this._valueChanged}"
allow-custom-entity
></ha-entity-picker>
<ha-icon-button
title="Move entity down"
icon="hass:arrow-down"
.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>
${guard([this.entities], () =>
this.entities!.map((entityConf, index) => {
return html`
<div class="entity" data-entity-id=${entityConf.entity}>
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
<ha-entity-picker
.hass=${this.hass}
.value=${entityConf.entity}
.index=${index}
@change=${this._valueChanged}
allow-custom-entity
></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;
if (target.value === "") {
return;
@ -83,26 +138,14 @@ export class HuiEntityEditor extends LitElement {
fireEvent(this, "entities-changed", { entities: newConfigEntities });
}
private _entityUp(ev: Event): void {
const target = ev.target! as EditorTarget;
private _entityMoved(ev: SortableEvent): void {
if (ev.oldIndex === ev.newIndex) {
return;
}
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 });
}
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],
];
newEntities.splice(ev.newIndex!, 0, newEntities.splice(ev.oldIndex!, 1)[0]);
fireEvent(this, "entities-changed", { entities: newEntities });
}
@ -123,16 +166,23 @@ export class HuiEntityEditor extends LitElement {
fireEvent(this, "entities-changed", { entities: newConfigEntities });
}
static get styles(): CSSResult {
return css`
.entity {
display: flex;
align-items: flex-end;
}
.entity ha-entity-picker {
flex-grow: 1;
}
`;
static get styles(): CSSResult[] {
return [
sortableStyles,
css`
.entity {
display: flex;
align-items: center;
}
.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:
"@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@*":
version "0.23.3"
resolved "https://registry.yarnpkg.com/@types/tern/-/tern-0.23.3.tgz#4b54538f04a88c9ff79de1f6f94f575a7f339460"