Skip to content

Add todo-list card #735

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

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .hass_dev/lovelace-mushroom-showcase.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
title: Mushroom Showcase
title: Mushroom Showcase
views:
- !include views/light-view.yaml
- !include views/cover-view.yaml
Expand All @@ -15,4 +15,5 @@ views:
- !include views/lock-view.yaml
- !include views/humidifier-view.yaml
- !include views/select-view.yaml
- !include views/number-view.yaml
- !include views/number-view.yaml
- !include views/todo-list-view.yaml
38 changes: 38 additions & 0 deletions .hass_dev/views/todo-list-view.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
title: Todo Lists
icon: mdi:clipboard-list
cards:
- type: grid
title: Todo List
cards:
- type: custom:mushroom-todo-list
entity: todo.shopping_list
name: My Shopping List
icon: mdi:format-list-checks
primary_info: name
secondary_info: state
columns: 1
square: false
- type: vertical-stack
title: Layout
cards:
- type: custom:mushroom-todo-list
entity: todo.shopping_list
name: Horizontal Todo List
layout: horizontal
primary_info: name
secondary_info: state
- type: grid
columns: 2
square: false
cards:
- type: custom:mushroom-todo-list
entity: todo.shopping_list
checked_icon: mdi:checkbox-blank-circle
unchecked_icon: mdi:checkbox-blank-circle-outline
- type: custom:mushroom-todo-list
entity: todo.shopping_list
name: Vertical Todo List
icon: mdi:arrow-down
layout: vertical
primary_info: name
secondary_info: state
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ Different cards are available for differents entities :
- 🌡 [Climate card](docs/cards/climate.md)
- 📑 [Select card](docs/cards/select.md)
- 🔢 [Number card](docs/cards/number.md)
- 📃 [Todo list card](docs/cards/todo-list.md)

### Theme customization

Expand Down
26 changes: 26 additions & 0 deletions docs/cards/todo-list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Todo list card

![Todo list light](../images/todo-list-light.png)
![Todo list dark](../images/todo-list-dark.png)

## Description

A mushroom card for the todo list entity.

## Configuration variables

All options are available in the lovelace editor but you can use `yaml` if you want.

| Name | Type | Default | Description |
|:--------------------|:----------------------------------|:-----------------------------|:-------------------------------------------------|
| `entity` | string | Required | Todo list entity. |
| `name` | string | Todo List | Name of the card. May be displayed as its title. |
| `icon` | string | `mdi:format-list-checks` | Icon displayed next to the title. |
| `layout` | `default` `horizontal` `vertical` | `default` | Affects the internal layout of the card. |
| `primary_info` | `name` `state` `none` | `name` | Info to show as title. |
| `secondary_info` | `name` `state` `none` | `state` | Info to show as subtitle. |
| `checked_icon` | string | `mdi:checkbox-marked` | Icon used for completed items. |
| `unchecked_icon` | string | `mdi:checkbox-blank-outline` | Icon used for open items. |
| `tap_action` | action | `none` | Home assistant action to perform on tap. |
| `hold_action` | action | `more-info` | Home assistant action to perform on hold. |
| `double_tap_action` | action | `none` | Home assistant action to perform on double_tap. |
Binary file added docs/images/todo-list-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/todo-list-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions src/cards/todo-list-card/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { PREFIX_NAME } from "../../const";

export const TODO_LIST_CARD_NAME = `${PREFIX_NAME}-todo-list`;
export const TODO_LIST_CARD_EDITOR_NAME = `${TODO_LIST_CARD_NAME}-editor`;
export const TODO_LIST_CARD_ITEM_NAME = `${TODO_LIST_CARD_NAME}-item`;
export const TODO_LIST_CARD_DIVIDER_NAME = `${TODO_LIST_CARD_NAME}-divider`;
export const TODO_LIST_CARD_INPUT_NAME = `${TODO_LIST_CARD_NAME}-input`;
export const TODO_LIST_ENTITY_DOMAINS = ["todo"];
export const TODO_LIST_UPDATED_EVENT = "shopping_list_updated"; // Still called shopping_list

export const DEFAULT_CHECKED_ICON = "mdi:checkbox-marked";
export const DEFAULT_UNCHECKED_ICON = "mdi:checkbox-blank-outline";
25 changes: 25 additions & 0 deletions src/cards/todo-list-card/todo-list-card-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { assign, object, optional, string } from "superstruct";
import { LovelaceCardConfig } from "../../ha";
import {
AppearanceSharedConfig,
appearanceSharedConfigStruct,
} from "../../shared/config/appearance-config";
import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config";
import { EntitySharedConfig, entitySharedConfigStruct } from "../../shared/config/entity-config";
import { actionsSharedConfigStruct } from "../../shared/config/actions-config";

export type TodoListCardConfig = LovelaceCardConfig &
EntitySharedConfig &
AppearanceSharedConfig & {
checked_icon?: string;
unchecked_icon?: string;
};

export const todoListCardConfigStruct = assign(
lovelaceCardConfigStruct,
assign(entitySharedConfigStruct, appearanceSharedConfigStruct, actionsSharedConfigStruct),
object({
checked_icon: optional(string()),
unchecked_icon: optional(string()),
})
);
81 changes: 81 additions & 0 deletions src/cards/todo-list-card/todo-list-card-divider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import "../../shared/badge-icon";
import "../../shared/button";
import "../../shared/card";
import "../../shared/shape-avatar";
import "../../shared/shape-icon";
import "../../shared/state-info";
import "../../shared/state-item";
import { TODO_LIST_CARD_DIVIDER_NAME } from "./const";

