Skip to content

Instantly share code, notes, and snippets.

@JonahKr
Created December 1, 2020 23:26
Show Gist options
  • Save JonahKr/f29d7b3e65c6707313ab4d421be9c8e7 to your computer and use it in GitHub Desktop.
Save JonahKr/f29d7b3e65c6707313ab4d421be9c8e7 to your computer and use it in GitHub Desktop.
HASS - Entities Row Editor using sortablejs
import { fireEvent, HomeAssistant, LovelaceCardEditor } from 'custom-card-helpers';
import { css, CSSResult, html, internalProperty, LitElement, property, TemplateResult } from 'lit-element';
import { guard } from 'lit-html/directives/guard';
import Sortable, { AutoScroll, OnSpill, SortableEvent } from 'sortablejs/modular/sortable.core.esm';
interface EREConfig {
wonderfullsetting?: string;
}
/**
* Entities Row Editor
* -------------------
* This Row Editor is based on the hui-entities-card-row-editor in homeassistant. (Thanks Zack for your help)
* If you are interested in using the Editor for your own card, i tried explaining everything with incode documentation
*/
export class EntitiesRowEditor extends LitElement implements LovelaceCardEditor {
@property({ attribute: false }) public hass?: HomeAssistant;
@internalProperty() private _config!: EREConfig;
private _entities = [];
/**
* These are needed for functionality of the DragnDrop
*/
@internalProperty() private _renderEmptySortable = false;
private _sortable?: Sortable;
@internalProperty() private _attached = false;
public setConfig(config: EREConfig): void {
this._config = config;
}
/**
* Generator for all entities in the config.entities list
* The Guard Function prevents unnecessary rendering
* @returns HTML for the Entities Editor
*/
private _render(): TemplateResult {
return html`
<h3>
Entities (Required)
</h3>
<div class="entities">
${guard([this._entities, this._renderEmptySortable], () =>
this._renderEmptySortable
? ''
: this._entities.map((entity, index) => {
return html`
<div class="entity">
<ha-icon class="handle" icon="mdi:drag"></ha-icon>
<ha-entity-picker
label="Entity"
allow-custom-entity
hideClearIcon
.hass=${this.hass}
.configValue=${'entity'}
.value=${entity}
.index=${index}
@value-changed=${this._valueChanged}
></ha-entity-picker>
<mwc-icon-button
aria-label="Remove"
class="remove-icon"
.index=${index}
@click=${this._removeRow}
>
<ha-icon icon="mdi:close"></ha-icon>
</mwc-icon-button>
<mwc-icon-button aria-label="edit" class="edit-icon" .index=${index} @click="${this._editRow}">
<ha-icon icon="mdi:pencil"></ha-icon>
</mwc-icon-button>
</div>
`;
}),
)}
</div>
</div>
<ha-entity-picker
.hass=${this.hass}
@value-changed=${this._addEntity}
></ha-entity-picker>
`;
}
private _valueChanged(ev): void {
if (!this._config || !this.hass) {
return;
}
if (ev.target) {
const target = ev.target;
if (target.configValue) {
if (target.value === '') {
delete this._config[target.configValue];
} else {
this._config = {
...this._config,
[target.configValue]: target.checked !== undefined ? target.checked : target.value,
};
}
}
}
fireEvent(this, 'config-changed', { config: this._config });
}
public connectedCallback(): void {
super.connectedCallback();
this._attached = true;
}
public disconnectedCallback(): void {
super.disconnectedCallback();
this._attached = false;
}
protected firstUpdated(): void {
Sortable.mount(OnSpill);
Sortable.mount(new AutoScroll());
}
/**
* This is for Checking if something relevant has changed and updating variables accordingly
* @param changedProps The Changed Property Values
*/
protected updated(changedProps): void {
super.updated(changedProps);
const attachedChanged = changedProps.has('_attached');
const entitiesChanged = changedProps.has('_config');
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;
}
/**TODO:
* If your gonna do Subeditors for each entity for example you will need a AND expression here like : && this._subElementEditor == undefined
*/
if (entitiesChanged) {
this._handleEntitiesChanged();
}
}
/**
* Since we have the guard function enabled to prevent unecessary renders, we need to handle switched rows seperately.
*/
private async _handleEntitiesChanged(): Promise<void> {
this._renderEmptySortable = true;
await this.updateComplete;
const container = this.shadowRoot?.querySelector('.entities') as HTMLElement;
while (container.lastElementChild) {
container.removeChild(container.lastElementChild);
}
this._renderEmptySortable = false;
}
/**
* Creating the Sortable Element (https://github.com/SortableJS/sortablejs) used as a foundation
*/
private _createSortable(): void {
const element = this.shadowRoot?.querySelector('.entities') as HTMLElement;
if (!element) return;
this._sortable = new Sortable(element, {
animation: 150,
fallbackClass: 'sortable-fallback',
handle: '.handle',
onEnd: async (evt: SortableEvent) => this._rowMoved(evt),
});
}
/**
* If you add an entity it needs to be appended to the Configuration!
* In this particular Case the Entity Generation is a bit more complicated and involves Presets
*/
private async _addEntity(ev): Promise<void> {
const value: string = ev.detail.value;
if (value === '') {
return;
}
const newEntities = this._entities.concat({
entity: value,
});
(ev.target as any).value = '';
//This basically fakes a event object
this._valueChanged({ target: { configValue: 'entities', value: newEntities } });
}
/**
* Handeling if the User drags elements to a different position in the list.
* @param ev Event containing old index, new index
*/
private _rowMoved(ev: SortableEvent): void {
if (ev.oldIndex === ev.newIndex) return;
const newEntities = [...this._entities];
newEntities.splice(ev.newIndex, 0, newEntities.splice(ev.oldIndex, 1)[0]);
this._valueChanged({ target: { configValue: 'entities', value: newEntities } });
}
/**
* When the Row is removed:
* @param ev Event containing a Target to remove
*/
private _removeRow(ev): void {
const index = ev.currentTarget?.index || 0;
const newEntities = [...this._entities];
newEntities.splice(index, 1);
this._valueChanged({ target: { configValue: 'entities', value: newEntities } });
}
/**
* When the Row is edited:
* @param ev Event containing a Target to remove
*/
private _editRow(ev): void {
const index = ev.currentTarget?.index || 0;
/**
* TODO: From here you can execute code by pressing the edit button
*/
}
/**
* The Second Part comes from here: https://github.com/home-assistant/frontend/blob/dev/src/resources/ha-sortable-style.ts
* @returns Editor CSS
*/
static get styles(): CSSResult[] {
return [
css`
.side-by-side {
display: flex;
}
.side-by-side > * {
flex: 1 1 0%;
padding-right: 4px;
}
.entity {
display: flex;
align-items: center;
}
.entity .handle {
padding-right: 8px;
cursor: move;
}
.entity ha-entity-picker {
flex-grow: 1;
}
.add-preset {
padding-right: 8px;
max-width: 130px;
}
.remove-icon,
.edit-icon,
.add-icon {
--mdc-icon-button-size: 36px;
color: var(--secondary-text-color);
}
.secondary {
font-size: 12px;
color: var(--secondary-text-color);
}
`,
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 a {
height: 48px;
display: flex;
}
#sortable {
outline: none;
display: block !important;
}
.hidden-panel {
display: flex !important;
}
.sortable-fallback {
display: none;
}
.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;
}
}
.show-panel,
.hide-panel {
display: none;
position: absolute;
top: 0;
right: 4px;
--mdc-icon-button-size: 40px;
}
:host([rtl]) .show-panel {
right: initial;
left: 4px;
}
.hide-panel {
top: 4px;
right: 8px;
}
:host([rtl]) .hide-panel {
right: initial;
left: 8px;
}
:host([expanded]) .hide-panel {
display: block;
}
:host([expanded]) .show-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;
}
`,
];
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment