-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Added Drag & Drop Reordering to Shopping List Card. #7296
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
f795a89
7a4c322
212bd40
fc00eda
3aa34f2
82e40e7
b3af653
20d083d
b91412f
99caed8
510c61f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 { | ||
|
|
@@ -51,6 +57,14 @@ class HuiShoppingListCard extends SubscribeMixin(LitElement) | |
|
|
||
| @internalProperty() private _checkedItems?: ShoppingListItem[]; | ||
|
|
||
| @internalProperty() private _reordering = false; | ||
|
|
||
| @internalProperty() private _renderEmptySortable = false; | ||
|
|
||
| private _sortable?; | ||
|
|
||
| @query("#sortable", true) private _sortableEl?: HTMLElement; | ||
|
|
||
| public getCardSize(): number { | ||
| return (this._config ? (this._config.title ? 2 : 0) : 0) + 3; | ||
| } | ||
|
|
@@ -103,15 +117,15 @@ class HuiShoppingListCard extends SubscribeMixin(LitElement) | |
| })} | ||
| > | ||
| <div class="addRow"> | ||
| <ha-icon | ||
| <ha-svg-icon | ||
| class="addButton" | ||
| icon="hass:plus" | ||
| .path=${mdiPlus} | ||
| .title=${this.hass!.localize( | ||
| "ui.panel.lovelace.cards.shopping-list.add_item" | ||
| )} | ||
| @click=${this._addItem} | ||
| > | ||
| </ha-icon> | ||
| </ha-svg-icon> | ||
| <paper-input | ||
| no-label-float | ||
| class="addBox" | ||
|
|
@@ -120,28 +134,27 @@ class HuiShoppingListCard extends SubscribeMixin(LitElement) | |
| )} | ||
| @keydown=${this._addKeyPress} | ||
| ></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> | ||
| ${repeat( | ||
| this._uncheckedItems!, | ||
| (item) => item.id, | ||
| (item) => | ||
| html` | ||
| <div class="editRow"> | ||
| <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` | ||
| <div id="sortable"> | ||
| ${guard([this._uncheckedItems, this._renderEmptySortable], () => | ||
| this._renderEmptySortable | ||
| ? "" | ||
| : this._renderItems(this._uncheckedItems!) | ||
| )} | ||
| </div> | ||
| ` | ||
| )} | ||
| : this._renderItems(this._uncheckedItems!)} | ||
| ${this._checkedItems!.length > 0 | ||
| ? html` | ||
| <div class="divider"></div> | ||
|
|
@@ -151,16 +164,16 @@ class HuiShoppingListCard extends SubscribeMixin(LitElement) | |
| "ui.panel.lovelace.cards.shopping-list.checked_items" | ||
| )} | ||
| </span> | ||
| <ha-icon | ||
| <ha-svg-icon | ||
| class="clearall" | ||
| tabindex="0" | ||
| icon="hass:notification-clear-all" | ||
| .path=${mdiNotificationClearAll} | ||
| .title=${this.hass!.localize( | ||
| "ui.panel.lovelace.cards.shopping-list.clear_items" | ||
| )} | ||
| @click=${this._clearItems} | ||
| > | ||
| </ha-icon> | ||
| </ha-svg-icon> | ||
| </div> | ||
| ${repeat( | ||
| this._checkedItems!, | ||
|
|
@@ -189,6 +202,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}> | ||
|
ShaneQi marked this conversation as resolved.
|
||
| <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> { | ||
| if (!this.hass) { | ||
| return; | ||
|
|
@@ -250,6 +301,49 @@ 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() { | ||
| this._sortable = new Sortable(this.shadowRoot!.getElementById("sortable"), { | ||
|
ShaneQi marked this conversation as resolved.
Outdated
ShaneQi marked this conversation as resolved.
Outdated
|
||
| animation: 150, | ||
| fallbackClass: "sortable-fallback", | ||
| dataIdAttr: "item-id", | ||
| handle: "ha-svg-icon", | ||
| onSort: async (evt) => { | ||
| reorderItems(this.hass!, this._sortable.toArray()).catch(() => | ||
| this._fetchData() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should let the user know it failed and why we undo all the sorting work.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will add a toast for the error message tonight. |
||
| ); | ||
| // Move the shopping list item in memory. | ||
| this._uncheckedItems!.splice( | ||
| evt.newIndex, | ||
| 0, | ||
| this._uncheckedItems!.splice(evt.oldIndex, 1)[0] | ||
| ); | ||
| this._renderEmptySortable = true; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should also do a round of
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tested the 2 scenarios without changing anything, both work fine. When
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is not true, Lit uses comments to keep track of things, because with sorting we change the DOM but not the comments Lit loses track of some elements. That's why we clear the DOM and render empty and start over. That can cause duplicate items in the list when not rendering empty.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll do more digging tonight.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bramkragten I took another look, I still think there is no need to explicitly 'do a round of Because at line 150: When add an item while sorting the list, In short: the list is messed up only after drag-n-drop, we do a round of
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But while I was digging into it, I found another edge case bug:
The bug: The fix I put in is that instead of observing After fix:
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The problem is that re-rendering doesn't clear up the nodes that have no comment anymore.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bramkragten now I see what your concern is, and it's a very valid point. My thinking is: the no-comment-nodes are only generated after drag&drop, so we only need to clear them up after drag&drop. We are already doing that in the sortable's If you still don't think it's safe, I have no problem adding the clear-up when BTW I need to work on the toast to show errors, I forgot it last time but now I remember.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bramkragten Thoughts? Does above make sense to you? Or should I go ahead add the clear-up when |
||
| await this.updateComplete; | ||
| while (this._sortableEl?.lastElementChild) { | ||
| this._sortableEl.removeChild(this._sortableEl.lastElementChild); | ||
|
ShaneQi marked this conversation as resolved.
Outdated
|
||
| } | ||
| this._renderEmptySortable = false; | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| static get styles(): CSSResult { | ||
| return css` | ||
| ha-card { | ||
|
|
@@ -280,6 +374,11 @@ class HuiShoppingListCard extends SubscribeMixin(LitElement) | |
| cursor: pointer; | ||
| } | ||
|
|
||
| .reorderButton { | ||
| padding-left: 16px; | ||
| cursor: pointer; | ||
| } | ||
|
|
||
| paper-checkbox { | ||
| padding-left: 4px; | ||
| padding-right: 20px; | ||
|
|
||


Uh oh!
There was an error while loading. Please reload this page.