Added Drag & Drop Reordering to Shopping List Card. (#7296)

This commit is contained in:
Shane Qi 2021-01-05 04:24:41 -06:00 committed by GitHub
parent 2fdc746392
commit 5e2ee1a16c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 141 additions and 26 deletions

View File

@ -38,3 +38,12 @@ export const addItem = (
type: "shopping_list/items/add", type: "shopping_list/items/add",
name, name,
}); });
export const reorderItems = (
hass: HomeAssistant,
itemIds: [string]
): Promise<ShoppingListItem> =>
hass.callWS({
type: "shopping_list/items/reorder",
item_ids: itemIds,
});

View File

@ -11,9 +11,12 @@ import {
property, property,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
query,
} from "lit-element"; } from "lit-element";
import { classMap } from "lit-html/directives/class-map"; import { classMap } from "lit-html/directives/class-map";
import { repeat } from "lit-html/directives/repeat"; import { repeat } from "lit-html/directives/repeat";
import { guard } from "lit-html/directives/guard";
import { mdiDrag, mdiSort, mdiPlus, mdiNotificationClearAll } from "@mdi/js";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-icon"; import "../../../components/ha-icon";
@ -23,12 +26,15 @@ import {
fetchItems, fetchItems,
ShoppingListItem, ShoppingListItem,
updateItem, updateItem,
reorderItems,
} from "../../../data/shopping-list"; } from "../../../data/shopping-list";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { LovelaceCard, LovelaceCardEditor } from "../types"; import { LovelaceCard, LovelaceCardEditor } from "../types";
import { SensorCardConfig, ShoppingListCardConfig } from "./types"; import { SensorCardConfig, ShoppingListCardConfig } from "./types";
let Sortable;
@customElement("hui-shopping-list-card") @customElement("hui-shopping-list-card")
class HuiShoppingListCard extends SubscribeMixin(LitElement) class HuiShoppingListCard extends SubscribeMixin(LitElement)
implements LovelaceCard { implements LovelaceCard {
@ -49,6 +55,14 @@ class HuiShoppingListCard extends SubscribeMixin(LitElement)
@internalProperty() private _checkedItems?: ShoppingListItem[]; @internalProperty() private _checkedItems?: ShoppingListItem[];
@internalProperty() private _reordering = false;
@internalProperty() private _renderEmptySortable = false;
private _sortable?;
@query("#sortable") private _sortableEl?: HTMLElement;
public getCardSize(): number { public getCardSize(): number {
return (this._config ? (this._config.title ? 2 : 0) : 0) + 3; return (this._config ? (this._config.title ? 2 : 0) : 0) + 3;
} }
@ -101,15 +115,15 @@ class HuiShoppingListCard extends SubscribeMixin(LitElement)
})} })}
> >
<div class="addRow"> <div class="addRow">
<ha-icon <ha-svg-icon
class="addButton" class="addButton"
icon="hass:plus" .path=${mdiPlus}
.title=${this.hass!.localize( .title=${this.hass!.localize(
"ui.panel.lovelace.cards.shopping-list.add_item" "ui.panel.lovelace.cards.shopping-list.add_item"
)} )}
@click=${this._addItem} @click=${this._addItem}
> >
</ha-icon> </ha-svg-icon>
<paper-input <paper-input
no-label-float no-label-float
class="addBox" class="addBox"
@ -118,28 +132,27 @@ class HuiShoppingListCard extends SubscribeMixin(LitElement)
)} )}
@keydown=${this._addKeyPress} @keydown=${this._addKeyPress}
></paper-input> ></paper-input>
<ha-svg-icon
class="reorderButton"
.path=${mdiSort}
.title=${this.hass!.localize(
"ui.panel.lovelace.cards.shopping-list.reorder_items"
)}
@click=${this._toggleReorder}
>
</ha-svg-icon>
</div> </div>
${repeat( ${this._reordering
this._uncheckedItems!, ? html`
(item) => item.id, <div id="sortable">
(item) => ${guard([this._uncheckedItems, this._renderEmptySortable], () =>
html` this._renderEmptySortable
<div class="editRow"> ? ""
<paper-checkbox : this._renderItems(this._uncheckedItems!)
tabindex="0" )}
?checked=${item.complete}
.itemId=${item.id}
@click=${this._completeItem}
></paper-checkbox>
<paper-input
no-label-float
.value=${item.name}
.itemId=${item.id}
@change=${this._saveEdit}
></paper-input>
</div> </div>
` `
)} : this._renderItems(this._uncheckedItems!)}
${this._checkedItems!.length > 0 ${this._checkedItems!.length > 0
? html` ? html`
<div class="divider"></div> <div class="divider"></div>
@ -149,16 +162,16 @@ class HuiShoppingListCard extends SubscribeMixin(LitElement)
"ui.panel.lovelace.cards.shopping-list.checked_items" "ui.panel.lovelace.cards.shopping-list.checked_items"
)} )}
</span> </span>
<ha-icon <ha-svg-icon
class="clearall" class="clearall"
tabindex="0" tabindex="0"
icon="hass:notification-clear-all" .path=${mdiNotificationClearAll}
.title=${this.hass!.localize( .title=${this.hass!.localize(
"ui.panel.lovelace.cards.shopping-list.clear_items" "ui.panel.lovelace.cards.shopping-list.clear_items"
)} )}
@click=${this._clearItems} @click=${this._clearItems}
> >
</ha-icon> </ha-svg-icon>
</div> </div>
${repeat( ${repeat(
this._checkedItems!, this._checkedItems!,
@ -187,6 +200,44 @@ class HuiShoppingListCard extends SubscribeMixin(LitElement)
`; `;
} }
private _renderItems(items: ShoppingListItem[]) {
return html`
${repeat(
items,
(item) => item.id,
(item) =>
html`
<div class="editRow" item-id=${item.id}>
<paper-checkbox
tabindex="0"
?checked=${item.complete}
.itemId=${item.id}
@click=${this._completeItem}
></paper-checkbox>
<paper-input
no-label-float
.value=${item.name}
.itemId=${item.id}
@change=${this._saveEdit}
></paper-input>
${this._reordering
? html`
<ha-svg-icon
.title=${this.hass!.localize(
"ui.panel.lovelace.cards.shopping-list.drag_and_drop"
)}
class="reorderButton"
.path=${mdiDrag}
>
</ha-svg-icon>
`
: ""}
</div>
`
)}
`;
}
private async _fetchData(): Promise<void> { private async _fetchData(): Promise<void> {
if (!this.hass) { if (!this.hass) {
return; return;
@ -248,6 +299,54 @@ class HuiShoppingListCard extends SubscribeMixin(LitElement)
} }
} }
private async _toggleReorder() {
if (!Sortable) {
const sortableImport = await import(
"sortablejs/modular/sortable.core.esm"
);
Sortable = sortableImport.Sortable;
}
this._reordering = !this._reordering;
await this.updateComplete;
if (this._reordering) {
this._createSortable();
} else {
this._sortable?.destroy();
this._sortable = undefined;
}
}
private _createSortable() {
const sortableEl = this._sortableEl;
this._sortable = new Sortable(sortableEl, {
animation: 150,
fallbackClass: "sortable-fallback",
dataIdAttr: "item-id",
handle: "ha-svg-icon",
onEnd: async (evt) => {
// Since this is `onEnd` event, it's possible that
// an item wa dragged away and was put back to its original position.
if (evt.oldIndex !== evt.newIndex) {
reorderItems(this.hass!, this._sortable.toArray()).catch(() =>
this._fetchData()
);
// Move the shopping list item in memory.
this._uncheckedItems!.splice(
evt.newIndex,
0,
this._uncheckedItems!.splice(evt.oldIndex, 1)[0]
);
}
this._renderEmptySortable = true;
await this.updateComplete;
while (sortableEl?.lastElementChild) {
sortableEl.removeChild(sortableEl.lastElementChild);
}
this._renderEmptySortable = false;
},
});
}
static get styles(): CSSResult { static get styles(): CSSResult {
return css` return css`
ha-card { ha-card {
@ -278,6 +377,11 @@ class HuiShoppingListCard extends SubscribeMixin(LitElement)
cursor: pointer; cursor: pointer;
} }
.reorderButton {
padding-left: 16px;
cursor: pointer;
}
paper-checkbox { paper-checkbox {
padding-left: 4px; padding-left: 4px;
padding-right: 20px; padding-right: 20px;

View File

@ -2401,7 +2401,9 @@
"shopping-list": { "shopping-list": {
"checked_items": "Checked items", "checked_items": "Checked items",
"clear_items": "Clear checked items", "clear_items": "Clear checked items",
"add_item": "Add item" "add_item": "Add item",
"reorder_items": "Reorder items",
"drag_and_drop": "Drag and drop"
}, },
"picture-elements": { "picture-elements": {
"hold": "Hold:", "hold": "Hold:",