mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-28 20:27:21 +00:00
Compare commits
5 Commits
unassigned
...
fix-undo-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdc2085f5b | ||
|
|
fe50c1212a | ||
|
|
c01fbf5f47 | ||
|
|
5c8da28b61 | ||
|
|
bbb3c0208b |
@@ -12,7 +12,7 @@ const UNDO_REDO_STACK_LIMIT = 75;
|
|||||||
*/
|
*/
|
||||||
export interface UndoRedoControllerConfig<ConfigType> {
|
export interface UndoRedoControllerConfig<ConfigType> {
|
||||||
stackLimit?: number;
|
stackLimit?: number;
|
||||||
currentConfig: () => ConfigType;
|
currentConfig: (itemBeingApplied?: ConfigType) => ConfigType;
|
||||||
apply: (config: ConfigType) => void;
|
apply: (config: ConfigType) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +34,9 @@ export class UndoRedoController<ConfigType> implements ReactiveController {
|
|||||||
throw new Error("No apply function provided");
|
throw new Error("No apply function provided");
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _currentConfig: () => ConfigType = () => {
|
private readonly _currentConfig: (
|
||||||
|
itemBeingApplied?: ConfigType
|
||||||
|
) => ConfigType = () => {
|
||||||
throw new Error("No currentConfig function provided");
|
throw new Error("No currentConfig function provided");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -105,8 +107,8 @@ export class UndoRedoController<ConfigType> implements ReactiveController {
|
|||||||
if (this._undoStack.length === 0) {
|
if (this._undoStack.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._redoStack.push({ ...this._currentConfig() });
|
|
||||||
const config = this._undoStack.pop()!;
|
const config = this._undoStack.pop()!;
|
||||||
|
this._redoStack.push({ ...this._currentConfig(config) });
|
||||||
this._apply(config);
|
this._apply(config);
|
||||||
this._host.requestUpdate();
|
this._host.requestUpdate();
|
||||||
}
|
}
|
||||||
@@ -119,8 +121,8 @@ export class UndoRedoController<ConfigType> implements ReactiveController {
|
|||||||
if (this._redoStack.length === 0) {
|
if (this._redoStack.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._undoStack.push({ ...this._currentConfig() });
|
|
||||||
const config = this._redoStack.pop()!;
|
const config = this._redoStack.pop()!;
|
||||||
|
this._undoStack.push({ ...this._currentConfig(config) });
|
||||||
this._apply(config);
|
this._apply(config);
|
||||||
this._host.requestUpdate();
|
this._host.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|||||||
187
src/mixins/scrollable-fade-mixin.ts
Normal file
187
src/mixins/scrollable-fade-mixin.ts
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||||
|
import { css, html } from "lit";
|
||||||
|
import type {
|
||||||
|
CSSResultGroup,
|
||||||
|
LitElement,
|
||||||
|
PropertyValues,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import { state } from "lit/decorators";
|
||||||
|
import type { Constructor } from "../types";
|
||||||
|
|
||||||
|
const stylesArray = (styles?: CSSResultGroup | CSSResultGroup[]) =>
|
||||||
|
styles === undefined ? [] : Array.isArray(styles) ? styles : [styles];
|
||||||
|
|
||||||
|
export const ScrollableFadeMixin = <T extends Constructor<LitElement>>(
|
||||||
|
superClass: T
|
||||||
|
) => {
|
||||||
|
class ScrollableFadeClass extends superClass {
|
||||||
|
@state() protected _contentScrolled = false;
|
||||||
|
|
||||||
|
@state() protected _contentScrollable = false;
|
||||||
|
|
||||||
|
private _scrollTarget?: HTMLElement | null;
|
||||||
|
|
||||||
|
private _onScroll = (ev: Event) => {
|
||||||
|
const target = ev.currentTarget as HTMLElement;
|
||||||
|
this._contentScrolled = (target.scrollTop ?? 0) > 0;
|
||||||
|
this._updateScrollableState(target);
|
||||||
|
};
|
||||||
|
|
||||||
|
private _resize = new ResizeController(this, {
|
||||||
|
target: null,
|
||||||
|
callback: (entries) => {
|
||||||
|
const target = entries[0]?.target as HTMLElement | undefined;
|
||||||
|
if (target) {
|
||||||
|
this._updateScrollableState(target);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
private static readonly DEFAULT_SAFE_AREA_PADDING = 16;
|
||||||
|
|
||||||
|
private static readonly DEFAULT_SCROLLABLE_ELEMENT: HTMLElement | null =
|
||||||
|
null;
|
||||||
|
|
||||||
|
protected get scrollFadeSafeAreaPadding() {
|
||||||
|
return ScrollableFadeClass.DEFAULT_SAFE_AREA_PADDING;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected get scrollableElement(): HTMLElement | null {
|
||||||
|
return ScrollableFadeClass.DEFAULT_SCROLLABLE_ELEMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(changedProperties: PropertyValues) {
|
||||||
|
super.firstUpdated?.(changedProperties);
|
||||||
|
this._attachScrollableElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProperties: PropertyValues) {
|
||||||
|
super.updated?.(changedProperties);
|
||||||
|
this._attachScrollableElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this._detachScrollableElement();
|
||||||
|
super.disconnectedCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected renderScrollableFades(rounded = false): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class=${classMap({
|
||||||
|
"fade-top": true,
|
||||||
|
rounded,
|
||||||
|
visible: this._contentScrolled,
|
||||||
|
})}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class=${classMap({
|
||||||
|
"fade-bottom": true,
|
||||||
|
rounded,
|
||||||
|
visible: this._contentScrollable,
|
||||||
|
})}
|
||||||
|
></div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
const superCtor = Object.getPrototypeOf(this) as
|
||||||
|
| typeof LitElement
|
||||||
|
| undefined;
|
||||||
|
const inheritedStyles = stylesArray(
|
||||||
|
(superCtor?.styles ?? []) as CSSResultGroup | CSSResultGroup[]
|
||||||
|
);
|
||||||
|
return [
|
||||||
|
...inheritedStyles,
|
||||||
|
css`
|
||||||
|
.fade-top,
|
||||||
|
.fade-bottom {
|
||||||
|
position: absolute;
|
||||||
|
left: var(--ha-space-0);
|
||||||
|
right: var(--ha-space-0);
|
||||||
|
height: var(--ha-space-4);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 180ms ease-in-out;
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
var(--shadow-color),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
border-radius: var(--ha-border-radius-square);
|
||||||
|
z-index: 100;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.fade-top {
|
||||||
|
top: var(--ha-space-0);
|
||||||
|
}
|
||||||
|
.fade-bottom {
|
||||||
|
bottom: var(--ha-space-0);
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-top.visible,
|
||||||
|
.fade-bottom.visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-top.rounded,
|
||||||
|
.fade-bottom.rounded {
|
||||||
|
border-radius: var(
|
||||||
|
--ha-card-border-radius,
|
||||||
|
var(--ha-border-radius-lg)
|
||||||
|
);
|
||||||
|
border-bottom-left-radius: var(--ha-border-radius-square);
|
||||||
|
border-bottom-right-radius: var(--ha-border-radius-square);
|
||||||
|
}
|
||||||
|
.fade-top.rounded {
|
||||||
|
border-top-left-radius: var(--ha-border-radius-square);
|
||||||
|
border-top-right-radius: var(--ha-border-radius-square);
|
||||||
|
}
|
||||||
|
.fade-bottom.rounded {
|
||||||
|
border-bottom-left-radius: var(--ha-border-radius-square);
|
||||||
|
border-bottom-right-radius: var(--ha-border-radius-square);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _attachScrollableElement() {
|
||||||
|
const element = this.scrollableElement;
|
||||||
|
if (element === this._scrollTarget) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._detachScrollableElement();
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._scrollTarget = element;
|
||||||
|
element.addEventListener("scroll", this._onScroll, { passive: true });
|
||||||
|
this._resize.observe(element);
|
||||||
|
this._updateScrollableState(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _detachScrollableElement() {
|
||||||
|
if (!this._scrollTarget) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._scrollTarget.removeEventListener("scroll", this._onScroll);
|
||||||
|
this._resize.unobserve?.(this._scrollTarget);
|
||||||
|
this._scrollTarget = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _updateScrollableState(element: HTMLElement) {
|
||||||
|
const safeAreaInsetBottom =
|
||||||
|
parseFloat(
|
||||||
|
getComputedStyle(element).getPropertyValue("--safe-area-inset-bottom")
|
||||||
|
) || 0;
|
||||||
|
const { scrollHeight = 0, clientHeight = 0, scrollTop = 0 } = element;
|
||||||
|
this._contentScrollable =
|
||||||
|
scrollHeight - clientHeight >
|
||||||
|
scrollTop + safeAreaInsetBottom + this.scrollFadeSafeAreaPadding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ScrollableFadeClass;
|
||||||
|
};
|
||||||
@@ -299,7 +299,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
|||||||
},
|
},
|
||||||
area: {
|
area: {
|
||||||
title: localize("ui.panel.config.automation.picker.headers.area"),
|
title: localize("ui.panel.config.automation.picker.headers.area"),
|
||||||
defaultHidden: true,
|
|
||||||
groupable: true,
|
groupable: true,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
|
||||||
import { mdiClose, mdiDotsVertical } from "@mdi/js";
|
import { mdiClose, mdiDotsVertical } from "@mdi/js";
|
||||||
import type { PropertyValues } from "lit";
|
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import {
|
import { customElement, property, query } from "lit/decorators";
|
||||||
customElement,
|
|
||||||
eventOptions,
|
|
||||||
property,
|
|
||||||
query,
|
|
||||||
state,
|
|
||||||
} from "lit/decorators";
|
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||||
@@ -17,8 +9,10 @@ import "../../../../components/ha-dialog-header";
|
|||||||
import "../../../../components/ha-icon-button";
|
import "../../../../components/ha-icon-button";
|
||||||
import "../../../../components/ha-md-button-menu";
|
import "../../../../components/ha-md-button-menu";
|
||||||
import "../../../../components/ha-md-divider";
|
import "../../../../components/ha-md-divider";
|
||||||
|
import { haStyleScrollbar } from "../../../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import "../ha-automation-editor-warning";
|
import "../ha-automation-editor-warning";
|
||||||
|
import { ScrollableFadeMixin } from "../../../../mixins/scrollable-fade-mixin";
|
||||||
|
|
||||||
export interface SidebarOverflowMenuEntry {
|
export interface SidebarOverflowMenuEntry {
|
||||||
clickAction: () => void;
|
clickAction: () => void;
|
||||||
@@ -31,7 +25,9 @@ export interface SidebarOverflowMenuEntry {
|
|||||||
export type SidebarOverflowMenu = (SidebarOverflowMenuEntry | "separator")[];
|
export type SidebarOverflowMenu = (SidebarOverflowMenuEntry | "separator")[];
|
||||||
|
|
||||||
@customElement("ha-automation-sidebar-card")
|
@customElement("ha-automation-sidebar-card")
|
||||||
export default class HaAutomationSidebarCard extends LitElement {
|
export default class HaAutomationSidebarCard extends ScrollableFadeMixin(
|
||||||
|
LitElement
|
||||||
|
) {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ type: Boolean, attribute: "wide" }) public isWide = false;
|
@property({ type: Boolean, attribute: "wide" }) public isWide = false;
|
||||||
@@ -42,23 +38,10 @@ export default class HaAutomationSidebarCard extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public narrow = false;
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
@state() private _contentScrolled = false;
|
|
||||||
|
|
||||||
@state() private _contentScrollable = false;
|
|
||||||
|
|
||||||
@query(".card-content") private _contentElement!: HTMLDivElement;
|
@query(".card-content") private _contentElement!: HTMLDivElement;
|
||||||
|
|
||||||
private _contentSize = new ResizeController(this, {
|
protected get scrollableElement(): HTMLElement | null {
|
||||||
target: null,
|
return this._contentElement;
|
||||||
callback: (entries) => {
|
|
||||||
if (entries[0]?.target) {
|
|
||||||
this._canScrollDown(entries[0].target);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
protected firstUpdated(_changedProperties: PropertyValues): void {
|
|
||||||
this._contentSize.observe(this._contentElement);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
@@ -70,9 +53,7 @@ export default class HaAutomationSidebarCard extends LitElement {
|
|||||||
yaml: this.yamlMode,
|
yaml: this.yamlMode,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<ha-dialog-header
|
<ha-dialog-header>
|
||||||
class=${classMap({ scrolled: this._contentScrolled })}
|
|
||||||
>
|
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
slot="navigationIcon"
|
slot="navigationIcon"
|
||||||
.label=${this.hass.localize("ui.common.close")}
|
.label=${this.hass.localize("ui.common.close")}
|
||||||
@@ -107,34 +88,14 @@ export default class HaAutomationSidebarCard extends LitElement {
|
|||||||
>
|
>
|
||||||
</ha-automation-editor-warning>`
|
</ha-automation-editor-warning>`
|
||||||
: nothing}
|
: nothing}
|
||||||
<div class="card-content" @scroll=${this._onScroll}>
|
<div class="card-content ha-scrollbar">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
${this.renderScrollableFades(this.isWide)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class=${classMap({ fade: true, scrollable: this._contentScrollable })}
|
|
||||||
></div>
|
|
||||||
</ha-card>
|
</ha-card>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@eventOptions({ passive: true })
|
|
||||||
private _onScroll(ev) {
|
|
||||||
const top = ev.target.scrollTop ?? 0;
|
|
||||||
this._contentScrolled = top > 0;
|
|
||||||
|
|
||||||
this._canScrollDown(ev.target);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _canScrollDown(element: HTMLElement) {
|
|
||||||
const safeAreaInsetBottom =
|
|
||||||
parseFloat(
|
|
||||||
getComputedStyle(element).getPropertyValue("--safe-area-inset-bottom")
|
|
||||||
) || 0;
|
|
||||||
this._contentScrollable =
|
|
||||||
(element.scrollHeight ?? 0) - (element.clientHeight ?? 0) >
|
|
||||||
(element.scrollTop ?? 0) + safeAreaInsetBottom + 16;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _closeSidebar() {
|
private _closeSidebar() {
|
||||||
fireEvent(this, "close-sidebar");
|
fireEvent(this, "close-sidebar");
|
||||||
}
|
}
|
||||||
@@ -144,7 +105,11 @@ export default class HaAutomationSidebarCard extends LitElement {
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = css`
|
static get styles() {
|
||||||
|
return [
|
||||||
|
...super.styles,
|
||||||
|
haStyleScrollbar,
|
||||||
|
css`
|
||||||
ha-card {
|
ha-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -168,8 +133,6 @@ export default class HaAutomationSidebarCard extends LitElement {
|
|||||||
|
|
||||||
ha-dialog-header {
|
ha-dialog-header {
|
||||||
border-radius: var(--ha-card-border-radius);
|
border-radius: var(--ha-card-border-radius);
|
||||||
box-shadow: none;
|
|
||||||
transition: box-shadow 180ms ease-in-out;
|
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -179,32 +142,6 @@ export default class HaAutomationSidebarCard extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-dialog-header.scrolled {
|
|
||||||
box-shadow: var(--bar-box-shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 1px;
|
|
||||||
left: 1px;
|
|
||||||
right: 1px;
|
|
||||||
height: 16px;
|
|
||||||
pointer-events: none;
|
|
||||||
transition: box-shadow 180ms ease-in-out;
|
|
||||||
background-color: var(
|
|
||||||
--ha-dialog-surface-background,
|
|
||||||
var(--mdc-theme-surface, #fff)
|
|
||||||
);
|
|
||||||
transform: rotate(180deg);
|
|
||||||
border-radius: var(--ha-card-border-radius);
|
|
||||||
border-bottom-left-radius: var(--ha-border-radius-square);
|
|
||||||
border-bottom-right-radius: var(--ha-border-radius-square);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade.scrollable {
|
|
||||||
box-shadow: var(--bar-box-shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-content {
|
.card-content {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -213,17 +150,18 @@ export default class HaAutomationSidebarCard extends LitElement {
|
|||||||
padding-bottom: max(var(--safe-area-inset-bottom, 0px), 32px);
|
padding-bottom: max(var(--safe-area-inset-bottom, 0px), 32px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and (max-width: 870px) {
|
.fade-top {
|
||||||
.fade {
|
top: var(--ha-space-17);
|
||||||
bottom: 0;
|
|
||||||
border-radius: var(--ha-border-radius-square);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 870px) {
|
||||||
.card-content {
|
.card-content {
|
||||||
padding-bottom: 42px;
|
padding-bottom: 42px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -271,7 +271,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
|||||||
},
|
},
|
||||||
area: {
|
area: {
|
||||||
title: localize("ui.panel.config.scene.picker.headers.area"),
|
title: localize("ui.panel.config.scene.picker.headers.area"),
|
||||||
defaultHidden: true,
|
|
||||||
groupable: true,
|
groupable: true,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
|||||||
@@ -281,7 +281,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
|||||||
},
|
},
|
||||||
area: {
|
area: {
|
||||||
title: localize("ui.panel.config.script.picker.headers.area"),
|
title: localize("ui.panel.config.script.picker.headers.area"),
|
||||||
defaultHidden: true,
|
|
||||||
groupable: true,
|
groupable: true,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { formatTimeWithSeconds } from "../../common/datetime/format_time";
|
|||||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { computeDomain } from "../../common/entity/compute_domain";
|
import { computeDomain } from "../../common/entity/compute_domain";
|
||||||
import { DEFAULT_ENTITY_NAME } from "../../common/entity/compute_entity_name_display";
|
|
||||||
import { navigate } from "../../common/navigate";
|
import { navigate } from "../../common/navigate";
|
||||||
import { computeTimelineColor } from "../../components/chart/timeline-color";
|
import { computeTimelineColor } from "../../components/chart/timeline-color";
|
||||||
import "../../components/entity/state-badge";
|
import "../../components/entity/state-badge";
|
||||||
@@ -464,24 +463,15 @@ class HaLogbookRenderer extends LitElement {
|
|||||||
entityName: string | undefined,
|
entityName: string | undefined,
|
||||||
noLink?: boolean
|
noLink?: boolean
|
||||||
) {
|
) {
|
||||||
if (!entityId) {
|
const hasState = entityId && entityId in this.hass.states;
|
||||||
return entityName || "";
|
const displayName =
|
||||||
}
|
|
||||||
|
|
||||||
const stateObj = this.hass.states[entityId];
|
|
||||||
const hasState = Boolean(stateObj);
|
|
||||||
|
|
||||||
const displayName = hasState
|
|
||||||
? this.hass.formatEntityName(stateObj, DEFAULT_ENTITY_NAME) ||
|
|
||||||
entityName ||
|
entityName ||
|
||||||
stateObj.attributes.friendly_name ||
|
(hasState
|
||||||
entityId
|
? this.hass.states[entityId].attributes.friendly_name || entityId
|
||||||
: entityName || entityId;
|
: entityId);
|
||||||
|
|
||||||
if (!hasState) {
|
if (!hasState) {
|
||||||
return displayName;
|
return displayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
return noLink
|
return noLink
|
||||||
? displayName
|
? displayName
|
||||||
: html`<button
|
: html`<button
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { HomeAssistant } from "../../../types";
|
|||||||
import type { Lovelace } from "../types";
|
import type { Lovelace } from "../types";
|
||||||
import { deleteBadge } from "./config-util";
|
import { deleteBadge } from "./config-util";
|
||||||
import type { LovelaceCardPath } from "./lovelace-path";
|
import type { LovelaceCardPath } from "./lovelace-path";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
|
||||||
export interface DeleteBadgeParams {
|
export interface DeleteBadgeParams {
|
||||||
path: LovelaceCardPath;
|
path: LovelaceCardPath;
|
||||||
@@ -23,14 +24,13 @@ export async function performDeleteBadge(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const action = async () => {
|
|
||||||
lovelace.saveConfig(oldConfig);
|
|
||||||
};
|
|
||||||
|
|
||||||
lovelace.showToast({
|
lovelace.showToast({
|
||||||
message: hass.localize("ui.common.successfully_deleted"),
|
message: hass.localize("ui.common.successfully_deleted"),
|
||||||
duration: 8000,
|
duration: 8000,
|
||||||
action: { action, text: hass.localize("ui.common.undo") },
|
action: {
|
||||||
|
action: () => fireEvent(window, "undo-change"),
|
||||||
|
text: hass.localize("ui.common.undo"),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { HomeAssistant } from "../../../types";
|
|||||||
import type { Lovelace } from "../types";
|
import type { Lovelace } from "../types";
|
||||||
import { deleteCard } from "./config-util";
|
import { deleteCard } from "./config-util";
|
||||||
import type { LovelaceCardPath } from "./lovelace-path";
|
import type { LovelaceCardPath } from "./lovelace-path";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
|
||||||
export interface DeleteCardParams {
|
export interface DeleteCardParams {
|
||||||
path: LovelaceCardPath;
|
path: LovelaceCardPath;
|
||||||
@@ -23,14 +24,13 @@ export async function performDeleteCard(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const action = async () => {
|
|
||||||
lovelace.saveConfig(oldConfig);
|
|
||||||
};
|
|
||||||
|
|
||||||
lovelace.showToast({
|
lovelace.showToast({
|
||||||
message: hass.localize("ui.common.successfully_deleted"),
|
message: hass.localize("ui.common.successfully_deleted"),
|
||||||
duration: 8000,
|
duration: 8000,
|
||||||
action: { action, text: hass.localize("ui.common.undo") },
|
action: {
|
||||||
|
action: () => fireEvent(window, "undo-change"),
|
||||||
|
text: hass.localize("ui.common.undo"),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ import {
|
|||||||
mdiMagnify,
|
mdiMagnify,
|
||||||
mdiPencil,
|
mdiPencil,
|
||||||
mdiPlus,
|
mdiPlus,
|
||||||
|
mdiRedo,
|
||||||
mdiRefresh,
|
mdiRefresh,
|
||||||
mdiRobot,
|
mdiRobot,
|
||||||
mdiShape,
|
mdiShape,
|
||||||
mdiSofa,
|
mdiSofa,
|
||||||
|
mdiUndo,
|
||||||
mdiViewDashboard,
|
mdiViewDashboard,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||||
@@ -50,7 +52,10 @@ import "../../components/ha-tab-group-tab";
|
|||||||
import "../../components/ha-tooltip";
|
import "../../components/ha-tooltip";
|
||||||
import { createAreaRegistryEntry } from "../../data/area_registry";
|
import { createAreaRegistryEntry } from "../../data/area_registry";
|
||||||
import type { LovelacePanelConfig } from "../../data/lovelace";
|
import type { LovelacePanelConfig } from "../../data/lovelace";
|
||||||
import type { LovelaceConfig } from "../../data/lovelace/config/types";
|
import type {
|
||||||
|
LovelaceConfig,
|
||||||
|
LovelaceRawConfig,
|
||||||
|
} from "../../data/lovelace/config/types";
|
||||||
import { isStrategyDashboard } from "../../data/lovelace/config/types";
|
import { isStrategyDashboard } from "../../data/lovelace/config/types";
|
||||||
import type { LovelaceViewConfig } from "../../data/lovelace/config/view";
|
import type { LovelaceViewConfig } from "../../data/lovelace/config/view";
|
||||||
import {
|
import {
|
||||||
@@ -92,6 +97,7 @@ import "./views/hui-view";
|
|||||||
import type { HUIView } from "./views/hui-view";
|
import type { HUIView } from "./views/hui-view";
|
||||||
import "./views/hui-view-background";
|
import "./views/hui-view-background";
|
||||||
import "./views/hui-view-container";
|
import "./views/hui-view-container";
|
||||||
|
import { UndoRedoController } from "../../common/controllers/undo-redo-controller";
|
||||||
|
|
||||||
interface ActionItem {
|
interface ActionItem {
|
||||||
icon: string;
|
icon: string;
|
||||||
@@ -113,6 +119,11 @@ interface SubActionItem {
|
|||||||
visible: boolean | undefined;
|
visible: boolean | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UndoStackItem {
|
||||||
|
location: string;
|
||||||
|
config: LovelaceRawConfig;
|
||||||
|
}
|
||||||
|
|
||||||
@customElement("hui-root")
|
@customElement("hui-root")
|
||||||
class HUIRoot extends LitElement {
|
class HUIRoot extends LitElement {
|
||||||
@property({ attribute: false }) public panel?: PanelInfo<LovelacePanelConfig>;
|
@property({ attribute: false }) public panel?: PanelInfo<LovelacePanelConfig>;
|
||||||
@@ -130,12 +141,22 @@ class HUIRoot extends LitElement {
|
|||||||
|
|
||||||
@state() private _curView?: number | "hass-unused-entities";
|
@state() private _curView?: number | "hass-unused-entities";
|
||||||
|
|
||||||
|
private _configChangedByUndo = false;
|
||||||
|
|
||||||
private _viewCache?: Record<string, HUIView>;
|
private _viewCache?: Record<string, HUIView>;
|
||||||
|
|
||||||
private _viewScrollPositions: Record<string, number> = {};
|
private _viewScrollPositions: Record<string, number> = {};
|
||||||
|
|
||||||
private _restoreScroll = false;
|
private _restoreScroll = false;
|
||||||
|
|
||||||
|
private _undoRedoController = new UndoRedoController<UndoStackItem>(this, {
|
||||||
|
apply: (config) => this._applyUndoRedo(config),
|
||||||
|
currentConfig: (itemBeingApplied) => ({
|
||||||
|
location: itemBeingApplied?.location ?? this.route!.path,
|
||||||
|
config: this.lovelace!.rawConfig,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
private _debouncedConfigChanged: () => void;
|
private _debouncedConfigChanged: () => void;
|
||||||
|
|
||||||
private _conversation = memoizeOne((_components) =>
|
private _conversation = memoizeOne((_components) =>
|
||||||
@@ -157,7 +178,29 @@ class HUIRoot extends LitElement {
|
|||||||
const result: TemplateResult[] = [];
|
const result: TemplateResult[] = [];
|
||||||
if (this._editMode) {
|
if (this._editMode) {
|
||||||
result.push(
|
result.push(
|
||||||
html`<ha-button
|
html`<ha-icon-button
|
||||||
|
slot="toolbar-icon"
|
||||||
|
.path=${mdiUndo}
|
||||||
|
@click=${this._undo}
|
||||||
|
.disabled=${!this._undoRedoController.canUndo}
|
||||||
|
id="button-undo"
|
||||||
|
>
|
||||||
|
</ha-icon-button>
|
||||||
|
<ha-tooltip placement="bottom" for="button-undo">
|
||||||
|
${this.hass.localize("ui.common.undo")}
|
||||||
|
</ha-tooltip>
|
||||||
|
<ha-icon-button
|
||||||
|
slot="toolbar-icon"
|
||||||
|
.path=${mdiRedo}
|
||||||
|
@click=${this._redo}
|
||||||
|
.disabled=${!this._undoRedoController.canRedo}
|
||||||
|
id="button-redo"
|
||||||
|
>
|
||||||
|
</ha-icon-button>
|
||||||
|
<ha-tooltip placement="bottom" for="button-redo">
|
||||||
|
${this.hass.localize("ui.common.redo")}
|
||||||
|
</ha-tooltip>
|
||||||
|
<ha-button
|
||||||
appearance="filled"
|
appearance="filled"
|
||||||
size="small"
|
size="small"
|
||||||
class="exit-edit-mode"
|
class="exit-edit-mode"
|
||||||
@@ -645,6 +688,27 @@ class HUIRoot extends LitElement {
|
|||||||
window.history.scrollRestoration = "auto";
|
window.history.scrollRestoration = "auto";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected willUpdate(changedProperties: PropertyValues): void {
|
||||||
|
if (changedProperties.has("lovelace")) {
|
||||||
|
const oldLovelace = changedProperties.get("lovelace") as
|
||||||
|
| Lovelace
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
if (
|
||||||
|
oldLovelace &&
|
||||||
|
this.lovelace!.rawConfig !== oldLovelace!.rawConfig &&
|
||||||
|
!this._configChangedByUndo
|
||||||
|
) {
|
||||||
|
this._undoRedoController.commit({
|
||||||
|
location: this.route!.path,
|
||||||
|
config: oldLovelace.rawConfig,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._configChangedByUndo = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected updated(changedProperties: PropertyValues): void {
|
protected updated(changedProperties: PropertyValues): void {
|
||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
|
|
||||||
@@ -1029,6 +1093,7 @@ class HUIRoot extends LitElement {
|
|||||||
|
|
||||||
private _editModeDisable(): void {
|
private _editModeDisable(): void {
|
||||||
this.lovelace!.setEditMode(false);
|
this.lovelace!.setEditMode(false);
|
||||||
|
this._undoRedoController.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _editDashboard() {
|
private async _editDashboard() {
|
||||||
@@ -1207,6 +1272,36 @@ class HUIRoot extends LitElement {
|
|||||||
showShortcutsDialog(this);
|
showShortcutsDialog(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _applyUndoRedo(item: UndoStackItem) {
|
||||||
|
this._configChangedByUndo = true;
|
||||||
|
try {
|
||||||
|
await this.lovelace!.saveConfig(item.config);
|
||||||
|
} catch (err: any) {
|
||||||
|
this._configChangedByUndo = false;
|
||||||
|
showToast(this, {
|
||||||
|
message: this.hass.localize(
|
||||||
|
"ui.panel.lovelace.editor.undo_redo_failed_to_apply_changes",
|
||||||
|
{
|
||||||
|
error: err.message,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
duration: 4000,
|
||||||
|
dismissable: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._navigateToView(item.location);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _undo() {
|
||||||
|
this._undoRedoController.undo();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _redo() {
|
||||||
|
this._undoRedoController.redo();
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export const colorStyles = css`
|
|||||||
--divider-color: rgba(0, 0, 0, 0.12);
|
--divider-color: rgba(0, 0, 0, 0.12);
|
||||||
--outline-color: rgba(0, 0, 0, 0.12);
|
--outline-color: rgba(0, 0, 0, 0.12);
|
||||||
--outline-hover-color: rgba(0, 0, 0, 0.24);
|
--outline-hover-color: rgba(0, 0, 0, 0.24);
|
||||||
|
--shadow-color: rgba(0, 0, 0, 0.16);
|
||||||
|
|
||||||
/* rgb */
|
/* rgb */
|
||||||
--rgb-primary-color: 0, 154, 199;
|
--rgb-primary-color: 0, 154, 199;
|
||||||
@@ -224,7 +225,7 @@ export const colorStyles = css`
|
|||||||
--table-row-alternative-background-color: var(--secondary-background-color);
|
--table-row-alternative-background-color: var(--secondary-background-color);
|
||||||
--data-table-background-color: var(--card-background-color);
|
--data-table-background-color: var(--card-background-color);
|
||||||
--markdown-code-background-color: var(--primary-background-color);
|
--markdown-code-background-color: var(--primary-background-color);
|
||||||
--bar-box-shadow: 0 2px 12px rgba(0, 0, 0, 0.16);
|
--bar-box-shadow: 0 2px 12px var(--shadow-color);
|
||||||
|
|
||||||
/* https://github.com/material-components/material-web/blob/master/docs/theming.md */
|
/* https://github.com/material-components/material-web/blob/master/docs/theming.md */
|
||||||
--mdc-theme-primary: var(--primary-color);
|
--mdc-theme-primary: var(--primary-color);
|
||||||
@@ -307,6 +308,8 @@ export const darkColorStyles = css`
|
|||||||
--divider-color: rgba(225, 225, 225, 0.12);
|
--divider-color: rgba(225, 225, 225, 0.12);
|
||||||
--outline-color: rgba(225, 225, 225, 0.12);
|
--outline-color: rgba(225, 225, 225, 0.12);
|
||||||
--outline-hover-color: rgba(225, 225, 225, 0.24);
|
--outline-hover-color: rgba(225, 225, 225, 0.24);
|
||||||
|
--shadow-color: rgba(0, 0, 0, 0.48);
|
||||||
|
|
||||||
--mdc-ripple-color: #aaaaaa;
|
--mdc-ripple-color: #aaaaaa;
|
||||||
--mdc-linear-progress-buffer-color: rgba(255, 255, 255, 0.1);
|
--mdc-linear-progress-buffer-color: rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
@@ -350,7 +353,7 @@ export const darkColorStyles = css`
|
|||||||
--ha-button-neutral-color: #d9dae0;
|
--ha-button-neutral-color: #d9dae0;
|
||||||
--ha-button-neutral-light-color: #6a7081;
|
--ha-button-neutral-light-color: #6a7081;
|
||||||
|
|
||||||
--bar-box-shadow: 0 2px 12px rgba(0, 0, 0, 0.48);
|
--bar-box-shadow: 0 2px 12px var(--shadow-color);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -7304,6 +7304,7 @@
|
|||||||
"editor": {
|
"editor": {
|
||||||
"header": "Edit UI",
|
"header": "Edit UI",
|
||||||
"yaml_unsupported": "The edit UI is not available when in YAML mode.",
|
"yaml_unsupported": "The edit UI is not available when in YAML mode.",
|
||||||
|
"undo_redo_failed_to_apply_changes": "Unable to apply changes: {error}",
|
||||||
"menu": {
|
"menu": {
|
||||||
"open": "Open dashboard menu",
|
"open": "Open dashboard menu",
|
||||||
"raw_editor": "Raw configuration editor",
|
"raw_editor": "Raw configuration editor",
|
||||||
|
|||||||
Reference in New Issue
Block a user