@customElement(TODO_LIST_CARD_DIVIDER_NAME)
export class TodoListCardDivider extends LitElement {
@property() localize: (key: string) => string = (key) => key;

private _buttonClicked() {
const event = new CustomEvent("clear-completed", { bubbles: true, composed: true });
this.dispatchEvent(event);
}

protected render(): TemplateResult {
return html`
<div class="divider">
<hr class="hr-start" />
<button class="button" @click=${this._buttonClicked}>
<ha-icon class="button-icon" icon="mdi:delete-sweep-outline"></ha-icon>
${this.localize("editor.card.todo_list.clear_completed")}
</button>
<hr class="hr-end" />
</div>
`;
}

static get styles(): CSSResultGroup {
return [
css`
.divider * {
box-sizing: border-box;
}

.divider {
display: flex;
align-items: center;
margin: 0 calc(var(--spacing) * -1);
}

.hr-start,
.hr-end {
height: 1px;
background: rgba(var(--rgb-primary-text-color), 0.05);
flex-shrink: 0;
border: none;
min-width: calc(var(--spacing) * 1);
}

.hr-start {
flex: 1;
}

.button {
flex-shrink: 0;
box-sizing: border-box;
background: transparent;
border: none;
font-weight: 600;
color: var(--secondary-text-color);
display: flex;
align-items: center;
justify-content: center;
padding: 0 var(--spacing);
cursor: pointer;
}

.button-icon {
--mdc-icon-size: 20px;
margin-inline-end: 0.5rem;
}
`,
];
}
}
85 changes: 85 additions & 0 deletions src/cards/todo-list-card/todo-list-card-editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { html, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators.js";
import { assert } from "superstruct";
import { fireEvent, LovelaceCardEditor } from "../../ha";
import setupCustomlocalize from "../../localize";
import { MushroomBaseElement } from "../../utils/base-element";
import { GENERIC_LABELS } from "../../utils/form/generic-fields";
import { HaFormSchema } from "../../utils/form/ha-form";
import { loadHaComponents } from "../../utils/loader";
import { TODO_LIST_CARD_EDITOR_NAME, TODO_LIST_ENTITY_DOMAINS } from "./const";
import { TodoListCardConfig, todoListCardConfigStruct } from "./todo-list-card-config";
import { computeActionsFormSchema } from "../../shared/config/actions-config";

export const TODO_LIST_LABELS = ["checked_icon", "unchecked_icon"];

const schema = [
{ name: "entity", selector: { entity: { domain: TODO_LIST_ENTITY_DOMAINS } } },
{ name: "name", selector: { text: {} } },
{ name: "icon", selector: { icon: {} } },
{
type: "grid",
name: "",
schema: [
{ name: "layout", selector: { mush_layout: {} } },
{ name: "primary_info", selector: { mush_info: {} } },
{ name: "secondary_info", selector: { mush_info: {} } },
],
},
{
type: "grid",
name: "",
schema: [
{ name: "unchecked_icon", selector: { icon: {} } },
{ name: "checked_icon", selector: { icon: {} } },
],
},
...computeActionsFormSchema(),
];

@customElement(TODO_LIST_CARD_EDITOR_NAME)
export class TodoListCardEditor extends MushroomBaseElement implements LovelaceCardEditor {
@state() private _config?: TodoListCardConfig;

connectedCallback() {
super.connectedCallback();
void loadHaComponents();
}

public setConfig(config: TodoListCardConfig): void {
assert(config, todoListCardConfigStruct);
this._config = config;
}

private _computeLabel = (schema: HaFormSchema) => {
const customLocalize = setupCustomlocalize(this.hass!);

if (GENERIC_LABELS.includes(schema.name)) {
return customLocalize(`editor.card.generic.${schema.name}`);
}
if (TODO_LIST_LABELS.includes(schema.name)) {
return customLocalize(`editor.card.todo_list.${schema.name}`);
}
return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`);
};

protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
}

return html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${schema}
.computeLabel=${this._computeLabel}
@value-changed=${this._valueChanged}
></ha-form>
`;
}

private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
}
60 changes: 60 additions & 0 deletions src/cards/todo-list-card/todo-list-card-input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import { fireEvent } from "../../ha";
import "../../shared/badge-icon";
import "../../shared/button";
import "../../shared/card";
import "../../shared/shape-avatar";
import "../../shared/shape-icon";
import "../../shared/state-info";
import "../../shared/state-item";
import { TODO_LIST_CARD_INPUT_NAME } from "./const";

@customElement(TODO_LIST_CARD_INPUT_NAME)
export class TodoListItemInput extends LitElement {
@property() value: string = "";
@property() placeholder: string = "";

private _handleInput(e) {
fireEvent(this, "value-changed", { value: e.target.value });
}

protected render(): TemplateResult {
return html`
<input
type="text"
class="input"
@input=${this._handleInput}
.placeholder=${this.placeholder}
.value=${this.value}
/>
`;
}

static get styles(): CSSResultGroup {
return [
css`
.input {
height: 42px;
display: block;
background: rgba(var(--rgb-primary-text-color), 0.05);
transition: background-color 280ms ease-in-out 0s;
border-radius: var(--control-border-radius);
border: none;
box-sizing: border-box;
padding: 0 1rem;
font-weight: 600;
color: var(--primary-text-color);
outline: none !important;
min-width: 0;
width: 100%;
}

.input:placeholder {
font-weight: 600;
color: var(--secondary-text-color);
}
`,
];
}
}
Loading