Created
December 1, 2020 23:26
-
-
Save JonahKr/f29d7b3e65c6707313ab4d421be9c8e7 to your computer and use it in GitHub Desktop.
HASS - Entities Row Editor using sortablejs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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