mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01:00
webview controls (#110)
This commit is contained in:
parent
e012cc16e6
commit
e140076801
@ -117,6 +117,77 @@
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.block-frame-textelems-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
gap: 8px;
|
||||||
|
height: 20px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.block-frame-header-iconbutton {
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.5;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-frame-div {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-radius: 3px;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 7px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
|
&.hovered {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
cursor: text;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.focused {
|
||||||
|
outline: 2px solid rgba(88, 193, 66, 0.5);
|
||||||
|
background: #181818;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrapper {
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
input {
|
||||||
|
background-color: transparent;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--app-text-color);
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-frame-header-iconbutton {
|
||||||
|
height: 100%;
|
||||||
|
width: 27px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.block-frame-end-icons {
|
.block-frame-end-icons {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -14,7 +14,7 @@ import { PlotView } from "@/view/plotview";
|
|||||||
import { PreviewView, makePreviewModel } from "@/view/preview";
|
import { PreviewView, makePreviewModel } from "@/view/preview";
|
||||||
import { TerminalView } from "@/view/term/term";
|
import { TerminalView } from "@/view/term/term";
|
||||||
import { WaveAi } from "@/view/waveai";
|
import { WaveAi } from "@/view/waveai";
|
||||||
import { WebView } from "@/view/webview";
|
import { WebView, makeWebViewModel } from "@/view/webview";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import * as jotai from "jotai";
|
import * as jotai from "jotai";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
@ -221,6 +221,23 @@ const IconButton = React.memo(({ decl, className }: { decl: HeaderIconButton; cl
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const Input = React.memo(({ decl, className }: { decl: HeaderInput; className: string }) => {
|
||||||
|
const { value, ref, onChange, onKeyDown, onFocus, onBlur } = decl;
|
||||||
|
return (
|
||||||
|
<div className="input-wrapper">
|
||||||
|
<input
|
||||||
|
ref={ref}
|
||||||
|
className={className}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e)}
|
||||||
|
onKeyDown={(e) => onKeyDown(e)}
|
||||||
|
onFocus={(e) => onFocus(e)}
|
||||||
|
onBlur={(e) => onBlur(e)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const BlockFrame_Default_Component = ({
|
const BlockFrame_Default_Component = ({
|
||||||
blockId,
|
blockId,
|
||||||
layoutModel,
|
layoutModel,
|
||||||
@ -289,7 +306,54 @@ const BlockFrame_Default_Component = ({
|
|||||||
endIconsElem.push(
|
endIconsElem.push(
|
||||||
<IconButton key="close" decl={closeDecl} className="block-frame-endicon-button block-frame-default-close" />
|
<IconButton key="close" decl={closeDecl} className="block-frame-endicon-button block-frame-default-close" />
|
||||||
);
|
);
|
||||||
let headerTextElems: JSX.Element[] = [];
|
|
||||||
|
function renderHeaderElements(headerTextUnion: HeaderElem[]): JSX.Element[] {
|
||||||
|
const headerTextElems: JSX.Element[] = [];
|
||||||
|
|
||||||
|
function renderElement(elem: HeaderElem, key: number): JSX.Element {
|
||||||
|
if (elem.elemtype == "iconbutton") {
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
key={key}
|
||||||
|
decl={elem}
|
||||||
|
className={clsx("block-frame-header-iconbutton", elem.className)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (elem.elemtype == "input") {
|
||||||
|
return <Input key={key} decl={elem} className={clsx("block-frame-input", elem.className)} />;
|
||||||
|
} else if (elem.elemtype == "text") {
|
||||||
|
return (
|
||||||
|
<div key={key} className="block-frame-text">
|
||||||
|
{elem.text}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (elem.elemtype == "div") {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
className={clsx("block-frame-div", elem.className)}
|
||||||
|
onMouseOver={elem.onMouseOver}
|
||||||
|
onMouseOut={elem.onMouseOut}
|
||||||
|
>
|
||||||
|
{elem.children.map((child, childIdx) => renderElement(child, childIdx))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let idx = 0; idx < headerTextUnion.length; idx++) {
|
||||||
|
const elem = headerTextUnion[idx];
|
||||||
|
const renderedElement = renderElement(elem, idx);
|
||||||
|
if (renderedElement) {
|
||||||
|
headerTextElems.push(renderedElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return headerTextElems;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerTextElems: JSX.Element[] = [];
|
||||||
if (typeof headerTextUnion === "string") {
|
if (typeof headerTextUnion === "string") {
|
||||||
if (!util.isBlank(headerTextUnion)) {
|
if (!util.isBlank(headerTextUnion)) {
|
||||||
headerTextElems.push(
|
headerTextElems.push(
|
||||||
@ -299,19 +363,9 @@ const BlockFrame_Default_Component = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (Array.isArray(headerTextUnion)) {
|
} else if (Array.isArray(headerTextUnion)) {
|
||||||
for (let idx = 0; idx < headerTextUnion.length; idx++) {
|
headerTextElems.push(...renderHeaderElements(headerTextUnion));
|
||||||
const elem = headerTextUnion[idx];
|
|
||||||
if (elem.elemtype == "iconbutton") {
|
|
||||||
headerTextElems.push(<IconButton key={idx} decl={elem} className="block-frame-header-iconbutton" />);
|
|
||||||
} else if (elem.elemtype == "text") {
|
|
||||||
headerTextElems.push(
|
|
||||||
<div key={idx} className="block-frame-text">
|
|
||||||
{elem.text}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@ -341,8 +395,7 @@ const BlockFrame_Default_Component = ({
|
|||||||
<div className="block-frame-blockid">[{blockId.substring(0, 8)}]</div>
|
<div className="block-frame-blockid">[{blockId.substring(0, 8)}]</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{headerTextElems}
|
<div className="block-frame-textelems-wrapper">{headerTextElems}</div>
|
||||||
<div className="flex-spacer"></div>
|
|
||||||
<div className="block-frame-end-icons">{endIconsElem}</div>
|
<div className="block-frame-end-icons">{endIconsElem}</div>
|
||||||
</div>
|
</div>
|
||||||
{preview ? <div className="block-frame-preview" /> : children}
|
{preview ? <div className="block-frame-preview" /> : children}
|
||||||
@ -445,7 +498,9 @@ function getViewElemAndModel(
|
|||||||
} else if (blockView === "codeedit") {
|
} else if (blockView === "codeedit") {
|
||||||
viewElem = <CodeEdit key={blockId} text={null} filename={null} />;
|
viewElem = <CodeEdit key={blockId} text={null} filename={null} />;
|
||||||
} else if (blockView === "web") {
|
} else if (blockView === "web") {
|
||||||
viewElem = <WebView key={blockId} blockId={blockId} parentRef={blockRef} />;
|
const webviewModel = makeWebViewModel(blockId);
|
||||||
|
viewElem = <WebView key={blockId} parentRef={blockRef} model={webviewModel} />;
|
||||||
|
viewModel = webviewModel;
|
||||||
} else if (blockView === "waveai") {
|
} else if (blockView === "waveai") {
|
||||||
viewElem = <WaveAi key={blockId} />;
|
viewElem = <WaveAi key={blockId} />;
|
||||||
}
|
}
|
||||||
@ -559,9 +614,12 @@ const BlockFull = React.memo(({ blockId, layoutModel }: BlockProps) => {
|
|||||||
if (focusableChildren.length == 0) {
|
if (focusableChildren.length == 0) {
|
||||||
focusElemRef.current.focus({ preventScroll: true });
|
focusElemRef.current.focus({ preventScroll: true });
|
||||||
} else {
|
} else {
|
||||||
(focusableChildren[0] as HTMLElement).focus({ preventScroll: true });
|
const firstFocusableChild = focusableChildren[0] as HTMLElement;
|
||||||
|
if (!firstFocusableChild.classList.contains("url-input")) {
|
||||||
|
firstFocusableChild.focus({ preventScroll: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [focusElemRef.current, getFocusableChildren]);
|
}, [getFocusableChildren]);
|
||||||
|
|
||||||
let { viewElem, viewModel } = React.useMemo(
|
let { viewElem, viewModel } = React.useMemo(
|
||||||
() => getViewElemAndModel(blockId, blockData?.view, blockRef),
|
() => getViewElemAndModel(blockId, blockData?.view, blockRef),
|
||||||
|
@ -25,8 +25,8 @@ export const useLongClick = (ref, onClick, onLongClick, ms = 300) => {
|
|||||||
const handleClick = useCallback(
|
const handleClick = useCallback(
|
||||||
(e: React.MouseEvent<any>) => {
|
(e: React.MouseEvent<any>) => {
|
||||||
if (longClickTriggered) {
|
if (longClickTriggered) {
|
||||||
event.preventDefault();
|
e.preventDefault();
|
||||||
event.stopPropagation();
|
e.stopPropagation();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onClick?.(e);
|
onClick?.(e);
|
||||||
|
@ -1,62 +1,9 @@
|
|||||||
// Copyright 2024, Command Line Inc.
|
// Copyright 2024, Command Line Inc.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
.webview-wrapper {
|
.webview {
|
||||||
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
border-bottom-left-radius: 4px;
|
||||||
flex-direction: column;
|
border-bottom-right-radius: 4px;
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
.navigation {
|
|
||||||
display: flex;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-right: none;
|
|
||||||
border-top-left-radius: 4px;
|
|
||||||
|
|
||||||
.button {
|
|
||||||
padding: 6px 12px;
|
|
||||||
|
|
||||||
i {
|
|
||||||
font-size: 16px;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
&.fa-rotate-right {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.url-input-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
padding: 3px;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-top-right-radius: 4px;
|
|
||||||
|
|
||||||
.url-input {
|
|
||||||
flex: 1;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border: none;
|
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
|
||||||
padding: 0 5px;
|
|
||||||
color: var(--app-color);
|
|
||||||
border-radius: 2px;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.webview {
|
|
||||||
flex-grow: 1;
|
|
||||||
width: 100%;
|
|
||||||
border-bottom-left-radius: 4px;
|
|
||||||
border-bottom-right-radius: 4px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,96 +1,336 @@
|
|||||||
// Copyright 2024, Command Line Inc.
|
// Copyright 2024, Command Line Inc.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import { Button } from "@/app/element/button";
|
|
||||||
import { getApi } from "@/app/store/global";
|
import { getApi } from "@/app/store/global";
|
||||||
import { WOS, useBlockAtom } from "@/store/global";
|
import { WOS, globalStore } from "@/store/global";
|
||||||
import * as services from "@/store/services";
|
import * as services from "@/store/services";
|
||||||
import { WebviewTag } from "electron";
|
import { WebviewTag } from "electron";
|
||||||
import * as jotai from "jotai";
|
import * as jotai from "jotai";
|
||||||
import React, { memo, useEffect, useMemo, useRef, useState } from "react";
|
|
||||||
|
import React, { memo, useEffect } from "react";
|
||||||
|
|
||||||
import "./webview.less";
|
import "./webview.less";
|
||||||
|
|
||||||
interface WebViewProps {
|
export class WebViewModel implements ViewModel {
|
||||||
blockId: string;
|
blockId: string;
|
||||||
parentRef: React.MutableRefObject<HTMLDivElement>;
|
blockAtom: jotai.Atom<Block>;
|
||||||
}
|
viewIcon: jotai.Atom<string | HeaderIconButton>;
|
||||||
|
viewName: jotai.Atom<string>;
|
||||||
|
viewText: jotai.Atom<HeaderElem[]>;
|
||||||
|
preIconButton: jotai.Atom<HeaderIconButton>;
|
||||||
|
endIconButtons: jotai.Atom<HeaderIconButton[]>;
|
||||||
|
url: jotai.PrimitiveAtom<string>;
|
||||||
|
urlInput: jotai.PrimitiveAtom<string>;
|
||||||
|
urlInputFocused: jotai.PrimitiveAtom<boolean>;
|
||||||
|
isLoading: jotai.PrimitiveAtom<boolean>;
|
||||||
|
urlWrapperClassName: jotai.PrimitiveAtom<string>;
|
||||||
|
refreshIcon: jotai.PrimitiveAtom<string>;
|
||||||
|
webviewRef: React.RefObject<WebviewTag>;
|
||||||
|
urlInputRef: React.RefObject<HTMLInputElement>;
|
||||||
|
historyStack: string[];
|
||||||
|
historyIndex: number;
|
||||||
|
recentUrls: { [key: string]: number };
|
||||||
|
|
||||||
function setBlockUrl(blockId: string, url: string) {
|
constructor(blockId: string) {
|
||||||
services.ObjectService.UpdateObjectMeta(WOS.makeORef("block", blockId), { url: url });
|
this.blockId = blockId;
|
||||||
}
|
this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
|
||||||
|
|
||||||
const WebView = memo(({ blockId, parentRef }: WebViewProps) => {
|
this.url = jotai.atom("");
|
||||||
const blockAtom = WOS.getWaveObjectAtom<Block>(WOS.makeORef("block", blockId));
|
this.urlInput = jotai.atom("");
|
||||||
const blockData = WOS.useWaveObjectValueWithSuspense<Block>(WOS.makeORef("block", blockId));
|
this.urlWrapperClassName = jotai.atom("");
|
||||||
const urlAtom = useBlockAtom<string>(blockId, "webview:url", () => {
|
this.urlInputFocused = jotai.atom(false);
|
||||||
return jotai.atom((get) => {
|
this.isLoading = jotai.atom(false);
|
||||||
const blockData = get(blockAtom);
|
this.refreshIcon = jotai.atom("rotate-right");
|
||||||
return blockData?.meta?.url;
|
this.historyStack = [];
|
||||||
|
this.historyIndex = 0;
|
||||||
|
this.recentUrls = {};
|
||||||
|
|
||||||
|
this.viewIcon = jotai.atom((get) => {
|
||||||
|
return "globe"; // should not be hardcoded
|
||||||
});
|
});
|
||||||
});
|
|
||||||
const realUrl = jotai.useAtomValue(urlAtom);
|
|
||||||
const [lastRealUrl, setLastRealUrl] = useState(realUrl);
|
|
||||||
const initialUrl = useMemo(() => blockData?.meta?.url, []);
|
|
||||||
const [inputUrl, setInputUrl] = useState(realUrl); // Separate state for the input field
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const webviewRef = useRef<WebviewTag>(null);
|
this.viewName = jotai.atom("Web");
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
this.urlInputRef = React.createRef<HTMLInputElement>();
|
||||||
const historyStack = useRef<string[]>([]);
|
this.webviewRef = React.createRef<WebviewTag>();
|
||||||
const historyIndex = useRef<number>(-1);
|
|
||||||
const recentUrls = useRef<{ [key: string]: number }>({});
|
|
||||||
|
|
||||||
useEffect(() => {
|
this.viewText = jotai.atom((get) => {
|
||||||
if (realUrl !== lastRealUrl) {
|
let url = get(this.blockAtom)?.meta?.url || "";
|
||||||
setLastRealUrl(realUrl);
|
if (url && this.historyStack.length === 0) {
|
||||||
setInputUrl(realUrl);
|
this.addToHistoryStack(url);
|
||||||
}
|
|
||||||
}, [realUrl, lastRealUrl]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
historyStack.current.push(initialUrl);
|
|
||||||
historyIndex.current = 0;
|
|
||||||
|
|
||||||
const webview = webviewRef.current;
|
|
||||||
|
|
||||||
const handleNavigation = (newUrl: string) => {
|
|
||||||
const normalizedNewUrl = normalizeUrl(newUrl);
|
|
||||||
const normalizedLastUrl = normalizeUrl(historyStack.current[historyIndex.current]);
|
|
||||||
|
|
||||||
if (normalizedLastUrl !== normalizedNewUrl) {
|
|
||||||
setBlockUrl(blockId, normalizedNewUrl);
|
|
||||||
setInputUrl(normalizedNewUrl); // Update input field as well
|
|
||||||
historyIndex.current += 1;
|
|
||||||
historyStack.current = historyStack.current.slice(0, historyIndex.current);
|
|
||||||
historyStack.current.push(normalizedNewUrl);
|
|
||||||
updateRecentUrls(normalizedNewUrl);
|
|
||||||
}
|
}
|
||||||
};
|
const currUrl = get(this.url);
|
||||||
|
if (currUrl) {
|
||||||
|
url = currUrl;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
elemtype: "iconbutton",
|
||||||
|
className: this.shouldDisabledBackButton() ? "disabled" : "",
|
||||||
|
icon: "chevron-left",
|
||||||
|
click: this.handleBack.bind(this),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
elemtype: "iconbutton",
|
||||||
|
className: this.shouldDisabledForwardButton() ? "disabled" : "",
|
||||||
|
icon: "chevron-right",
|
||||||
|
click: this.handleForward.bind(this),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
elemtype: "div",
|
||||||
|
className: get(this.urlWrapperClassName),
|
||||||
|
onMouseOver: this.handleUrlWrapperMouseOver.bind(this),
|
||||||
|
onMouseOut: this.handleUrlWrapperMouseOut.bind(this),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
elemtype: "input",
|
||||||
|
value: url,
|
||||||
|
ref: this.urlInputRef,
|
||||||
|
className: "url-input",
|
||||||
|
onChange: this.handleUrlChange.bind(this),
|
||||||
|
onKeyDown: this.handleKeyDown.bind(this),
|
||||||
|
onFocus: this.handleFocus.bind(this),
|
||||||
|
onBlur: this.handleBlur.bind(this),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
elemtype: "iconbutton",
|
||||||
|
icon: get(this.refreshIcon),
|
||||||
|
click: this.handleRefresh.bind(this),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
] as HeaderElem[];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldDisabledBackButton() {
|
||||||
|
return this.historyIndex === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldDisabledForwardButton() {
|
||||||
|
return this.historyIndex === this.historyStack.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUrlWrapperMouseOver(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||||
|
const urlInputFocused = globalStore.get(this.urlInputFocused);
|
||||||
|
if (e.type === "mouseover" && !urlInputFocused) {
|
||||||
|
globalStore.set(this.urlWrapperClassName, "hovered");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUrlWrapperMouseOut(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||||
|
const urlInputFocused = globalStore.get(this.urlInputFocused);
|
||||||
|
if (e.type === "mouseout" && !urlInputFocused) {
|
||||||
|
globalStore.set(this.urlWrapperClassName, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBack(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (this.historyIndex > 0) {
|
||||||
|
do {
|
||||||
|
this.historyIndex -= 1;
|
||||||
|
} while (this.historyIndex > 0 && this.isRecentUrl(this.historyStack[this.historyIndex]));
|
||||||
|
|
||||||
|
const prevUrl = this.historyStack[this.historyIndex];
|
||||||
|
this.setBlockUrl(this.blockId, prevUrl);
|
||||||
|
globalStore.set(this.url, prevUrl);
|
||||||
|
if (this.webviewRef.current) {
|
||||||
|
this.webviewRef.current.src = prevUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleForward(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (this.historyIndex < this.historyStack.length - 1) {
|
||||||
|
do {
|
||||||
|
this.historyIndex += 1;
|
||||||
|
} while (
|
||||||
|
this.historyIndex < this.historyStack.length - 1 &&
|
||||||
|
this.isRecentUrl(this.historyStack[this.historyIndex])
|
||||||
|
);
|
||||||
|
|
||||||
|
const nextUrl = this.historyStack[this.historyIndex];
|
||||||
|
this.setBlockUrl(this.blockId, nextUrl);
|
||||||
|
globalStore.set(this.url, nextUrl);
|
||||||
|
if (this.webviewRef.current) {
|
||||||
|
this.webviewRef.current.src = nextUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRefresh(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (this.webviewRef.current) {
|
||||||
|
if (globalStore.get(this.isLoading)) {
|
||||||
|
this.webviewRef.current.stop();
|
||||||
|
} else {
|
||||||
|
this.webviewRef.current.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUrlChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
globalStore.set(this.url, event.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
let url = globalStore.get(this.url);
|
||||||
|
if (!url) {
|
||||||
|
url = this.historyStack[this.historyIndex];
|
||||||
|
}
|
||||||
|
this.navigateTo(url);
|
||||||
|
this.urlInputRef.current?.blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFocus(event: React.FocusEvent<HTMLInputElement>) {
|
||||||
|
globalStore.set(this.urlWrapperClassName, "focused");
|
||||||
|
globalStore.set(this.urlInputFocused, true);
|
||||||
|
this.urlInputRef.current.focus();
|
||||||
|
event.target.select();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBlur(event: React.FocusEvent<HTMLInputElement>) {
|
||||||
|
globalStore.set(this.urlWrapperClassName, "");
|
||||||
|
globalStore.set(this.urlInputFocused, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureUrlScheme(url: string) {
|
||||||
|
if (/^(localhost|(\d{1,3}\.){3}\d{1,3})(:\d+)?/.test(url)) {
|
||||||
|
// If the URL starts with localhost or an IP address (with optional port)
|
||||||
|
return `http://${url}`;
|
||||||
|
} else if (!/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url)) {
|
||||||
|
// If the URL doesn't start with a protocol
|
||||||
|
return `https://${url}`;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeUrl(url: string) {
|
||||||
|
if (!url) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
if (parsedUrl.hostname.startsWith("www.")) {
|
||||||
|
parsedUrl.hostname = parsedUrl.hostname.slice(4);
|
||||||
|
}
|
||||||
|
return parsedUrl.href;
|
||||||
|
} catch (e) {
|
||||||
|
return url.replace(/\/+$/, "") + "/";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateTo(newUrl: string) {
|
||||||
|
const finalUrl = this.ensureUrlScheme(newUrl);
|
||||||
|
const normalizedFinalUrl = this.normalizeUrl(finalUrl);
|
||||||
|
const normalizedLastUrl = this.normalizeUrl(this.historyStack[this.historyIndex]);
|
||||||
|
|
||||||
|
if (normalizedLastUrl !== normalizedFinalUrl) {
|
||||||
|
this.setBlockUrl(this.blockId, normalizedFinalUrl);
|
||||||
|
globalStore.set(this.url, normalizedFinalUrl);
|
||||||
|
this.historyIndex += 1;
|
||||||
|
this.historyStack = this.historyStack.slice(0, this.historyIndex);
|
||||||
|
this.addToHistoryStack(normalizedFinalUrl);
|
||||||
|
if (this.webviewRef.current) {
|
||||||
|
this.webviewRef.current.src = normalizedFinalUrl;
|
||||||
|
}
|
||||||
|
this.updateRecentUrls(normalizedFinalUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addToHistoryStack(url: string) {
|
||||||
|
if (this.historyStack.length === 0 || this.historyStack[this.historyStack.length - 1] !== url) {
|
||||||
|
this.historyStack.push(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setBlockUrl(blockId: string, url: string) {
|
||||||
|
services.ObjectService.UpdateObjectMeta(WOS.makeORef("block", blockId), { url: url });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRecentUrls(url: string) {
|
||||||
|
if (this.recentUrls[url]) {
|
||||||
|
this.recentUrls[url]++;
|
||||||
|
} else {
|
||||||
|
this.recentUrls[url] = 1;
|
||||||
|
}
|
||||||
|
// Clean up old entries after a certain threshold
|
||||||
|
if (Object.keys(this.recentUrls).length > 50) {
|
||||||
|
this.recentUrls = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isRecentUrl(url: string) {
|
||||||
|
return this.recentUrls[url] > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRefreshIcon(refreshIcon: string) {
|
||||||
|
globalStore.set(this.refreshIcon, refreshIcon);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(isLoading: boolean) {
|
||||||
|
globalStore.set(this.isLoading, isLoading);
|
||||||
|
}
|
||||||
|
|
||||||
|
getUrl() {
|
||||||
|
return this.historyStack[this.historyIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeWebViewModel(blockId: string): WebViewModel {
|
||||||
|
const webviewModel = new WebViewModel(blockId);
|
||||||
|
return webviewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WebViewProps {
|
||||||
|
parentRef: React.RefObject<HTMLDivElement>;
|
||||||
|
model: WebViewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WebView = memo(({ parentRef, model }: WebViewProps) => {
|
||||||
|
const url = model.getUrl();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const webview = model.webviewRef.current;
|
||||||
|
|
||||||
if (webview) {
|
if (webview) {
|
||||||
const navigateListener = (event: any) => {
|
const navigateListener = (e: any) => {
|
||||||
handleNavigation(event.url);
|
model.navigateTo(e.url);
|
||||||
};
|
};
|
||||||
|
|
||||||
webview.addEventListener("did-navigate", navigateListener);
|
webview.addEventListener("did-navigate", (e) => {
|
||||||
webview.addEventListener("did-navigate-in-page", navigateListener);
|
console.log("did-navigate");
|
||||||
webview.addEventListener("did-start-loading", () => setIsLoading(true));
|
navigateListener(e);
|
||||||
webview.addEventListener("did-stop-loading", () => setIsLoading(false));
|
});
|
||||||
|
webview.addEventListener("did-start-loading", () => {
|
||||||
|
model.setRefreshIcon("xmark-large");
|
||||||
|
model.setIsLoading(true);
|
||||||
|
});
|
||||||
|
webview.addEventListener("did-stop-loading", () => {
|
||||||
|
model.setRefreshIcon("rotate-right");
|
||||||
|
model.setIsLoading(false);
|
||||||
|
});
|
||||||
|
|
||||||
// Handle new-window event
|
// Handle new-window event
|
||||||
webview.addEventListener("new-window", (event: any) => {
|
webview.addEventListener("new-window", (e: any) => {
|
||||||
event.preventDefault();
|
e.preventDefault();
|
||||||
const newUrl = event.detail.url;
|
const newUrl = e.detail.url;
|
||||||
getApi().openExternal(newUrl);
|
getApi().openExternal(newUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Suppress errors
|
// Suppress errors
|
||||||
webview.addEventListener("did-fail-load", (event: any) => {
|
webview.addEventListener("did-fail-load", (e: any) => {
|
||||||
if (event.errorCode === -3) {
|
if (e.errorCode === -3) {
|
||||||
console.log("Suppressed ERR_ABORTED error");
|
e.log("Suppressed ERR_ABORTED error");
|
||||||
} else {
|
} else {
|
||||||
console.error(`Failed to load ${event.validatedURL}: ${event.errorDescription}`);
|
console.error(`Failed to load ${e.validatedURL}: ${e.errorDescription}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -98,32 +338,32 @@ const WebView = memo(({ blockId, parentRef }: WebViewProps) => {
|
|||||||
return () => {
|
return () => {
|
||||||
webview.removeEventListener("did-navigate", navigateListener);
|
webview.removeEventListener("did-navigate", navigateListener);
|
||||||
webview.removeEventListener("did-navigate-in-page", navigateListener);
|
webview.removeEventListener("did-navigate-in-page", navigateListener);
|
||||||
webview.removeEventListener("new-window", (event: any) => {
|
webview.removeEventListener("new-window", (e: any) => {
|
||||||
webview.src = event.url;
|
model.navigateTo(e.url);
|
||||||
});
|
});
|
||||||
webview.removeEventListener("did-fail-load", (event: any) => {
|
webview.removeEventListener("did-fail-load", (e: any) => {
|
||||||
if (event.errorCode === -3) {
|
if (e.errorCode === -3) {
|
||||||
console.log("Suppressed ERR_ABORTED error");
|
console.log("Suppressed ERR_ABORTED error");
|
||||||
} else {
|
} else {
|
||||||
console.error(`Failed to load ${event.validatedURL}: ${event.errorDescription}`);
|
console.error(`Failed to load ${e.validatedURL}: ${e.errorDescription}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [initialUrl]);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if ((event.ctrlKey || event.metaKey) && event.key === "l") {
|
if ((e.ctrlKey || e.metaKey) && e.key === "l") {
|
||||||
event.preventDefault();
|
e.preventDefault();
|
||||||
if (inputRef.current) {
|
if (model.urlInputRef) {
|
||||||
inputRef.current.focus();
|
model.urlInputRef.current.focus();
|
||||||
inputRef.current.select();
|
model.urlInputRef.current.select();
|
||||||
}
|
}
|
||||||
} else if ((event.ctrlKey || event.metaKey) && event.key === "r") {
|
} else if ((e.ctrlKey || e.metaKey) && e.key === "r") {
|
||||||
event.preventDefault();
|
e.preventDefault();
|
||||||
if (webviewRef.current) {
|
if (model.webviewRef.current) {
|
||||||
webviewRef.current.reload();
|
model.webviewRef.current.reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -140,169 +380,7 @@ const WebView = memo(({ blockId, parentRef }: WebViewProps) => {
|
|||||||
};
|
};
|
||||||
}, [parentRef]);
|
}, [parentRef]);
|
||||||
|
|
||||||
const ensureUrlScheme = (url: string) => {
|
return <webview id="webview" className="webview" ref={model.webviewRef} src={url}></webview>;
|
||||||
if (/^(localhost|(\d{1,3}\.){3}\d{1,3})(:\d+)?/.test(url)) {
|
|
||||||
// If the URL starts with localhost or an IP address (with optional port)
|
|
||||||
return `http://${url}`;
|
|
||||||
} else if (!/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url)) {
|
|
||||||
// If the URL doesn't start with a protocol
|
|
||||||
return `https://${url}`;
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeUrl = (url: string) => {
|
|
||||||
try {
|
|
||||||
const parsedUrl = new URL(url);
|
|
||||||
if (parsedUrl.hostname.startsWith("www.")) {
|
|
||||||
parsedUrl.hostname = parsedUrl.hostname.slice(4);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure pathname ends with a trailing slash
|
|
||||||
if (!parsedUrl.pathname.endsWith("/")) {
|
|
||||||
parsedUrl.pathname += "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure hash fragments end with a trailing slash
|
|
||||||
if (parsedUrl.hash && !parsedUrl.hash.endsWith("/")) {
|
|
||||||
parsedUrl.hash += "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure search parameters end with a trailing slash
|
|
||||||
if (parsedUrl.search && !parsedUrl.search.endsWith("/")) {
|
|
||||||
parsedUrl.search += "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsedUrl.href;
|
|
||||||
} catch (e) {
|
|
||||||
return url.replace(/\/+$/, "") + "/";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const navigateTo = (newUrl: string) => {
|
|
||||||
const finalUrl = ensureUrlScheme(newUrl);
|
|
||||||
const normalizedFinalUrl = normalizeUrl(finalUrl);
|
|
||||||
const normalizedLastUrl = normalizeUrl(historyStack.current[historyIndex.current]);
|
|
||||||
|
|
||||||
if (normalizedLastUrl !== normalizedFinalUrl) {
|
|
||||||
setBlockUrl(blockId, normalizedFinalUrl);
|
|
||||||
setInputUrl(normalizedFinalUrl);
|
|
||||||
historyIndex.current += 1;
|
|
||||||
historyStack.current = historyStack.current.slice(0, historyIndex.current);
|
|
||||||
historyStack.current.push(normalizedFinalUrl);
|
|
||||||
if (webviewRef.current) {
|
|
||||||
webviewRef.current.src = normalizedFinalUrl;
|
|
||||||
}
|
|
||||||
updateRecentUrls(normalizedFinalUrl);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBack = () => {
|
|
||||||
if (historyIndex.current > 0) {
|
|
||||||
do {
|
|
||||||
historyIndex.current -= 1;
|
|
||||||
} while (historyIndex.current > 0 && isRecentUrl(historyStack.current[historyIndex.current]));
|
|
||||||
|
|
||||||
const prevUrl = historyStack.current[historyIndex.current];
|
|
||||||
setBlockUrl(blockId, prevUrl);
|
|
||||||
setInputUrl(prevUrl);
|
|
||||||
if (webviewRef.current) {
|
|
||||||
webviewRef.current.src = prevUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleForward = () => {
|
|
||||||
if (historyIndex.current < historyStack.current.length - 1) {
|
|
||||||
do {
|
|
||||||
historyIndex.current += 1;
|
|
||||||
} while (
|
|
||||||
historyIndex.current < historyStack.current.length - 1 &&
|
|
||||||
isRecentUrl(historyStack.current[historyIndex.current])
|
|
||||||
);
|
|
||||||
|
|
||||||
const nextUrl = historyStack.current[historyIndex.current];
|
|
||||||
setBlockUrl(blockId, nextUrl);
|
|
||||||
setInputUrl(nextUrl);
|
|
||||||
if (webviewRef.current) {
|
|
||||||
webviewRef.current.src = nextUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRefresh = () => {
|
|
||||||
if (webviewRef.current) {
|
|
||||||
if (isLoading) {
|
|
||||||
webviewRef.current.stop();
|
|
||||||
} else {
|
|
||||||
webviewRef.current.reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUrlChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setInputUrl(event.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (event.key === "Enter") {
|
|
||||||
navigateTo(inputUrl);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {
|
|
||||||
event.target.select();
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateRecentUrls = (url: string) => {
|
|
||||||
if (recentUrls.current[url]) {
|
|
||||||
recentUrls.current[url]++;
|
|
||||||
} else {
|
|
||||||
recentUrls.current[url] = 1;
|
|
||||||
}
|
|
||||||
// Clean up old entries after a certain threshold
|
|
||||||
if (Object.keys(recentUrls.current).length > 50) {
|
|
||||||
recentUrls.current = {};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isRecentUrl = (url: string) => {
|
|
||||||
return recentUrls.current[url] > 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="webview-wrapper">
|
|
||||||
<div className="toolbar">
|
|
||||||
<div className="navigation">
|
|
||||||
<Button className="secondary ghost back" onClick={handleBack} disabled={historyIndex.current <= 0}>
|
|
||||||
<i className="fa-sharp fa-regular fa-arrow-left"></i>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleForward}
|
|
||||||
className="secondary ghost forward"
|
|
||||||
disabled={historyIndex.current >= historyStack.current.length - 1}
|
|
||||||
>
|
|
||||||
<i className="fa-sharp fa-regular fa-arrow-right"></i>
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleRefresh} className="secondary ghost refresh">
|
|
||||||
<i className={`fa-sharp fa-regular ${isLoading ? "fa-xmark" : "fa-rotate-right"}`}></i>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="url-input-wrapper">
|
|
||||||
<input
|
|
||||||
className="url-input"
|
|
||||||
ref={inputRef}
|
|
||||||
type="text"
|
|
||||||
value={inputUrl}
|
|
||||||
onChange={handleUrlChange}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onFocus={handleFocus}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<webview id="webview" className="webview" ref={webviewRef} src={realUrl}></webview>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export { WebView };
|
export { WebView, makeWebViewModel };
|
||||||
|
22
frontend/types/custom.d.ts
vendored
22
frontend/types/custom.d.ts
vendored
@ -116,11 +116,12 @@ declare global {
|
|||||||
|
|
||||||
type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void };
|
type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void };
|
||||||
|
|
||||||
type HeaderElem = HeaderIconButton | HeaderText;
|
type HeaderElem = HeaderIconButton | HeaderText | HeaderInput | HeaderDiv;
|
||||||
|
|
||||||
type HeaderIconButton = {
|
type HeaderIconButton = {
|
||||||
elemtype: "iconbutton";
|
elemtype: "iconbutton";
|
||||||
icon: string;
|
icon: string;
|
||||||
|
className?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
click?: (e: React.MouseEvent<any>) => void;
|
click?: (e: React.MouseEvent<any>) => void;
|
||||||
longClick?: (e: React.MouseEvent<any>) => void;
|
longClick?: (e: React.MouseEvent<any>) => void;
|
||||||
@ -131,6 +132,25 @@ declare global {
|
|||||||
text: string;
|
text: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type HeaderInput = {
|
||||||
|
elemtype: "input";
|
||||||
|
value: string;
|
||||||
|
className?: string;
|
||||||
|
ref?: React.MutableRefObject<HTMLInputElement>;
|
||||||
|
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||||
|
onFocus?: (e: React.FocusEvent<HTMLInputElement>) => void;
|
||||||
|
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type HeaderDiv = {
|
||||||
|
elemtype: "div";
|
||||||
|
className?: string;
|
||||||
|
children: HeaderElem[];
|
||||||
|
onMouseOver?: (e: React.MouseEvent<any>) => void;
|
||||||
|
onMouseOut?: (e: React.MouseEvent<any>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
interface ViewModel {
|
interface ViewModel {
|
||||||
viewIcon?: jotai.Atom<string | HeaderIconButton>;
|
viewIcon?: jotai.Atom<string | HeaderIconButton>;
|
||||||
viewName?: jotai.Atom<string>;
|
viewName?: jotai.Atom<string>;
|
||||||
|
@ -58,6 +58,7 @@
|
|||||||
"@table-nav/core": "^0.0.7",
|
"@table-nav/core": "^0.0.7",
|
||||||
"@table-nav/react": "^0.0.7",
|
"@table-nav/react": "^0.0.7",
|
||||||
"@tanstack/react-table": "^8.17.3",
|
"@tanstack/react-table": "^8.17.3",
|
||||||
|
"@types/electron": "^1.6.10",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/addon-serialize": "^0.13.0",
|
"@xterm/addon-serialize": "^0.13.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
|
23
yarn.lock
23
yarn.lock
@ -4151,6 +4151,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/electron@npm:^1.6.10":
|
||||||
|
version: 1.6.10
|
||||||
|
resolution: "@types/electron@npm:1.6.10"
|
||||||
|
dependencies:
|
||||||
|
electron: "npm:*"
|
||||||
|
checksum: 10c0/d9d7facf29280dbfcecca287c453c5dc51f3d10cbfd63ea7e78670d37acf51aabc6e5e2ef1fe5f48d67e7862fa9f590bb6ef703901eb62599837a14b4278b0e1
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/emscripten@npm:^1.39.6":
|
"@types/emscripten@npm:^1.39.6":
|
||||||
version: 1.39.12
|
version: 1.39.12
|
||||||
resolution: "@types/emscripten@npm:1.39.12"
|
resolution: "@types/emscripten@npm:1.39.12"
|
||||||
@ -6506,6 +6515,19 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"electron@npm:*":
|
||||||
|
version: 31.2.0
|
||||||
|
resolution: "electron@npm:31.2.0"
|
||||||
|
dependencies:
|
||||||
|
"@electron/get": "npm:^2.0.0"
|
||||||
|
"@types/node": "npm:^20.9.0"
|
||||||
|
extract-zip: "npm:^2.0.1"
|
||||||
|
bin:
|
||||||
|
electron: cli.js
|
||||||
|
checksum: 10c0/559f94b4d51d4f3dfdaf4fa9a2443834b98c13402f193f5df2e8c10335cb8a2e8b1e6c6eed8499a04be0db28b52d1ddb190217e6122fb9d20cad27c6b42a9f2b
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"electron@npm:^31.1.0":
|
"electron@npm:^31.1.0":
|
||||||
version: 31.1.0
|
version: 31.1.0
|
||||||
resolution: "electron@npm:31.1.0"
|
resolution: "electron@npm:31.1.0"
|
||||||
@ -12448,6 +12470,7 @@ __metadata:
|
|||||||
"@table-nav/core": "npm:^0.0.7"
|
"@table-nav/core": "npm:^0.0.7"
|
||||||
"@table-nav/react": "npm:^0.0.7"
|
"@table-nav/react": "npm:^0.0.7"
|
||||||
"@tanstack/react-table": "npm:^8.17.3"
|
"@tanstack/react-table": "npm:^8.17.3"
|
||||||
|
"@types/electron": "npm:^1.6.10"
|
||||||
"@types/node": "npm:^20.12.12"
|
"@types/node": "npm:^20.12.12"
|
||||||
"@types/papaparse": "npm:^5"
|
"@types/papaparse": "npm:^5"
|
||||||
"@types/react": "npm:^18.3.2"
|
"@types/react": "npm:^18.3.2"
|
||||||
|
Loading…
Reference in New Issue
Block a user