mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-20 21:21:44 +01:00
Web Search (#1631)
Adds support for Cmd:f, Ctrl:f, and Alt:f to activate search in the Web and Help widgets
This commit is contained in:
parent
6de98ac3fb
commit
477052e8fc
@ -1,19 +1,18 @@
|
|||||||
import { autoUpdate, FloatingPortal, Middleware, offset, useDismiss, useFloating } from "@floating-ui/react";
|
import { autoUpdate, FloatingPortal, Middleware, offset, useDismiss, useFloating } from "@floating-ui/react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { atom, PrimitiveAtom, useAtom, useAtomValue } from "jotai";
|
import { atom, useAtom } from "jotai";
|
||||||
import { memo, useCallback, useRef, useState } from "react";
|
import { memo, useCallback, useEffect, useMemo, useRef } from "react";
|
||||||
import { IconButton } from "./iconbutton";
|
import { IconButton } from "./iconbutton";
|
||||||
import { Input } from "./input";
|
import { Input } from "./input";
|
||||||
import "./search.scss";
|
import "./search.scss";
|
||||||
|
|
||||||
type SearchProps = {
|
type SearchProps = SearchAtoms & {
|
||||||
searchAtom: PrimitiveAtom<string>;
|
|
||||||
indexAtom: PrimitiveAtom<number>;
|
|
||||||
numResultsAtom: PrimitiveAtom<number>;
|
|
||||||
isOpenAtom: PrimitiveAtom<boolean>;
|
|
||||||
anchorRef?: React.RefObject<HTMLElement>;
|
anchorRef?: React.RefObject<HTMLElement>;
|
||||||
offsetX?: number;
|
offsetX?: number;
|
||||||
offsetY?: number;
|
offsetY?: number;
|
||||||
|
onSearch?: (search: string) => void;
|
||||||
|
onNext?: () => void;
|
||||||
|
onPrev?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SearchComponent = ({
|
const SearchComponent = ({
|
||||||
@ -24,23 +23,54 @@ const SearchComponent = ({
|
|||||||
anchorRef,
|
anchorRef,
|
||||||
offsetX = 10,
|
offsetX = 10,
|
||||||
offsetY = 10,
|
offsetY = 10,
|
||||||
|
onSearch,
|
||||||
|
onNext,
|
||||||
|
onPrev,
|
||||||
}: SearchProps) => {
|
}: SearchProps) => {
|
||||||
const [isOpen, setIsOpen] = useAtom(isOpenAtom);
|
const [isOpen, setIsOpen] = useAtom<boolean>(isOpenAtom);
|
||||||
const [search, setSearch] = useAtom(searchAtom);
|
const [search, setSearch] = useAtom<string>(searchAtom);
|
||||||
const [index, setIndex] = useAtom(indexAtom);
|
const [index, setIndex] = useAtom<number>(indexAtom);
|
||||||
const numResults = useAtomValue(numResultsAtom);
|
const [numResults, setNumResults] = useAtom<number>(numResultsAtom);
|
||||||
|
|
||||||
const handleOpenChange = useCallback((open: boolean) => {
|
const handleOpenChange = useCallback((open: boolean) => {
|
||||||
setIsOpen(open);
|
setIsOpen(open);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSearch("");
|
||||||
|
setIndex(0);
|
||||||
|
setNumResults(0);
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIndex(0);
|
||||||
|
setNumResults(0);
|
||||||
|
onSearch?.(search);
|
||||||
|
}, [search]);
|
||||||
|
|
||||||
const middleware: Middleware[] = [];
|
const middleware: Middleware[] = [];
|
||||||
middleware.push(
|
const offsetCallback = useCallback(
|
||||||
offset(({ rects }) => ({
|
({ rects }) => {
|
||||||
mainAxis: -rects.floating.height - offsetY,
|
const docRect = document.documentElement.getBoundingClientRect();
|
||||||
crossAxis: -offsetX,
|
let yOffsetCalc = -rects.floating.height - offsetY;
|
||||||
}))
|
let xOffsetCalc = -offsetX;
|
||||||
|
const floatingBottom = rects.reference.y + rects.floating.height + offsetY;
|
||||||
|
const floatingLeft = rects.reference.x + rects.reference.width - (rects.floating.width + offsetX);
|
||||||
|
if (floatingBottom > docRect.bottom) {
|
||||||
|
yOffsetCalc -= docRect.bottom - floatingBottom;
|
||||||
|
}
|
||||||
|
if (floatingLeft < 5) {
|
||||||
|
xOffsetCalc += 5 - floatingLeft;
|
||||||
|
}
|
||||||
|
console.log("offsetCalc", yOffsetCalc, xOffsetCalc);
|
||||||
|
return {
|
||||||
|
mainAxis: yOffsetCalc,
|
||||||
|
crossAxis: xOffsetCalc,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[offsetX, offsetY]
|
||||||
);
|
);
|
||||||
|
middleware.push(offset(offsetCallback));
|
||||||
|
|
||||||
const { refs, floatingStyles, context } = useFloating({
|
const { refs, floatingStyles, context } = useFloating({
|
||||||
placement: "top-end",
|
placement: "top-end",
|
||||||
@ -55,26 +85,47 @@ const SearchComponent = ({
|
|||||||
|
|
||||||
const dismiss = useDismiss(context);
|
const dismiss = useDismiss(context);
|
||||||
|
|
||||||
|
const onPrevWrapper = useCallback(
|
||||||
|
() => (onPrev ? onPrev() : setIndex((index - 1) % numResults)),
|
||||||
|
[onPrev, index, numResults]
|
||||||
|
);
|
||||||
|
const onNextWrapper = useCallback(
|
||||||
|
() => (onNext ? onNext() : setIndex((index + 1) % numResults)),
|
||||||
|
[onNext, index, numResults]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onKeyDown = useCallback(
|
||||||
|
(e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
onPrevWrapper();
|
||||||
|
} else {
|
||||||
|
onNextWrapper();
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onPrevWrapper, onNextWrapper, setIsOpen]
|
||||||
|
);
|
||||||
|
|
||||||
const prevDecl: IconButtonDecl = {
|
const prevDecl: IconButtonDecl = {
|
||||||
elemtype: "iconbutton",
|
elemtype: "iconbutton",
|
||||||
icon: "chevron-up",
|
icon: "chevron-up",
|
||||||
title: "Previous Result",
|
title: "Previous Result (Shift+Enter)",
|
||||||
disabled: index === 0,
|
click: onPrevWrapper,
|
||||||
click: () => setIndex(index - 1),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextDecl: IconButtonDecl = {
|
const nextDecl: IconButtonDecl = {
|
||||||
elemtype: "iconbutton",
|
elemtype: "iconbutton",
|
||||||
icon: "chevron-down",
|
icon: "chevron-down",
|
||||||
title: "Next Result",
|
title: "Next Result (Enter)",
|
||||||
disabled: !numResults || index === numResults - 1,
|
click: onNextWrapper,
|
||||||
click: () => setIndex(index + 1),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeDecl: IconButtonDecl = {
|
const closeDecl: IconButtonDecl = {
|
||||||
elemtype: "iconbutton",
|
elemtype: "iconbutton",
|
||||||
icon: "xmark-large",
|
icon: "xmark-large",
|
||||||
title: "Close",
|
title: "Close (Esc)",
|
||||||
click: () => setIsOpen(false),
|
click: () => setIsOpen(false),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -83,7 +134,13 @@ const SearchComponent = ({
|
|||||||
{isOpen && (
|
{isOpen && (
|
||||||
<FloatingPortal>
|
<FloatingPortal>
|
||||||
<div className="search-container" style={{ ...floatingStyles }} {...dismiss} ref={refs.setFloating}>
|
<div className="search-container" style={{ ...floatingStyles }} {...dismiss} ref={refs.setFloating}>
|
||||||
<Input placeholder="Search" value={search} onChange={setSearch} />
|
<Input
|
||||||
|
placeholder="Search"
|
||||||
|
value={search}
|
||||||
|
onChange={setSearch}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
className={clsx("search-results", { hidden: numResults === 0 })}
|
className={clsx("search-results", { hidden: numResults === 0 })}
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
@ -105,11 +162,16 @@ const SearchComponent = ({
|
|||||||
|
|
||||||
export const Search = memo(SearchComponent) as typeof SearchComponent;
|
export const Search = memo(SearchComponent) as typeof SearchComponent;
|
||||||
|
|
||||||
export function useSearch(anchorRef?: React.RefObject<HTMLElement>): SearchProps {
|
export function useSearch(anchorRef?: React.RefObject<HTMLElement>, viewModel?: ViewModel): SearchProps {
|
||||||
const [searchAtom] = useState(atom(""));
|
const searchAtoms: SearchAtoms = useMemo(
|
||||||
const [indexAtom] = useState(atom(0));
|
() => ({ searchAtom: atom(""), indexAtom: atom(0), numResultsAtom: atom(0), isOpenAtom: atom(false) }),
|
||||||
const [numResultsAtom] = useState(atom(0));
|
[]
|
||||||
const [isOpenAtom] = useState(atom(false));
|
);
|
||||||
anchorRef ??= useRef(null);
|
anchorRef ??= useRef(null);
|
||||||
return { searchAtom, indexAtom, numResultsAtom, isOpenAtom, anchorRef };
|
useEffect(() => {
|
||||||
|
if (viewModel) {
|
||||||
|
viewModel.searchAtoms = searchAtoms;
|
||||||
|
}
|
||||||
|
}, [viewModel]);
|
||||||
|
return { ...searchAtoms, anchorRef };
|
||||||
}
|
}
|
||||||
|
@ -321,6 +321,28 @@ function registerGlobalKeys() {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
function activateSearch(): boolean {
|
||||||
|
console.log("activateSearch");
|
||||||
|
const bcm = getBlockComponentModel(getFocusedBlockInStaticTab());
|
||||||
|
if (bcm.viewModel.searchAtoms) {
|
||||||
|
console.log("activateSearch2");
|
||||||
|
globalStore.set(bcm.viewModel.searchAtoms.isOpenAtom, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function deactivateSearch(): boolean {
|
||||||
|
console.log("deactivateSearch");
|
||||||
|
const bcm = getBlockComponentModel(getFocusedBlockInStaticTab());
|
||||||
|
if (bcm.viewModel.searchAtoms && globalStore.get(bcm.viewModel.searchAtoms.isOpenAtom)) {
|
||||||
|
globalStore.set(bcm.viewModel.searchAtoms.isOpenAtom, false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
globalKeyMap.set("Cmd:f", activateSearch);
|
||||||
|
globalKeyMap.set("Ctrl:f", activateSearch);
|
||||||
|
globalKeyMap.set("Escape", deactivateSearch);
|
||||||
const allKeys = Array.from(globalKeyMap.keys());
|
const allKeys = Array.from(globalKeyMap.keys());
|
||||||
// special case keys, handled by web view
|
// special case keys, handled by web view
|
||||||
allKeys.push("Cmd:l", "Cmd:r", "Cmd:ArrowRight", "Cmd:ArrowLeft");
|
allKeys.push("Cmd:l", "Cmd:r", "Cmd:ArrowRight", "Cmd:ArrowLeft");
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
// Copyright 2024, Command Line Inc.
|
// Copyright 2024, Command Line Inc.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
.webview {
|
.webview,
|
||||||
|
.webview-container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import { BlockNodeModel } from "@/app/block/blocktypes";
|
import { BlockNodeModel } from "@/app/block/blocktypes";
|
||||||
|
import { Search, useSearch } from "@/app/element/search";
|
||||||
import { getApi, getBlockMetaKeyAtom, getSettingsKeyAtom, openLink } from "@/app/store/global";
|
import { getApi, getBlockMetaKeyAtom, getSettingsKeyAtom, openLink } from "@/app/store/global";
|
||||||
import { getSimpleControlShiftAtom } from "@/app/store/keymodel";
|
import { getSimpleControlShiftAtom } from "@/app/store/keymodel";
|
||||||
import { ObjectService } from "@/app/store/services";
|
import { ObjectService } from "@/app/store/services";
|
||||||
@ -12,8 +13,8 @@ import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil"
|
|||||||
import { fireAndForget } from "@/util/util";
|
import { fireAndForget } from "@/util/util";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { WebviewTag } from "electron";
|
import { WebviewTag } from "electron";
|
||||||
import { Atom, PrimitiveAtom, atom, useAtomValue } from "jotai";
|
import { Atom, PrimitiveAtom, atom, useAtomValue, useSetAtom } from "jotai";
|
||||||
import { Fragment, createRef, memo, useEffect, useRef, useState } from "react";
|
import { Fragment, createRef, memo, useCallback, useEffect, useRef, useState } from "react";
|
||||||
import "./webview.scss";
|
import "./webview.scss";
|
||||||
|
|
||||||
let webviewPreloadUrl = null;
|
let webviewPreloadUrl = null;
|
||||||
@ -50,6 +51,7 @@ export class WebViewModel implements ViewModel {
|
|||||||
mediaMuted: PrimitiveAtom<boolean>;
|
mediaMuted: PrimitiveAtom<boolean>;
|
||||||
modifyExternalUrl?: (url: string) => string;
|
modifyExternalUrl?: (url: string) => string;
|
||||||
domReady: PrimitiveAtom<boolean>;
|
domReady: PrimitiveAtom<boolean>;
|
||||||
|
searchAtoms?: SearchAtoms;
|
||||||
|
|
||||||
constructor(blockId: string, nodeModel: BlockNodeModel) {
|
constructor(blockId: string, nodeModel: BlockNodeModel) {
|
||||||
this.nodeModel = nodeModel;
|
this.nodeModel = nodeModel;
|
||||||
@ -296,6 +298,9 @@ export class WebViewModel implements ViewModel {
|
|||||||
handleNavigate(url: string) {
|
handleNavigate(url: string) {
|
||||||
fireAndForget(() => ObjectService.UpdateObjectMeta(WOS.makeORef("block", this.blockId), { url }));
|
fireAndForget(() => ObjectService.UpdateObjectMeta(WOS.makeORef("block", this.blockId), { url }));
|
||||||
globalStore.set(this.url, url);
|
globalStore.set(this.url, url);
|
||||||
|
if (this.searchAtoms) {
|
||||||
|
globalStore.set(this.searchAtoms.isOpenAtom, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureUrlScheme(url: string, searchTemplate: string) {
|
ensureUrlScheme(url: string, searchTemplate: string) {
|
||||||
@ -389,6 +394,11 @@ export class WebViewModel implements ViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
giveFocus(): boolean {
|
giveFocus(): boolean {
|
||||||
|
console.log("webview giveFocus");
|
||||||
|
if (this.searchAtoms && globalStore.get(this.searchAtoms.isOpenAtom)) {
|
||||||
|
console.log("search is open, not giving focus");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const ctrlShiftState = globalStore.get(getSimpleControlShiftAtom());
|
const ctrlShiftState = globalStore.get(getSimpleControlShiftAtom());
|
||||||
if (ctrlShiftState) {
|
if (ctrlShiftState) {
|
||||||
// this is really weird, we don't get keyup events from webview
|
// this is really weird, we don't get keyup events from webview
|
||||||
@ -537,6 +547,49 @@ const WebView = memo(({ model, onFailLoad }: WebViewProps) => {
|
|||||||
const metaUrlRef = useRef(metaUrl);
|
const metaUrlRef = useRef(metaUrl);
|
||||||
const zoomFactor = useAtomValue(getBlockMetaKeyAtom(model.blockId, "web:zoom")) || 1;
|
const zoomFactor = useAtomValue(getBlockMetaKeyAtom(model.blockId, "web:zoom")) || 1;
|
||||||
|
|
||||||
|
// Search
|
||||||
|
const searchProps = useSearch(model.webviewRef, model);
|
||||||
|
const searchVal = useAtomValue<string>(searchProps.searchAtom);
|
||||||
|
const setSearchIndex = useSetAtom(searchProps.indexAtom);
|
||||||
|
const setNumSearchResults = useSetAtom(searchProps.numResultsAtom);
|
||||||
|
const onSearch = useCallback((search: string) => {
|
||||||
|
try {
|
||||||
|
if (search) {
|
||||||
|
model.webviewRef.current?.findInPage(search, { findNext: true });
|
||||||
|
} else {
|
||||||
|
model.webviewRef.current?.stopFindInPage("clearSelection");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to search", e);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
const onSearchNext = useCallback(() => {
|
||||||
|
try {
|
||||||
|
console.log("search next", searchVal);
|
||||||
|
model.webviewRef.current?.findInPage(searchVal, { findNext: false, forward: true });
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to search next", e);
|
||||||
|
}
|
||||||
|
}, [searchVal]);
|
||||||
|
const onSearchPrev = useCallback(() => {
|
||||||
|
try {
|
||||||
|
console.log("search prev", searchVal);
|
||||||
|
model.webviewRef.current?.findInPage(searchVal, { findNext: false, forward: false });
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to search prev", e);
|
||||||
|
}
|
||||||
|
}, [searchVal]);
|
||||||
|
const onFoundInPage = useCallback((event: any) => {
|
||||||
|
const result = event.result;
|
||||||
|
console.log("found in page", result);
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setNumSearchResults(result.matches);
|
||||||
|
setSearchIndex(result.activeMatchOrdinal - 1);
|
||||||
|
}, []);
|
||||||
|
// End Search
|
||||||
|
|
||||||
// The initial value of the block metadata URL when the component first renders. Used to set the starting src value for the webview.
|
// The initial value of the block metadata URL when the component first renders. Used to set the starting src value for the webview.
|
||||||
const [metaUrlInitial] = useState(metaUrl);
|
const [metaUrlInitial] = useState(metaUrl);
|
||||||
|
|
||||||
@ -669,6 +722,7 @@ const WebView = memo(({ model, onFailLoad }: WebViewProps) => {
|
|||||||
webview.addEventListener("dom-ready", handleDomReady);
|
webview.addEventListener("dom-ready", handleDomReady);
|
||||||
webview.addEventListener("media-started-playing", handleMediaPlaying);
|
webview.addEventListener("media-started-playing", handleMediaPlaying);
|
||||||
webview.addEventListener("media-paused", handleMediaPaused);
|
webview.addEventListener("media-paused", handleMediaPaused);
|
||||||
|
webview.addEventListener("found-in-page", onFoundInPage);
|
||||||
|
|
||||||
// Clean up event listeners on component unmount
|
// Clean up event listeners on component unmount
|
||||||
return () => {
|
return () => {
|
||||||
@ -684,6 +738,7 @@ const WebView = memo(({ model, onFailLoad }: WebViewProps) => {
|
|||||||
webview.removeEventListener("dom-ready", handleDomReady);
|
webview.removeEventListener("dom-ready", handleDomReady);
|
||||||
webview.removeEventListener("media-started-playing", handleMediaPlaying);
|
webview.removeEventListener("media-started-playing", handleMediaPlaying);
|
||||||
webview.removeEventListener("media-paused", handleMediaPaused);
|
webview.removeEventListener("media-paused", handleMediaPaused);
|
||||||
|
webview.removeEventListener("found-in-page", onFoundInPage);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -705,6 +760,7 @@ const WebView = memo(({ model, onFailLoad }: WebViewProps) => {
|
|||||||
<div>{errorText}</div>
|
<div>{errorText}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<Search {...searchProps} onSearch={onSearch} onNext={onSearchNext} onPrev={onSearchPrev} />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
10
frontend/types/custom.d.ts
vendored
10
frontend/types/custom.d.ts
vendored
@ -228,6 +228,13 @@ declare global {
|
|||||||
elemtype: "menubutton";
|
elemtype: "menubutton";
|
||||||
} & MenuButtonProps;
|
} & MenuButtonProps;
|
||||||
|
|
||||||
|
type SearchAtoms = {
|
||||||
|
searchAtom: PrimitiveAtom<string>;
|
||||||
|
indexAtom: PrimitiveAtom<number>;
|
||||||
|
numResultsAtom: PrimitiveAtom<number>;
|
||||||
|
isOpenAtom: PrimitiveAtom<boolean>;
|
||||||
|
};
|
||||||
|
|
||||||
interface ViewModel {
|
interface ViewModel {
|
||||||
viewType: string;
|
viewType: string;
|
||||||
viewIcon?: jotai.Atom<string | IconButtonDecl>;
|
viewIcon?: jotai.Atom<string | IconButtonDecl>;
|
||||||
@ -239,11 +246,10 @@ declare global {
|
|||||||
manageConnection?: jotai.Atom<boolean>;
|
manageConnection?: jotai.Atom<boolean>;
|
||||||
noPadding?: jotai.Atom<boolean>;
|
noPadding?: jotai.Atom<boolean>;
|
||||||
filterOutNowsh?: jotai.Atom<boolean>;
|
filterOutNowsh?: jotai.Atom<boolean>;
|
||||||
|
searchAtoms?: SearchAtoms;
|
||||||
|
|
||||||
onBack?: () => void;
|
onBack?: () => void;
|
||||||
onForward?: () => void;
|
onForward?: () => void;
|
||||||
onSearchChange?: (text: string) => void;
|
|
||||||
onSearch?: (text: string) => void;
|
|
||||||
getSettingsMenuItems?: () => ContextMenuItem[];
|
getSettingsMenuItems?: () => ContextMenuItem[];
|
||||||
giveFocus?: () => boolean;
|
giveFocus?: () => boolean;
|
||||||
keyDownHandler?: (e: WaveKeyboardEvent) => boolean;
|
keyDownHandler?: (e: WaveKeyboardEvent) => boolean;
|
||||||
|
@ -78,6 +78,9 @@ function parseKeyDescription(keyDescription: string): KeyPressDecl {
|
|||||||
rtn.mods.Option = true;
|
rtn.mods.Option = true;
|
||||||
}
|
}
|
||||||
rtn.mods.Meta = true;
|
rtn.mods.Meta = true;
|
||||||
|
} else if (key == "Esc") {
|
||||||
|
rtn.key = "Escape";
|
||||||
|
rtn.keyType = KeyTypeKey;
|
||||||
} else {
|
} else {
|
||||||
let { key: parsedKey, type: keyType } = parseKey(key);
|
let { key: parsedKey, type: keyType } = parseKey(key);
|
||||||
rtn.key = parsedKey;
|
rtn.key = parsedKey;
|
||||||
|
Loading…
Reference in New Issue
Block a user