From 5e2ee1a16cf570b933a3aacc499433866fdaa93d Mon Sep 17 00:00:00 2001 From: Shane Qi Date: Tue, 5 Jan 2021 04:24:41 -0600 Subject: [PATCH] Added Drag & Drop Reordering to Shopping List Card. (#7296) --- src/data/shopping-list.ts | 9 + .../lovelace/cards/hui-shopping-list-card.ts | 154 +++++++++++++++--- src/translations/en.json | 4 +- 3 files changed, 141 insertions(+), 26 deletions(-) diff --git a/src/data/shopping-list.ts b/src/data/shopping-list.ts index d3dbdd8327..1036cde3f1 100644 --- a/src/data/shopping-list.ts +++ b/src/data/shopping-list.ts @@ -38,3 +38,12 @@ export const addItem = ( type: "shopping_list/items/add", name, }); + +export const reorderItems = ( + hass: HomeAssistant, + itemIds: [string] +): Promise => + hass.callWS({ + type: "shopping_list/items/reorder", + item_ids: itemIds, + }); diff --git a/src/panels/lovelace/cards/hui-shopping-list-card.ts b/src/panels/lovelace/cards/hui-shopping-list-card.ts index 88132f288c..921b6fdcd5 100644 --- a/src/panels/lovelace/cards/hui-shopping-list-card.ts +++ b/src/panels/lovelace/cards/hui-shopping-list-card.ts @@ -11,9 +11,12 @@ import { property, PropertyValues, TemplateResult, + query, } from "lit-element"; import { classMap } from "lit-html/directives/class-map"; 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 "../../../components/ha-card"; import "../../../components/ha-icon"; @@ -23,12 +26,15 @@ import { fetchItems, ShoppingListItem, updateItem, + reorderItems, } from "../../../data/shopping-list"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../../types"; import { LovelaceCard, LovelaceCardEditor } from "../types"; import { SensorCardConfig, ShoppingListCardConfig } from "./types"; +let Sortable; + @customElement("hui-shopping-list-card") class HuiShoppingListCard extends SubscribeMixin(LitElement) implements LovelaceCard { @@ -49,6 +55,14 @@ class HuiShoppingListCard extends SubscribeMixin(LitElement) @internalProperty() private _checkedItems?: ShoppingListItem[]; + @internalProperty() private _reordering = false; + + @internalProperty() private _renderEmptySortable = false; + + private _sortable?; + + @query("#sortable") private _sortableEl?: HTMLElement; + public getCardSize(): number { return (this._config ? (this._config.title ? 2 : 0) : 0) + 3; } @@ -101,15 +115,15 @@ class HuiShoppingListCard extends SubscribeMixin(LitElement) })} >
- - + + +
- ${repeat( - this._uncheckedItems!, - (item) => item.id, - (item) => - html` -
- - + ${this._reordering + ? html` +
+ ${guard([this._uncheckedItems, this._renderEmptySortable], () => + this._renderEmptySortable + ? "" + : this._renderItems(this._uncheckedItems!) + )}
` - )} + : this._renderItems(this._uncheckedItems!)} ${this._checkedItems!.length > 0 ? html`
@@ -149,16 +162,16 @@ class HuiShoppingListCard extends SubscribeMixin(LitElement) "ui.panel.lovelace.cards.shopping-list.checked_items" )} - - +
${repeat( this._checkedItems!, @@ -187,6 +200,44 @@ class HuiShoppingListCard extends SubscribeMixin(LitElement) `; } + private _renderItems(items: ShoppingListItem[]) { + return html` + ${repeat( + items, + (item) => item.id, + (item) => + html` +
+ + + ${this._reordering + ? html` + + + ` + : ""} +
+ ` + )} + `; + } + private async _fetchData(): Promise { if (!this.hass) { 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 { return css` ha-card { @@ -278,6 +377,11 @@ class HuiShoppingListCard extends SubscribeMixin(LitElement) cursor: pointer; } + .reorderButton { + padding-left: 16px; + cursor: pointer; + } + paper-checkbox { padding-left: 4px; padding-right: 20px; diff --git a/src/translations/en.json b/src/translations/en.json index 9411c14584..9d18749495 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2401,7 +2401,9 @@ "shopping-list": { "checked_items": "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": { "hold": "Hold:",