import { mdiPlus } from "@mdi/js"; import type { CSSResultGroup } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { repeat } from "lit/directives/repeat"; import { styleMap } from "lit/directives/style-map"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-ripple"; import "../../../components/ha-sortable"; import type { HaSortableOptions } from "../../../components/ha-sortable"; import type { LovelaceSectionElement } from "../../../data/lovelace"; import type { LovelaceCardConfig } from "../../../data/lovelace/config/card"; import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import type { HuiCard } from "../cards/hui-card"; import { computeCardGridSize } from "../common/compute-card-grid-size"; import "../components/hui-card-edit-mode"; import type { LovelacePath } from "../editor/lovelace-path"; import { moveAtPath } from "../editor/lovelace-path"; import type { Lovelace } from "../types"; const CARD_SORTABLE_OPTIONS: HaSortableOptions = { delay: 100, delayOnTouchOnly: true, direction: "vertical", invertedSwapThreshold: 0.7, group: "card", } as HaSortableOptions; const IMPORT_MODE_CARD_SORTABLE_OPTIONS: HaSortableOptions = { ...CARD_SORTABLE_OPTIONS, sort: false, group: { name: "card", put: false, }, }; @customElement("hui-grid-section") export class GridSection extends LitElement implements LovelaceSectionElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public lovelace?: Lovelace; @property({ attribute: false }) public path?: LovelacePath; @property({ attribute: false }) public isStrategy = false; @property({ attribute: false }) public cards: HuiCard[] = []; @property({ attribute: "import-only", type: Boolean }) public importOnly = false; @state() _config?: LovelaceSectionConfig; @state() _dragging = false; public setConfig(config: LovelaceSectionConfig): void { this._config = config; } private _cardConfigKeys = new WeakMap(); private _getKey(cardConfig: LovelaceCardConfig) { if ( !this._cardConfigKeys.has(cardConfig) && typeof cardConfig === "object" ) { this._cardConfigKeys.set(cardConfig, Math.random().toString()); } return this._cardConfigKeys.get(cardConfig)!; } render() { if (!this.cards || !this._config) return nothing; const cardsConfig = this._config?.cards ?? []; const editMode = Boolean(this.lovelace?.editMode && !this.isStrategy); const sortableOptions = this.importOnly ? IMPORT_MODE_CARD_SORTABLE_OPTIONS : CARD_SORTABLE_OPTIONS; return html`
${repeat( cardsConfig, (cardConfig) => this._getKey(cardConfig), (_cardConfig, idx) => { const card = this.cards![idx]; card.layout = "grid"; const gridOptions = card.getGridOptions(); const { rows, columns } = computeCardGridSize(gridOptions); const cardPath: LovelacePath | undefined = editMode ? [...this.path!, "cards", idx] : undefined; return html`
${ editMode ? html` ${card} ` : card }
`; } )} ${ editMode && !this.importOnly ? html` ` : nothing }
`; } private _cardVisibilityChanged() { // Re-render, so the hidden class follows the card's visibility this.requestUpdate(); } private _cardMoved(ev) { ev.stopPropagation(); const { oldIndex, newIndex } = ev.detail; const newConfig = moveAtPath( this.lovelace!.config, [...this.path!, "cards", oldIndex], [...this.path!, "cards", newIndex] ); this.lovelace!.saveConfig(newConfig); } private _cardAdded(ev) { ev.stopPropagation(); const { index, data } = ev.detail; const newConfig = moveAtPath(this.lovelace!.config, data as LovelacePath, [ ...this.path!, "cards", index, ]); this.lovelace!.saveConfig(newConfig); } private _cardRemoved(ev) { ev.stopPropagation(); // Do nothing, it's handled by the "item-added" event from the new parent. } private _dragStart() { this._dragging = true; } private _dragEnd() { this._dragging = false; } private _addCard() { fireEvent(this, "ll-create-card", { path: [...this.path!, "cards"], suggested: ["tile", "heading"], }); } static get styles(): CSSResultGroup { return [ haStyle, css` :host { --base-column-count: 12; --row-gap: var(--ha-section-grid-row-gap, 8px); --column-gap: var(--ha-section-grid-column-gap, 8px); --row-height: var(--ha-section-grid-row-height, 56px); display: flex; flex-direction: column; gap: var(--row-gap); } .container { --grid-column-count: calc( var(--base-column-count) * var(--column-span, 1) ); display: grid; grid-template-columns: repeat( var(--grid-column-count), minmax(0, 1fr) ); grid-auto-rows: auto; row-gap: var(--row-gap); column-gap: var(--column-gap); padding: 0; margin: 0 auto; } .card { border-radius: var( --ha-card-border-radius, var(--ha-border-radius-lg) ); position: relative; grid-row: span var(--row-size, 1); grid-column: span min(var(--column-size, 1), var(--grid-column-count)); } .container.edit-mode .card { min-height: calc((var(--row-height) - var(--row-gap)) / 2); } .card.fit-rows { height: calc( (var(--row-size, 1) * (var(--row-height) + var(--row-gap))) - var( --row-gap ) ); } .card.full-width { grid-column: 1 / -1; } .card:has(> *) { display: block; } /* A class instead of :has(), which older browsers like the one on some Chromecasts don't support. */ .card.hidden { display: none; } .add { position: relative; outline: none; grid-row: span 1; grid-column: span 3; background: none; cursor: pointer; border-radius: var( --ha-card-border-radius, var(--ha-border-radius-lg) ); border: 2px dashed var(--primary-color); height: var(--row-height); order: 1; --ha-ripple-color: var(--primary-color); --ha-ripple-hover-opacity: 0.04; --ha-ripple-pressed-opacity: 0.12; } .add:focus { border-style: solid; } .sortable-ghost { border-radius: var( --ha-card-border-radius, var(--ha-border-radius-lg) ); } `, ]; } } declare global { interface HTMLElementTagNameMap { "hui-grid-section": GridSection; } }