diff --git a/demo/src/configs/teachingbirds/lovelace.ts b/demo/src/configs/teachingbirds/lovelace.ts index 1ff4ff8807..28333fce9b 100644 --- a/demo/src/configs/teachingbirds/lovelace.ts +++ b/demo/src/configs/teachingbirds/lovelace.ts @@ -220,7 +220,8 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({ state_filter: ["on"], }, { - type: "shopping-list", + type: "todo-list", + entity: "todo.shopping_list", }, { entities: [ diff --git a/gallery/src/pages/lovelace/shopping-list-card.markdown b/gallery/src/pages/lovelace/shopping-list-card.markdown deleted file mode 100644 index 57811b3859..0000000000 --- a/gallery/src/pages/lovelace/shopping-list-card.markdown +++ /dev/null @@ -1,3 +0,0 @@ ---- -title: Shopping List Card ---- diff --git a/gallery/src/pages/lovelace/todo-list-card.markdown b/gallery/src/pages/lovelace/todo-list-card.markdown new file mode 100644 index 0000000000..1d3286ca02 --- /dev/null +++ b/gallery/src/pages/lovelace/todo-list-card.markdown @@ -0,0 +1,3 @@ +--- +title: Todo List Card +--- diff --git a/gallery/src/pages/lovelace/shopping-list-card.ts b/gallery/src/pages/lovelace/todo-list-card.ts similarity index 86% rename from gallery/src/pages/lovelace/shopping-list-card.ts rename to gallery/src/pages/lovelace/todo-list-card.ts index f7822be937..49a61a61a1 100644 --- a/gallery/src/pages/lovelace/shopping-list-card.ts +++ b/gallery/src/pages/lovelace/todo-list-card.ts @@ -19,22 +19,22 @@ const CONFIGS = [ { heading: "List example", config: ` -- type: shopping-list +- type: todo-list entity: todo.shopping_list `, }, { heading: "List with title example", config: ` -- type: shopping-list +- type: todo-list title: Shopping List entity: todo.read_only `, }, ]; -@customElement("demo-lovelace-shopping-list-card") -class DemoShoppingListEntity extends LitElement { +@customElement("demo-lovelace-todo-list-card") +class DemoTodoListEntity extends LitElement { @query("#demos") private _demoRoot!: HTMLElement; protected render(): TemplateResult { @@ -54,6 +54,6 @@ class DemoShoppingListEntity extends LitElement { declare global { interface HTMLElementTagNameMap { - "demo-lovelace-shopping-list-card": DemoShoppingListEntity; + "demo-lovelace-todo-list-card": DemoTodoListEntity; } } diff --git a/src/panels/lovelace/cards/hui-shopping-list-card.ts b/src/panels/lovelace/cards/hui-shopping-list-card.ts index 2f0aa58e73..7c0718a772 100644 --- a/src/panels/lovelace/cards/hui-shopping-list-card.ts +++ b/src/panels/lovelace/cards/hui-shopping-list-card.ts @@ -1,535 +1,28 @@ -import { mdiDrag, mdiNotificationClearAll, mdiPlus, mdiSort } from "@mdi/js"; -import { UnsubscribeFunc } from "home-assistant-js-websocket"; -import { - CSSResultGroup, - LitElement, - PropertyValueMap, - PropertyValues, - css, - html, - nothing, -} from "lit"; -import { customElement, property, query, state } from "lit/decorators"; -import { classMap } from "lit/directives/class-map"; -import { guard } from "lit/directives/guard"; -import { repeat } from "lit/directives/repeat"; -import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; -import { supportsFeature } from "../../../common/entity/supports-feature"; -import "../../../components/ha-card"; -import "../../../components/ha-checkbox"; -import "../../../components/ha-list-item"; -import "../../../components/ha-select"; -import "../../../components/ha-svg-icon"; -import "../../../components/ha-textfield"; -import type { HaTextField } from "../../../components/ha-textfield"; -import { - TodoItem, - TodoItemStatus, - TodoListEntityFeature, - createItem, - deleteItem, - fetchItems, - getTodoLists, - moveItem, - updateItem, -} from "../../../data/todo"; -import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; -import type { SortableInstance } from "../../../resources/sortable"; -import { HomeAssistant } from "../../../types"; -import { findEntities } from "../common/find-entities"; -import { LovelaceCard, LovelaceCardEditor } from "../types"; -import { ShoppingListCardConfig } from "./types"; +import { customElement } from "lit/decorators"; +import { HuiTodoListCard } from "./hui-todo-list-card"; +import { getTodoLists } from "../../../data/todo"; @customElement("hui-shopping-list-card") -class HuiShoppingListCard - extends SubscribeMixin(LitElement) - implements LovelaceCard -{ - public static async getConfigElement(): Promise { - await import("../editor/config-elements/hui-shopping-list-editor"); - return document.createElement("hui-shopping-list-card-editor"); +class HuiShoppingListCard extends HuiTodoListCard { + override checkConfig(): void { + // skip config check, entity is not required for shopping list card } - public static getStubConfig( - hass: HomeAssistant, - entities: string[], - entitiesFallback: string[] - ): ShoppingListCardConfig { - const includeDomains = ["todo"]; - const maxEntities = 1; - const foundEntities = findEntities( - hass, - maxEntities, - entities, - entitiesFallback, - includeDomains - ); - - return { type: "shopping-list", entity: foundEntities[0] || "" }; - } - - @property({ attribute: false }) public hass?: HomeAssistant; - - @state() private _config?: ShoppingListCardConfig; - - @state() private _entityId?: string; - - @state() private _items: Record = {}; - - @state() private _uncheckedItems?: TodoItem[]; - - @state() private _checkedItems?: TodoItem[]; - - @state() private _reordering = false; - - @state() private _renderEmptySortable = false; - - private _sortable?: SortableInstance; - - @query("#sortable") private _sortableEl?: HTMLElement; - - public getCardSize(): number { - return (this._config ? (this._config.title ? 2 : 0) : 0) + 3; - } - - public setConfig(config: ShoppingListCardConfig): void { - this._config = config; - this._uncheckedItems = []; - this._checkedItems = []; - - if (this._config!.entity) { - this._entityId = this._config!.entity; - } - } - - public willUpdate( - _changedProperties: PropertyValueMap | Map - ): void { - if (!this.hasUpdated) { - if (!this._entityId) { - const todoLists = getTodoLists(this.hass!); - if (todoLists.length) { - if (todoLists.length > 1) { - // find first entity provided by "shopping_list" - for (const list of todoLists) { - const entityReg = this.hass?.entities[list.entity_id]; - if (entityReg?.platform === "shopping_list") { - this._entityId = list.entity_id; - break; - } - } - } - if (!this._entityId) { - this._entityId = todoLists[0].entity_id; + override getEntityId(): string | undefined { + const todoLists = getTodoLists(this.hass!); + if (todoLists.length) { + if (todoLists.length > 1) { + // find first entity provided by "shopping_list" + for (const list of todoLists) { + const entityReg = this.hass!.entities[list.entity_id]; + if (entityReg?.platform === "shopping_list") { + return list.entity_id; } } } - this._fetchData(); + return todoLists[0].entity_id; } - } - - public hassSubscribe(): Promise[] { - return [ - this.hass!.connection.subscribeEvents( - () => this._fetchData(), - "shopping_list_updated" - ), - ]; - } - - protected updated(changedProps: PropertyValues): void { - super.updated(changedProps); - if (!this._config || !this.hass) { - return; - } - - const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - const oldConfig = changedProps.get("_config") as - | ShoppingListCardConfig - | undefined; - - if ( - (changedProps.has("hass") && oldHass?.themes !== this.hass.themes) || - (changedProps.has("_config") && oldConfig?.theme !== this._config.theme) - ) { - applyThemesOnElement(this, this.hass.themes, this._config.theme); - } - } - - protected render() { - if (!this._config || !this.hass || !this._entityId) { - return nothing; - } - - return html` - -
- ${this.todoListSupportsFeature(TodoListEntityFeature.CREATE_TODO_ITEM) - ? html` - - - - ` - : nothing} - ${this.todoListSupportsFeature(TodoListEntityFeature.MOVE_TODO_ITEM) - ? html` - - - ` - : nothing} -
- ${this._reordering - ? html` -
- ${guard( - [this._uncheckedItems, this._renderEmptySortable], - () => - this._renderEmptySortable - ? "" - : this._renderItems(this._uncheckedItems!) - )} -
- ` - : this._renderItems(this._uncheckedItems!)} - ${this._checkedItems!.length > 0 - ? html` -
-
- - ${this.hass!.localize( - "ui.panel.lovelace.cards.shopping-list.checked_items" - )} - - ${this.todoListSupportsFeature( - TodoListEntityFeature.DELETE_TODO_ITEM - ) - ? html` - ` - : nothing} -
- ${repeat( - this._checkedItems!, - (item) => item.uid, - (item) => html` -
- ${this.todoListSupportsFeature( - TodoListEntityFeature.UPDATE_TODO_ITEM - ) - ? html` ` - : nothing} - -
- ` - )} - ` - : ""} -
- `; - } - - private _renderItems(items: TodoItem[]) { - return html` - ${repeat( - items, - (item) => item.uid, - (item) => html` -
- ${this.todoListSupportsFeature( - TodoListEntityFeature.UPDATE_TODO_ITEM - ) - ? html` ` - : nothing} - - ${this._reordering - ? html` - - - ` - : ""} -
- ` - )} - `; - } - - private todoListSupportsFeature(feature: number): boolean { - const entityStateObj = this.hass!.states[this._entityId!]; - return entityStateObj && supportsFeature(entityStateObj, feature); - } - - private async _fetchData(): Promise { - if (!this.hass || !this._entityId) { - return; - } - const checkedItems: TodoItem[] = []; - const uncheckedItems: TodoItem[] = []; - const items = await fetchItems(this.hass!, this._entityId!); - const records: Record = {}; - items.forEach((item) => { - records[item.uid!] = item; - if (item.status === TodoItemStatus.Completed) { - checkedItems.push(item); - } else { - uncheckedItems.push(item); - } - }); - this._items = records; - this._checkedItems = checkedItems; - this._uncheckedItems = uncheckedItems; - } - - private _completeItem(ev): void { - const item = this._items[ev.target.itemId]; - updateItem(this.hass!, this._entityId!, { - ...item, - status: ev.target.checked - ? TodoItemStatus.Completed - : TodoItemStatus.NeedsAction, - }).finally(() => this._fetchData()); - } - - private _saveEdit(ev): void { - // If name is not empty, update the item otherwise remove it - if (ev.target.value) { - const item = this._items[ev.target.itemId]; - updateItem(this.hass!, this._entityId!, { - ...item, - summary: ev.target.value, - }).finally(() => this._fetchData()); - } else if ( - this.todoListSupportsFeature(TodoListEntityFeature.DELETE_TODO_ITEM) - ) { - deleteItem(this.hass!, this._entityId!, ev.target.itemId).finally(() => - this._fetchData() - ); - } - - ev.target.blur(); - } - - private async _clearCompletedItems(): Promise { - if (!this.hass) { - return; - } - const deleteActions: Array> = []; - this._checkedItems!.forEach((item: TodoItem) => { - deleteActions.push(deleteItem(this.hass!, this._entityId!, item.uid!)); - }); - await Promise.all(deleteActions).finally(() => this._fetchData()); - } - - private get _newItem(): HaTextField { - return this.shadowRoot!.querySelector(".addBox") as HaTextField; - } - - private _addItem(ev): void { - const newItem = this._newItem; - if (newItem.value!.length > 0) { - createItem(this.hass!, this._entityId!, newItem.value!).finally(() => - this._fetchData() - ); - } - - newItem.value = ""; - if (ev) { - newItem.focus(); - } - } - - private _addKeyPress(ev): void { - if (ev.key === "Enter") { - this._addItem(null); - } - } - - private async _toggleReorder() { - this._reordering = !this._reordering; - await this.updateComplete; - if (this._reordering) { - this._createSortable(); - } else { - this._sortable?.destroy(); - this._sortable = undefined; - } - } - - private async _createSortable() { - const Sortable = (await import("../../../resources/sortable")).default; - const sortableEl = this._sortableEl; - this._sortable = new Sortable(sortableEl!, { - animation: 150, - fallbackClass: "sortable-fallback", - dataIdAttr: "item-id", - handle: "ha-svg-icon", - onEnd: async (evt) => { - if (evt.newIndex === undefined || evt.oldIndex === undefined) { - return; - } - // 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) { - const item = this._uncheckedItems![evt.oldIndex]; - moveItem( - this.hass!, - this._entityId!, - item.uid!, - evt.newIndex - ).finally(() => 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(): CSSResultGroup { - return css` - ha-card { - padding: 16px; - height: 100%; - box-sizing: border-box; - } - - .has-header { - padding-top: 0; - } - - .editRow, - .addRow, - .checked { - display: flex; - flex-direction: row; - align-items: center; - } - - .item { - margin-top: 8px; - } - - .addButton { - padding-right: 16px; - padding-inline-end: 16px; - cursor: pointer; - direction: var(--direction); - } - - .reorderButton { - padding-left: 16px; - padding-inline-start: 16px; - cursor: pointer; - direction: var(--direction); - } - - ha-checkbox { - margin-left: -12px; - margin-inline-start: -12px; - direction: var(--direction); - } - - ha-textfield { - flex-grow: 1; - } - - .checked { - margin: 12px 0; - justify-content: space-between; - } - - .checked span { - color: var(--primary-text-color); - font-weight: 500; - } - - .divider { - height: 1px; - background-color: var(--divider-color); - margin: 10px 0; - } - - .clearall { - cursor: pointer; - } - - .todoList { - display: block; - padding: 8px; - } - `; + return undefined; } } diff --git a/src/panels/lovelace/cards/hui-todo-list-card.ts b/src/panels/lovelace/cards/hui-todo-list-card.ts new file mode 100644 index 0000000000..8a3112f0cd --- /dev/null +++ b/src/panels/lovelace/cards/hui-todo-list-card.ts @@ -0,0 +1,534 @@ +import { mdiDrag, mdiNotificationClearAll, mdiPlus, mdiSort } from "@mdi/js"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { + CSSResultGroup, + LitElement, + PropertyValueMap, + PropertyValues, + css, + html, + nothing, +} from "lit"; +import { customElement, property, query, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; +import { guard } from "lit/directives/guard"; +import { repeat } from "lit/directives/repeat"; +import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; +import { supportsFeature } from "../../../common/entity/supports-feature"; +import "../../../components/ha-card"; +import "../../../components/ha-checkbox"; +import "../../../components/ha-list-item"; +import "../../../components/ha-select"; +import "../../../components/ha-svg-icon"; +import "../../../components/ha-textfield"; +import type { HaTextField } from "../../../components/ha-textfield"; +import { + TodoItem, + TodoItemStatus, + TodoListEntityFeature, + createItem, + deleteItem, + fetchItems, + moveItem, + updateItem, +} from "../../../data/todo"; +import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; +import type { SortableInstance } from "../../../resources/sortable"; +import { HomeAssistant } from "../../../types"; +import { findEntities } from "../common/find-entities"; +import { LovelaceCard, LovelaceCardEditor } from "../types"; +import { TodoListCardConfig } from "./types"; + +@customElement("hui-todo-list-card") +export class HuiTodoListCard + extends SubscribeMixin(LitElement) + implements LovelaceCard +{ + public static async getConfigElement(): Promise { + await import("../editor/config-elements/hui-todo-list-editor"); + return document.createElement("hui-todo-list-card-editor"); + } + + public static getStubConfig( + hass: HomeAssistant, + entities: string[], + entitiesFallback: string[] + ): TodoListCardConfig { + const includeDomains = ["todo"]; + const maxEntities = 1; + const foundEntities = findEntities( + hass, + maxEntities, + entities, + entitiesFallback, + includeDomains + ); + + return { type: "todo-list", entity: foundEntities[0] || "" }; + } + + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: TodoListCardConfig; + + @state() private _entityId?: string; + + @state() private _items: Record = {}; + + @state() private _uncheckedItems?: TodoItem[]; + + @state() private _checkedItems?: TodoItem[]; + + @state() private _reordering = false; + + @state() private _renderEmptySortable = false; + + private _sortable?: SortableInstance; + + @query("#sortable") private _sortableEl?: HTMLElement; + + public getCardSize(): number { + return (this._config ? (this._config.title ? 2 : 0) : 0) + 3; + } + + public setConfig(config: TodoListCardConfig): void { + this.checkConfig(config); + + this._config = config; + this._entityId = config.entity; + this._uncheckedItems = []; + this._checkedItems = []; + } + + protected checkConfig(config: TodoListCardConfig): void { + if (!config.entity || config.entity.split(".")[0] !== "todo") { + throw new Error("Specify an entity from within the todo domain"); + } + } + + protected getEntityId(): string | undefined { + // not implemented, todo list should always have an entity id set; + return undefined; + } + + public willUpdate( + _changedProperties: PropertyValueMap | Map + ): void { + if (!this.hasUpdated) { + if (!this._entityId) { + this._entityId = this.getEntityId(); + } + this._fetchData(); + } + } + + public hassSubscribe(): Promise[] { + return [ + this.hass!.connection.subscribeEvents( + () => this._fetchData(), + "shopping_list_updated" + ), + ]; + } + + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + if (!this._config || !this.hass) { + return; + } + + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + const oldConfig = changedProps.get("_config") as + | TodoListCardConfig + | undefined; + + if ( + (changedProps.has("hass") && oldHass?.themes !== this.hass.themes) || + (changedProps.has("_config") && oldConfig?.theme !== this._config.theme) + ) { + applyThemesOnElement(this, this.hass.themes, this._config.theme); + } + } + + protected render() { + if (!this._config || !this.hass || !this._entityId) { + return nothing; + } + + return html` + +
+ ${this.todoListSupportsFeature(TodoListEntityFeature.CREATE_TODO_ITEM) + ? html` + + + + ` + : nothing} + ${this.todoListSupportsFeature(TodoListEntityFeature.MOVE_TODO_ITEM) + ? html` + + + ` + : nothing} +
+ ${this._reordering + ? html` +
+ ${guard( + [this._uncheckedItems, this._renderEmptySortable], + () => + this._renderEmptySortable + ? "" + : this._renderItems(this._uncheckedItems!) + )} +
+ ` + : this._renderItems(this._uncheckedItems!)} + ${this._checkedItems!.length > 0 + ? html` +
+
+ + ${this.hass!.localize( + "ui.panel.lovelace.cards.todo-list.checked_items" + )} + + ${this.todoListSupportsFeature( + TodoListEntityFeature.DELETE_TODO_ITEM + ) + ? html` + ` + : nothing} +
+ ${repeat( + this._checkedItems!, + (item) => item.uid, + (item) => html` +
+ ${this.todoListSupportsFeature( + TodoListEntityFeature.UPDATE_TODO_ITEM + ) + ? html` ` + : nothing} + +
+ ` + )} + ` + : ""} +
+ `; + } + + private _renderItems(items: TodoItem[]) { + return html` + ${repeat( + items, + (item) => item.uid, + (item) => html` +
+ ${this.todoListSupportsFeature( + TodoListEntityFeature.UPDATE_TODO_ITEM + ) + ? html` ` + : nothing} + + ${this._reordering + ? html` + + + ` + : ""} +
+ ` + )} + `; + } + + private todoListSupportsFeature(feature: number): boolean { + const entityStateObj = this.hass!.states[this._entityId!]; + return entityStateObj && supportsFeature(entityStateObj, feature); + } + + private async _fetchData(): Promise { + if (!this.hass || !this._entityId) { + return; + } + const checkedItems: TodoItem[] = []; + const uncheckedItems: TodoItem[] = []; + const items = await fetchItems(this.hass!, this._entityId!); + const records: Record = {}; + items.forEach((item) => { + records[item.uid!] = item; + if (item.status === TodoItemStatus.Completed) { + checkedItems.push(item); + } else { + uncheckedItems.push(item); + } + }); + this._items = records; + this._checkedItems = checkedItems; + this._uncheckedItems = uncheckedItems; + } + + private _completeItem(ev): void { + const item = this._items[ev.target.itemId]; + updateItem(this.hass!, this._entityId!, { + ...item, + status: ev.target.checked + ? TodoItemStatus.Completed + : TodoItemStatus.NeedsAction, + }).finally(() => this._fetchData()); + } + + private _saveEdit(ev): void { + // If name is not empty, update the item otherwise remove it + if (ev.target.value) { + const item = this._items[ev.target.itemId]; + updateItem(this.hass!, this._entityId!, { + ...item, + summary: ev.target.value, + }).finally(() => this._fetchData()); + } else if ( + this.todoListSupportsFeature(TodoListEntityFeature.DELETE_TODO_ITEM) + ) { + deleteItem(this.hass!, this._entityId!, ev.target.itemId).finally(() => + this._fetchData() + ); + } + + ev.target.blur(); + } + + private async _clearCompletedItems(): Promise { + if (!this.hass) { + return; + } + const deleteActions: Array> = []; + this._checkedItems!.forEach((item: TodoItem) => { + deleteActions.push(deleteItem(this.hass!, this._entityId!, item.uid!)); + }); + await Promise.all(deleteActions).finally(() => this._fetchData()); + } + + private get _newItem(): HaTextField { + return this.shadowRoot!.querySelector(".addBox") as HaTextField; + } + + private _addItem(ev): void { + const newItem = this._newItem; + if (newItem.value!.length > 0) { + createItem(this.hass!, this._entityId!, newItem.value!).finally(() => + this._fetchData() + ); + } + + newItem.value = ""; + if (ev) { + newItem.focus(); + } + } + + private _addKeyPress(ev): void { + if (ev.key === "Enter") { + this._addItem(null); + } + } + + private async _toggleReorder() { + this._reordering = !this._reordering; + await this.updateComplete; + if (this._reordering) { + this._createSortable(); + } else { + this._sortable?.destroy(); + this._sortable = undefined; + } + } + + private async _createSortable() { + const Sortable = (await import("../../../resources/sortable")).default; + const sortableEl = this._sortableEl; + this._sortable = new Sortable(sortableEl!, { + animation: 150, + fallbackClass: "sortable-fallback", + dataIdAttr: "item-id", + handle: "ha-svg-icon", + onEnd: async (evt) => { + if (evt.newIndex === undefined || evt.oldIndex === undefined) { + return; + } + // 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) { + const item = this._uncheckedItems![evt.oldIndex]; + moveItem( + this.hass!, + this._entityId!, + item.uid!, + evt.newIndex + ).finally(() => 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(): CSSResultGroup { + return css` + ha-card { + padding: 16px; + height: 100%; + box-sizing: border-box; + } + + .has-header { + padding-top: 0; + } + + .editRow, + .addRow, + .checked { + display: flex; + flex-direction: row; + align-items: center; + } + + .item { + margin-top: 8px; + } + + .addButton { + padding-right: 16px; + padding-inline-end: 16px; + cursor: pointer; + direction: var(--direction); + } + + .reorderButton { + padding-left: 16px; + padding-inline-start: 16px; + cursor: pointer; + direction: var(--direction); + } + + ha-checkbox { + margin-left: -12px; + margin-inline-start: -12px; + direction: var(--direction); + } + + ha-textfield { + flex-grow: 1; + } + + .checked { + margin: 12px 0; + justify-content: space-between; + } + + .checked span { + color: var(--primary-text-color); + font-weight: 500; + } + + .divider { + height: 1px; + background-color: var(--divider-color); + margin: 10px 0; + } + + .clearall { + cursor: pointer; + } + + .todoList { + display: block; + padding: 8px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-todo-list-card": HuiTodoListCard; + } +} diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index c80896bc14..f895961a4e 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -427,7 +427,7 @@ export interface SensorCardConfig extends LovelaceCardConfig { }; } -export interface ShoppingListCardConfig extends LovelaceCardConfig { +export interface TodoListCardConfig extends LovelaceCardConfig { title?: string; theme?: string; entity?: string; diff --git a/src/panels/lovelace/create-element/create-card-element.ts b/src/panels/lovelace/create-element/create-card-element.ts index c1dcb19898..8c3fb31cd4 100644 --- a/src/panels/lovelace/create-element/create-card-element.ts +++ b/src/panels/lovelace/create-element/create-card-element.ts @@ -78,6 +78,7 @@ const LAZY_LOAD_TYPES = { picture: () => import("../cards/hui-picture-card"), "plant-status": () => import("../cards/hui-plant-status-card"), "recovery-mode": () => import("../cards/hui-recovery-mode-card"), + "todo-list": () => import("../cards/hui-todo-list-card"), "shopping-list": () => import("../cards/hui-shopping-list-card"), starting: () => import("../cards/hui-starting-card"), "statistics-graph": () => import("../cards/hui-statistics-graph-card"), diff --git a/src/panels/lovelace/editor/config-elements/hui-shopping-list-editor.ts b/src/panels/lovelace/editor/config-elements/hui-todo-list-editor.ts similarity index 89% rename from src/panels/lovelace/editor/config-elements/hui-shopping-list-editor.ts rename to src/panels/lovelace/editor/config-elements/hui-todo-list-editor.ts index e1063b7b2f..3eb9061740 100644 --- a/src/panels/lovelace/editor/config-elements/hui-shopping-list-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-todo-list-editor.ts @@ -6,7 +6,7 @@ import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/ha-alert"; import "../../../../components/ha-form/ha-form"; import { HomeAssistant } from "../../../../types"; -import { ShoppingListCardConfig } from "../../cards/types"; +import { TodoListCardConfig } from "../../cards/types"; import { LovelaceCardEditor } from "../../types"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { SchemaUnion } from "../../../../components/ha-form/types"; @@ -32,16 +32,16 @@ const SCHEMA = [ { name: "theme", selector: { theme: {} } }, ] as const; -@customElement("hui-shopping-list-card-editor") -export class HuiShoppingListEditor +@customElement("hui-todo-list-card-editor") +export class HuiTodoListEditor extends LitElement implements LovelaceCardEditor { @property({ attribute: false }) public hass?: HomeAssistant; - @state() private _config?: ShoppingListCardConfig; + @state() private _config?: TodoListCardConfig; - public setConfig(config: ShoppingListCardConfig): void { + public setConfig(config: TodoListCardConfig): void { assert(config, cardConfigStruct); this._config = config; } @@ -101,6 +101,6 @@ export class HuiShoppingListEditor declare global { interface HTMLElementTagNameMap { - "hui-shopping-list-card-editor": HuiShoppingListEditor; + "hui-todo-list-card-editor": HuiTodoListEditor; } } diff --git a/src/panels/lovelace/editor/lovelace-cards.ts b/src/panels/lovelace/editor/lovelace-cards.ts index 9dbefa4506..c84df7d6aa 100644 --- a/src/panels/lovelace/editor/lovelace-cards.ts +++ b/src/panels/lovelace/editor/lovelace-cards.ts @@ -123,6 +123,6 @@ export const coreCards: Card[] = [ type: "vertical-stack", }, { - type: "shopping-list", + type: "todo-list", }, ]; diff --git a/src/panels/todo/ha-panel-todo.ts b/src/panels/todo/ha-panel-todo.ts index 5e79de67f5..ebdfa58173 100644 --- a/src/panels/todo/ha-panel-todo.ts +++ b/src/panels/todo/ha-panel-todo.ts @@ -113,7 +113,7 @@ class PanelTodo extends LitElement { return; } this._card = createCardElement({ - type: "shopping-list", + type: "todo-list", entity: this._entityId, }) as LovelaceCard; this._card.hass = this.hass; diff --git a/src/translations/en.json b/src/translations/en.json index e527941fef..e5ab96aaa9 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -8,7 +8,7 @@ "logbook": "Logbook", "history": "History", "mailbox": "Mailbox", - "todo": "To-do Lists", + "todo": "To-do lists", "developer_tools": "Developer tools", "media_browser": "Media", "profile": "Profile" @@ -4471,7 +4471,7 @@ "entities": { "never_triggered": "Never triggered" }, - "shopping-list": { + "todo-list": { "lists": "To-do Lists", "checked_items": "Checked items", "clear_items": "Clear checked items", @@ -5045,9 +5045,9 @@ "graph_type": "Graph type", "description": "The Sensor card gives you a quick overview of your sensors state with an optional graph to visualize change over time." }, - "shopping-list": { - "name": "Shopping list", - "description": "The Shopping list card allows you to add, edit, check-off, and clear items from your shopping list.", + "todo-list": { + "name": "Todo list", + "description": "The to-do list card allows you to add, edit, check-off, and clear items from your to-do list.", "integration_not_loaded": "This card requires the `todo` integration to be set up." }, "thermostat": {