webview controls (#110)

This commit is contained in:
Red J Adaya
2024-07-15 09:40:28 -07:00
committed by GitHub
parent e012cc16e6
commit e140076801
8 changed files with 522 additions and 324 deletions
+71
View File
@@ -117,6 +117,77 @@
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 {
display: flex;
align-items: center;
+77 -19
View File
@@ -14,7 +14,7 @@ import { PlotView } from "@/view/plotview";
import { PreviewView, makePreviewModel } from "@/view/preview";
import { TerminalView } from "@/view/term/term";
import { WaveAi } from "@/view/waveai";
import { WebView } from "@/view/webview";
import { WebView, makeWebViewModel } from "@/view/webview";
import clsx from "clsx";
import * as jotai from "jotai";
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 = ({
blockId,
layoutModel,
@@ -289,7 +306,54 @@ const BlockFrame_Default_Component = ({
endIconsElem.push(
<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 (!util.isBlank(headerTextUnion)) {
headerTextElems.push(
@@ -299,19 +363,9 @@ const BlockFrame_Default_Component = ({
);
}
} else if (Array.isArray(headerTextUnion)) {
for (let idx = 0; idx < headerTextUnion.length; idx++) {
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>
);
}
}
headerTextElems.push(...renderHeaderElements(headerTextUnion));
}
return (
<div
className={clsx(
@@ -341,8 +395,7 @@ const BlockFrame_Default_Component = ({
<div className="block-frame-blockid">[{blockId.substring(0, 8)}]</div>
)}
</div>
{headerTextElems}
<div className="flex-spacer"></div>
<div className="block-frame-textelems-wrapper">{headerTextElems}</div>
<div className="block-frame-end-icons">{endIconsElem}</div>
</div>
{preview ? <div className="block-frame-preview" /> : children}
@@ -445,7 +498,9 @@ function getViewElemAndModel(
} else if (blockView === "codeedit") {
viewElem = <CodeEdit key={blockId} text={null} filename={null} />;
} 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") {
viewElem = <WaveAi key={blockId} />;
}
@@ -559,9 +614,12 @@ const BlockFull = React.memo(({ blockId, layoutModel }: BlockProps) => {
if (focusableChildren.length == 0) {
focusElemRef.current.focus({ preventScroll: true });
} 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(
() => getViewElemAndModel(blockId, blockData?.view, blockRef),
+2 -2
View File
@@ -25,8 +25,8 @@ export const useLongClick = (ref, onClick, onLongClick, ms = 300) => {
const handleClick = useCallback(
(e: React.MouseEvent<any>) => {
if (longClickTriggered) {
event.preventDefault();
event.stopPropagation();
e.preventDefault();
e.stopPropagation();
return;
}
onClick?.(e);
+4 -57
View File
@@ -1,62 +1,9 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
.webview-wrapper {
.webview {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
.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;
}
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
File diff suppressed because it is too large Load Diff
+21 -1
View File
@@ -116,11 +116,12 @@ declare global {
type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void };
type HeaderElem = HeaderIconButton | HeaderText;
type HeaderElem = HeaderIconButton | HeaderText | HeaderInput | HeaderDiv;
type HeaderIconButton = {
elemtype: "iconbutton";
icon: string;
className?: string;
title?: string;
click?: (e: React.MouseEvent<any>) => void;
longClick?: (e: React.MouseEvent<any>) => void;
@@ -131,6 +132,25 @@ declare global {
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 {
viewIcon?: jotai.Atom<string | HeaderIconButton>;
viewName?: jotai.Atom<string>;
+1
View File
@@ -58,6 +58,7 @@
"@table-nav/core": "^0.0.7",
"@table-nav/react": "^0.0.7",
"@tanstack/react-table": "^8.17.3",
"@types/electron": "^1.6.10",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-serialize": "^0.13.0",
"@xterm/xterm": "^5.5.0",
+23
View File
@@ -4151,6 +4151,15 @@ __metadata:
languageName: node
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":
version: 1.39.12
resolution: "@types/emscripten@npm:1.39.12"
@@ -6506,6 +6515,19 @@ __metadata:
languageName: node
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":
version: 31.1.0
resolution: "electron@npm:31.1.0"
@@ -12448,6 +12470,7 @@ __metadata:
"@table-nav/core": "npm:^0.0.7"
"@table-nav/react": "npm:^0.0.7"
"@tanstack/react-table": "npm:^8.17.3"
"@types/electron": "npm:^1.6.10"
"@types/node": "npm:^20.12.12"
"@types/papaparse": "npm:^5"
"@types/react": "npm:^18.3.2"