mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-29 04:06:35 +00:00
Add full-screen button to code editors (#25903)
This commit is contained in:
parent
fcbc8de95a
commit
884341656f
@ -6,6 +6,7 @@ import type {
|
|||||||
} from "@codemirror/autocomplete";
|
} from "@codemirror/autocomplete";
|
||||||
import type { Extension, TransactionSpec } from "@codemirror/state";
|
import type { Extension, TransactionSpec } from "@codemirror/state";
|
||||||
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
|
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
|
||||||
|
import { mdiArrowExpand, mdiArrowCollapse } from "@mdi/js";
|
||||||
import type { HassEntities } from "home-assistant-js-websocket";
|
import type { HassEntities } from "home-assistant-js-websocket";
|
||||||
import type { PropertyValues } from "lit";
|
import type { PropertyValues } from "lit";
|
||||||
import { css, ReactiveElement } from "lit";
|
import { css, ReactiveElement } from "lit";
|
||||||
@ -15,6 +16,7 @@ import { fireEvent } from "../common/dom/fire_event";
|
|||||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import "./ha-icon";
|
import "./ha-icon";
|
||||||
|
import "./ha-icon-button";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
@ -59,8 +61,13 @@ export class HaCodeEditor extends ReactiveElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public error = false;
|
@property({ type: Boolean }) public error = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "enable-fullscreen" })
|
||||||
|
public enableFullscreen = true;
|
||||||
|
|
||||||
@state() private _value = "";
|
@state() private _value = "";
|
||||||
|
|
||||||
|
@state() private _isFullscreen = false;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||||
private _loadedCodeMirror?: typeof import("../resources/codemirror");
|
private _loadedCodeMirror?: typeof import("../resources/codemirror");
|
||||||
|
|
||||||
@ -92,6 +99,7 @@ export class HaCodeEditor extends ReactiveElement {
|
|||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
this.addEventListener("keydown", stopPropagation);
|
this.addEventListener("keydown", stopPropagation);
|
||||||
|
this.addEventListener("keydown", this._handleKeyDown);
|
||||||
// This is unreachable as editor will not exist yet,
|
// This is unreachable as editor will not exist yet,
|
||||||
// but focus should not behave like this for good a11y.
|
// but focus should not behave like this for good a11y.
|
||||||
// (@steverep to fix in autofocus PR)
|
// (@steverep to fix in autofocus PR)
|
||||||
@ -106,6 +114,10 @@ export class HaCodeEditor extends ReactiveElement {
|
|||||||
public disconnectedCallback() {
|
public disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
this.removeEventListener("keydown", stopPropagation);
|
this.removeEventListener("keydown", stopPropagation);
|
||||||
|
this.removeEventListener("keydown", this._handleKeyDown);
|
||||||
|
if (this._isFullscreen) {
|
||||||
|
this._toggleFullscreen();
|
||||||
|
}
|
||||||
this.updateComplete.then(() => {
|
this.updateComplete.then(() => {
|
||||||
this.codemirror!.destroy();
|
this.codemirror!.destroy();
|
||||||
delete this.codemirror;
|
delete this.codemirror;
|
||||||
@ -164,6 +176,12 @@ export class HaCodeEditor extends ReactiveElement {
|
|||||||
if (changedProps.has("error")) {
|
if (changedProps.has("error")) {
|
||||||
this.classList.toggle("error-state", this.error);
|
this.classList.toggle("error-state", this.error);
|
||||||
}
|
}
|
||||||
|
if (changedProps.has("_isFullscreen")) {
|
||||||
|
this.classList.toggle("fullscreen", this._isFullscreen);
|
||||||
|
}
|
||||||
|
if (changedProps.has("enableFullscreen")) {
|
||||||
|
this._updateFullscreenButton();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _mode() {
|
private get _mode() {
|
||||||
@ -238,8 +256,74 @@ export class HaCodeEditor extends ReactiveElement {
|
|||||||
}),
|
}),
|
||||||
parent: this.renderRoot,
|
parent: this.renderRoot,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this._updateFullscreenButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _updateFullscreenButton() {
|
||||||
|
const existingButton = this.renderRoot.querySelector(".fullscreen-button");
|
||||||
|
|
||||||
|
if (!this.enableFullscreen) {
|
||||||
|
// Remove button if it exists and fullscreen is disabled
|
||||||
|
if (existingButton) {
|
||||||
|
existingButton.remove();
|
||||||
|
}
|
||||||
|
// Exit fullscreen if currently in fullscreen mode
|
||||||
|
if (this._isFullscreen) {
|
||||||
|
this._isFullscreen = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create button if it doesn't exist
|
||||||
|
if (!existingButton) {
|
||||||
|
const button = document.createElement("ha-icon-button");
|
||||||
|
(button as any).path = this._isFullscreen
|
||||||
|
? mdiArrowCollapse
|
||||||
|
: mdiArrowExpand;
|
||||||
|
button.setAttribute(
|
||||||
|
"label",
|
||||||
|
this._isFullscreen ? "Exit fullscreen" : "Enter fullscreen"
|
||||||
|
);
|
||||||
|
button.classList.add("fullscreen-button");
|
||||||
|
// Use bound method to ensure proper this context
|
||||||
|
button.addEventListener("click", this._handleFullscreenClick);
|
||||||
|
this.renderRoot.appendChild(button);
|
||||||
|
} else {
|
||||||
|
// Update existing button
|
||||||
|
(existingButton as any).path = this._isFullscreen
|
||||||
|
? mdiArrowCollapse
|
||||||
|
: mdiArrowExpand;
|
||||||
|
existingButton.setAttribute(
|
||||||
|
"label",
|
||||||
|
this._isFullscreen ? "Exit fullscreen" : "Enter fullscreen"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleFullscreenClick = (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this._toggleFullscreen();
|
||||||
|
};
|
||||||
|
|
||||||
|
private _toggleFullscreen() {
|
||||||
|
this._isFullscreen = !this._isFullscreen;
|
||||||
|
this._updateFullscreenButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (this._isFullscreen && e.key === "Escape") {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this._toggleFullscreen();
|
||||||
|
} else if (e.key === "F11" && this.enableFullscreen) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this._toggleFullscreen();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private _getStates = memoizeOne((states: HassEntities): Completion[] => {
|
private _getStates = memoizeOne((states: HassEntities): Completion[] => {
|
||||||
if (!states) {
|
if (!states) {
|
||||||
return [];
|
return [];
|
||||||
@ -460,9 +544,78 @@ export class HaCodeEditor extends ReactiveElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
:host(.error-state) .cm-gutters {
|
:host(.error-state) .cm-gutters {
|
||||||
border-color: var(--error-state-color, red);
|
border-color: var(--error-state-color, red);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fullscreen-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
z-index: 10;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
background-color: var(--card-background-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
--mdc-icon-button-size: 32px;
|
||||||
|
--mdc-icon-size: 18px;
|
||||||
|
/* Ensure button is clickable on iOS */
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-button:hover,
|
||||||
|
.fullscreen-button:active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: none) {
|
||||||
|
.fullscreen-button {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(.fullscreen) {
|
||||||
|
position: fixed !important;
|
||||||
|
top: var(--header-height, 56px) !important;
|
||||||
|
left: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
|
bottom: 0 !important;
|
||||||
|
z-index: 9999 !important;
|
||||||
|
background-color: var(--primary-background-color) !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 16px !important;
|
||||||
|
/* Respect iOS safe areas while accounting for header */
|
||||||
|
padding-top: max(16px, env(safe-area-inset-top)) !important;
|
||||||
|
padding-left: max(16px, env(safe-area-inset-left)) !important;
|
||||||
|
padding-right: max(16px, env(safe-area-inset-right)) !important;
|
||||||
|
padding-bottom: max(16px, env(safe-area-inset-bottom)) !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(.fullscreen) .cm-editor {
|
||||||
|
height: 100% !important;
|
||||||
|
max-height: 100% !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(.fullscreen) .fullscreen-button {
|
||||||
|
position: fixed;
|
||||||
|
top: calc(
|
||||||
|
var(--header-height, 56px) + max(8px, env(safe-area-inset-top))
|
||||||
|
);
|
||||||
|
right: max(24px, calc(env(safe-area-inset-right) + 8px));
|
||||||
|
z-index: 10000;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user