This commit is contained in:
Bram Kragten 2024-01-19 11:03:37 +01:00
parent 6234f7b7d2
commit 539472bd96
15 changed files with 407 additions and 12 deletions

View File

@ -109,6 +109,7 @@
"element-internals-polyfill": "1.3.10",
"fuse.js": "7.0.0",
"google-timezones-json": "1.2.0",
"gridstack": "10.0.1",
"hls.js": "1.5.1",
"home-assistant-js-websocket": "9.1.0",
"idb-keyval": "6.2.1",

View File

@ -25,7 +25,6 @@ import "../ha-dialog";
import "../ha-dialog-header";
import "../ha-svg-icon";
import "../ha-tip";
import "./ha-media-player-browse";
import "./ha-media-upload-button";
import type { MediaManageDialogParams } from "./show-media-manage-dialog";
import { isComponentLoaded } from "../../common/config/is_component_loaded";

View File

@ -73,6 +73,7 @@ class HuiGridCard extends HuiStackCard<GridCardConfig> {
super.sharedStyles,
css`
#root {
height: 100%;
display: grid;
grid-template-columns: repeat(
var(--grid-card-column-count, ${DEFAULT_COLUMNS}),

View File

@ -453,12 +453,14 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
.secondary=${localizedState}
></ha-tile-info>
</div>
<hui-card-features
.hass=${this.hass}
.stateObj=${stateObj}
.color=${this._config.color}
.features=${this._config.features}
></hui-card-features>
${this._config.features?.length
? html`<hui-card-features
.hass=${this.hass}
.stateObj=${stateObj}
.color=${this._config.color}
.features=${this._config.features}
></hui-card-features>`
: nothing}
</ha-card>
`;
}
@ -481,6 +483,9 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
transition:
box-shadow 180ms ease-in-out,
border-color 180ms ease-in-out;
display: flex;
flex-direction: column;
justify-content: space-around;
}
ha-card.active {
--tile-color: var(--state-icon-color);

View File

@ -9,6 +9,7 @@ const ALWAYS_LOADED_LAYOUTS = new Set(["masonry"]);
const LAZY_LOAD_LAYOUTS = {
panel: () => import("../views/hui-panel-view"),
sidebar: () => import("../views/hui-sidebar-view"),
manual: () => import("../views/hui-manual-view"),
};
export const createViewElement = (

View File

@ -212,6 +212,7 @@ export class HuiCreateDialogCard
showEditCardDialog(this, {
lovelaceConfig: this._params!.lovelaceConfig,
preSaveConfig: this._params!.preSaveConfig,
saveConfig: this._params!.saveConfig,
path: this._params!.path,
cardConfig: config,

View File

@ -367,17 +367,20 @@ export class HuiDialogEditCard
return;
}
this._saving = true;
const cardConfig = this._params?.preSaveConfig
? await this._params.preSaveConfig(this._cardConfig!)
: this._cardConfig!;
await this._params!.saveConfig(
this._params!.path.length === 1
? addCard(
this._params!.lovelaceConfig,
this._params!.path as [number],
this._cardConfig!
cardConfig
)
: replaceCard(
this._params!.lovelaceConfig,
this._params!.path as [number, number],
this._cardConfig!
cardConfig
)
);
this._saving = false;

View File

@ -1,8 +1,12 @@
import { fireEvent } from "../../../../common/dom/fire_event";
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
export interface CreateCardDialogParams {
lovelaceConfig: LovelaceConfig;
preSaveConfig?: (
config: LovelaceCardConfig
) => LovelaceCardConfig | Promise<LovelaceCardConfig>;
saveConfig: (config: LovelaceConfig) => void;
path: [number] | [number, number];
entities?: string[]; // We can pass entity id's that will be added to the config when a card is picked

View File

@ -4,6 +4,9 @@ import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
export interface EditCardDialogParams {
lovelaceConfig: LovelaceConfig;
preSaveConfig?: (
config: LovelaceCardConfig
) => LovelaceCardConfig | Promise<LovelaceCardConfig>;
saveConfig: (config: LovelaceConfig) => void;
path: [number] | [number, number];
cardConfig?: LovelaceCardConfig;

View File

@ -9,6 +9,7 @@ import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import {
DEFAULT_VIEW_LAYOUT,
MANUAL_VIEW_LAYOUT,
PANEL_VIEW_LAYOUT,
SIDEBAR_VIEW_LAYOUT,
} from "../../views/const";
@ -53,6 +54,7 @@ export class HuiViewEditor extends LitElement {
DEFAULT_VIEW_LAYOUT,
SIDEBAR_VIEW_LAYOUT,
PANEL_VIEW_LAYOUT,
MANUAL_VIEW_LAYOUT,
] as const
).map((type) => ({
value: type,

View File

@ -1,4 +1,5 @@
export const DEFAULT_VIEW_LAYOUT = "masonry";
export const PANEL_VIEW_LAYOUT = "panel";
export const SIDEBAR_VIEW_LAYOUT = "sidebar";
export const MANUAL_VIEW_LAYOUT = "manual";
export const VIEWS_NO_BADGE_SUPPORT = [PANEL_VIEW_LAYOUT, SIDEBAR_VIEW_LAYOUT];

View File

@ -0,0 +1,358 @@
import { mdiCursorMove, mdiDelete, mdiPencil, mdiPlus } from "@mdi/js";
import { GridStack, GridStackWidget } from "gridstack";
import gridStackStyleExtra from "gridstack/dist/gridstack-extra.min.css";
import gridStackStyle from "gridstack/dist/gridstack.min.css";
import {
CSSResultGroup,
LitElement,
PropertyValues,
TemplateResult,
css,
html,
nothing,
unsafeCSS,
} from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeRTL } from "../../../common/util/compute_rtl";
import "../../../components/entity/ha-state-label-badge";
import "../../../components/ha-svg-icon";
import type { LovelaceViewElement } from "../../../data/lovelace";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import type { HuiErrorCard } from "../cards/hui-error-card";
import { createCardElement } from "../custom-card-helpers";
import { replaceView } from "../editor/config-util";
import type { Lovelace, LovelaceBadge, LovelaceCard } from "../types";
@customElement("hui-manual-view")
export class ManualView extends LitElement implements LovelaceViewElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public lovelace?: Lovelace;
@property({ type: Boolean }) public narrow = false;
@property({ type: Number }) public index?: number;
@property({ type: Boolean }) public isStrategy = false;
@property({ attribute: false }) public cards: Array<
LovelaceCard | HuiErrorCard
> = [];
@property({ attribute: false }) public badges: LovelaceBadge[] = [];
public setConfig(_config: LovelaceViewConfig): void {}
private _grid?: GridStack;
connectedCallback() {
super.connectedCallback();
if (this.hasUpdated) {
this._setupGrid();
}
}
disconnectedCallback() {
super.disconnectedCallback();
this._grid?.destroy(false);
this._grid = undefined;
}
protected render(): TemplateResult {
return html`
${this.badges.length > 0
? html`<div class="badges">${this.badges}</div>`
: ""}
<div class="grid-stack">
${this.cards.map(
(card, i) =>
html`<div class="grid-stack-item" gs-id=${i}>
${this.lovelace?.editMode
? html` <div class="controls">
<ha-svg-icon
class="handle"
.path=${mdiCursorMove}
></ha-svg-icon>
<ha-icon-button
@click=${this._editCard}
.path=${mdiPencil}
.index=${i}
></ha-icon-button>
<ha-icon-button
@click=${this._deleteCard}
.index=${i}
class="warning"
.path=${mdiDelete}
></ha-icon-button>
</div>`
: nothing}
<div class="grid-stack-item-content">${card}</div>
</div>`
)}
</div>
${this.lovelace?.editMode
? html`
<ha-fab
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.add"
)}
extended
@click=${this._addCard}
class=${classMap({
rtl: computeRTL(this.hass!),
})}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
`
: ""}
`;
}
firstUpdated(changed) {
super.firstUpdated(changed);
this._setupGrid();
}
updated(changedProperties: PropertyValues) {
if (
changedProperties.has("lovelace") &&
this.lovelace?.editMode !== changedProperties.get("lovelace")?.editMode
) {
if (this.lovelace?.editMode) {
this._grid!.setStatic(false);
this._grid!.setAnimation(true);
// this.grid.addWidget(
// '<div class="grid-stack-item"><div class="grid-stack-item-content">hello</div></div>',
// { w: 3 }
// );
} else {
this._grid!.setStatic(true);
this._grid!.setAnimation(false);
}
}
if (
changedProperties.has("cards") &&
changedProperties.get("cards") &&
!this.lovelace?.editMode
) {
this._grid!.load(
(
this.lovelace?.config.views[this.index!] as LovelaceViewConfig
).cards?.map((card, i) => ({
id: i.toString(),
...card.view_layout,
})) || [],
false
);
}
}
public willUpdate(changedProperties: PropertyValues) {
super.willUpdate(changedProperties);
if (
changedProperties.has("lovelace") &&
this.lovelace?.editMode !== changedProperties.get("lovelace")?.editMode
) {
if (this.lovelace?.editMode) {
import("./default-view-editable");
} else if (this._grid) {
this._saveLayout();
}
}
if (changedProperties.has("hass")) {
const oldHass = changedProperties.get("hass") as
| HomeAssistant
| undefined;
if (this.hass!.dockedSidebar !== oldHass?.dockedSidebar) {
// this._updateColumns();
return;
}
}
if (changedProperties.has("narrow")) {
// this._updateColumns();
return;
}
const oldLovelace = changedProperties.get("lovelace") as
| Lovelace
| undefined;
if (
changedProperties.has("cards") ||
(changedProperties.has("lovelace") &&
oldLovelace &&
(oldLovelace.config !== this.lovelace!.config ||
oldLovelace.editMode !== this.lovelace!.editMode))
) {
// this._createColumns();
}
}
private async _editCard(ev): Promise<void> {
const index = ev.target.index;
fireEvent(this, "ll-edit-card", { path: [this.index!, index] });
}
private async _deleteCard(ev): Promise<void> {
const index = ev.target.index;
fireEvent(this, "ll-delete-card", {
path: [this.index!, index],
confirm: true,
});
}
private async _addCard(): Promise<void> {
fireEvent(this, "ll-create-card", {
preSaveConfig: async (config) => {
const card = createCardElement(config);
const height = await card.getCardSize();
const add = this._grid!.addWidget({
w: 3,
h: height,
content: "",
});
return {
...config,
view_layout: {
x: add.gridstackNode!.x,
y: add.gridstackNode!.y,
w: add.gridstackNode!.w,
h: add.gridstackNode!.h,
},
};
},
});
}
private async _saveLayout(): Promise<void> {
if (!this._grid || !this.lovelace?.editMode) {
return;
}
const layouts = this._grid.save(false) as GridStackWidget[];
layouts
.sort((a, b) => Number(a.id!) - Number(b.id!))
.forEach((layout) => {
delete layout.id;
});
const cardConfigs = (
this.lovelace?.config.views[this.index!] as LovelaceViewConfig
).cards?.map((card, i) => ({
...card,
view_layout: layouts[i],
}));
await this.lovelace!.saveConfig(
replaceView(this.hass!, this.lovelace!.config, this.index!, {
...this.lovelace!.config.views[this.index!],
cards: cardConfigs,
})
);
}
private _setupGrid(): void {
this._grid = GridStack.init(
{
cellHeight: 60,
animate: false,
columnOpts: {
layout: "moveScale",
// breakpointForWindow: true, // test window vs grid size
breakpoints: [
{ w: 700, c: 1 },
{ w: 850, c: 4 },
{ w: 950, c: 8 },
{ w: 1100, c: 12 },
],
},
minRow: 5,
sizeToContent: false,
handleClass: "handle",
staticGrid: true,
margin: 4,
},
this.shadowRoot!.querySelector(".grid-stack") as HTMLElement
);
this._grid.load(
(
this.lovelace?.config.views[this.index!] as LovelaceViewConfig
).cards?.map((card, i) => ({
id: i.toString(),
...card.view_layout,
})) || [],
false
);
this._grid.on("dragstop resizestop", () => {
this._saveLayout();
});
}
static get styles(): CSSResultGroup {
return css`
${unsafeCSS(gridStackStyle)}
${unsafeCSS(gridStackStyleExtra)}
:host {
display: block;
padding-top: 4px;
}
.grid-stack {
height: 100vh;
margin: 4px;
}
.grid-stack-item {
position: relative;
}
.controls {
display: none;
z-index: 999;
= }
.grid-stack-item:hover .controls {
position: absolute;
display: flex;
top: 8px;
right: 8px;
}
.handle {
width: 24px;
height: 24px;
padding: 12px;
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab !important;
}
.badges {
margin: 8px 16px;
font-size: 85%;
text-align: center;
}
ha-fab {
position: fixed;
right: calc(16px + env(safe-area-inset-right));
bottom: calc(16px + env(safe-area-inset-bottom));
z-index: 1;
}
ha-fab.rtl {
right: auto;
left: calc(16px + env(safe-area-inset-left));
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-manual-view": ManualView;
}
}

View File

@ -35,7 +35,13 @@ import {
declare global {
// for fire event
interface HASSDomEvents {
"ll-create-card": undefined;
"ll-create-card":
| {
preSaveConfig?: (
config: LovelaceCardConfig
) => LovelaceCardConfig | Promise<LovelaceCardConfig>;
}
| undefined;
"ll-edit-card": { path: [number] | [number, number] };
"ll-delete-card": { path: [number] | [number, number]; confirm: boolean };
}
@ -236,9 +242,10 @@ export class HUIView extends ReactiveElement {
private _createLayoutElement(config: LovelaceViewConfig): void {
this._layoutElement = createViewElement(config) as LovelaceViewElement;
this._layoutElementType = config.type;
this._layoutElement.addEventListener("ll-create-card", () => {
this._layoutElement.addEventListener("ll-create-card", (ev) => {
showCreateCardDialog(this, {
lovelaceConfig: this.lovelace.config,
preSaveConfig: ev.detail.preSaveConfig,
saveConfig: this.lovelace.saveConfig,
path: [this.index],
});

View File

@ -4984,7 +4984,8 @@
"types": {
"masonry": "Masonry (default)",
"sidebar": "Sidebar",
"panel": "Panel (1 card)"
"panel": "Panel (1 card)",
"manual": "Manual"
},
"subview": "Subview",
"subview_helper": "Subviews don't appear in tabs and have a back button.",

View File

@ -9235,6 +9235,13 @@ __metadata:
languageName: node
linkType: hard
"gridstack@npm:10.0.1":
version: 10.0.1
resolution: "gridstack@npm:10.0.1"
checksum: 5310d1e299f01bba68162d1cf69725248a3a4baee2088276927597f74894da6a432981d2e1879464faee91f0011fc4f6cd8b71dceb812a175bb7cbcf4ab7ea8b
languageName: node
linkType: hard
"gulp-cli@npm:^2.2.0":
version: 2.3.0
resolution: "gulp-cli@npm:2.3.0"
@ -9613,6 +9620,7 @@ __metadata:
fuse.js: "npm:7.0.0"
glob: "npm:10.3.10"
google-timezones-json: "npm:1.2.0"
gridstack: "npm:10.0.1"
gulp: "npm:4.0.2"
gulp-flatmap: "npm:1.0.2"
gulp-json-transform: "npm:0.4.8"