suggestions UI (#286)

This commit is contained in:
Red J Adaya 2024-08-29 14:47:45 +08:00 committed by GitHub
parent bfab5e4223
commit c440fb774e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 377 additions and 224 deletions

View File

@ -46,7 +46,13 @@ function makeViewModel(blockId: string, blockView: string): ViewModel {
return makeDefaultViewModel(blockId, blockView);
}
function getViewElem(blockId: string, blockView: string, viewModel: ViewModel): JSX.Element {
function getViewElem(
blockId: string,
blockRef: React.RefObject<HTMLDivElement>,
contentRef: React.RefObject<HTMLDivElement>,
blockView: string,
viewModel: ViewModel
): JSX.Element {
if (util.isBlank(blockView)) {
return <CenteredDiv>No View</CenteredDiv>;
}
@ -54,7 +60,15 @@ function getViewElem(blockId: string, blockView: string, viewModel: ViewModel):
return <TerminalView key={blockId} blockId={blockId} model={viewModel as TermViewModel} />;
}
if (blockView === "preview") {
return <PreviewView key={blockId} blockId={blockId} model={viewModel as PreviewModel} />;
return (
<PreviewView
key={blockId}
blockId={blockId}
blockRef={blockRef}
contentRef={contentRef}
model={viewModel as PreviewModel}
/>
);
}
if (blockView === "plot") {
return <PlotView key={blockId} />;
@ -179,7 +193,7 @@ const BlockFull = React.memo(({ nodeModel, viewModel }: FullBlockProps) => {
}, [innerRect, disablePointerEvents, blockContentOffset]);
const viewElem = React.useMemo(
() => getViewElem(nodeModel.blockId, blockData?.meta?.view, viewModel),
() => getViewElem(nodeModel.blockId, blockRef, contentRef, blockData?.meta?.view, viewModel),
[nodeModel.blockId, blockData?.meta?.view, viewModel]
);

View File

@ -128,6 +128,7 @@ const BlockFrame_Header = ({
nodeModel,
viewModel,
preview,
connBtnRef,
changeConnModalAtom,
}: BlockFrameProps & { changeConnModalAtom: jotai.PrimitiveAtom<boolean> }) => {
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", nodeModel.blockId));
@ -176,8 +177,8 @@ const BlockFrame_Header = ({
if (manageConnection) {
const connButtonElem = (
<ConnectionButton
ref={connBtnRef}
key={nodeModel.blockId}
blockId={nodeModel.blockId}
connection={blockData?.meta?.connection}
changeConnModalAtom={changeConnModalAtom}
/>
@ -206,7 +207,11 @@ const HeaderTextElem = React.memo(({ elem, preview }: { elem: HeaderElem; previe
} else if (elem.elemtype == "input") {
return <Input decl={elem} className={clsx("block-frame-input", elem.className)} preview={preview} />;
} else if (elem.elemtype == "text") {
return <div className="block-frame-text">{elem.text}</div>;
return (
<div ref={preview ? null : elem.ref} className="block-frame-text">
{elem.text}
</div>
);
} else if (elem.elemtype == "textbutton") {
return (
<Button className={elem.className} onClick={(e) => elem.onClick(e)}>
@ -278,6 +283,7 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => {
const changeConnModalAtom = useBlockAtom(nodeModel.blockId, "changeConn", () => {
return jotai.atom(false);
}) as jotai.PrimitiveAtom<boolean>;
const connBtnRef = React.useRef<HTMLDivElement>();
const viewIconElem = getViewIconElem(viewIconUnion, blockData);
@ -315,18 +321,19 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => {
onKeyDown={keydownWrapper(handleKeyDown)}
>
<BlockMask nodeModel={nodeModel} />
<div className="block-frame-default-inner" style={innerStyle}>
<BlockFrame_Header {...props} connBtnRef={connBtnRef} changeConnModalAtom={changeConnModalAtom} />
{preview ? previewElem : children}
</div>
{preview ? null : (
<ChangeConnectionBlockModal
blockId={nodeModel.blockId}
viewModel={viewModel}
blockRef={blockModel?.blockRef}
changeConnModalAtom={changeConnModalAtom}
connBtnRef={connBtnRef}
/>
)}
<div className="block-frame-default-inner" style={innerStyle}>
<BlockFrame_Header {...props} changeConnModalAtom={changeConnModalAtom} />
{preview ? previewElem : children}
</div>
</div>
);
};
@ -336,11 +343,13 @@ const ChangeConnectionBlockModal = React.memo(
blockId,
viewModel,
blockRef,
connBtnRef,
changeConnModalAtom,
}: {
blockId: string;
viewModel: ViewModel;
blockRef: React.RefObject<HTMLDivElement>;
connBtnRef: React.RefObject<HTMLDivElement>;
changeConnModalAtom: jotai.PrimitiveAtom<boolean>;
}) => {
const [connSelected, setConnSelected] = React.useState("");
@ -377,15 +386,18 @@ const ChangeConnectionBlockModal = React.memo(
}
return (
<TypeAheadModal
anchor={blockRef}
suggestions={[]}
blockRef={blockRef}
anchorRef={connBtnRef}
// suggestions={[]}
onSelect={(selected: string) => {
changeConnection(selected);
globalStore.set(changeConnModalAtom, false);
}}
onKeyDown={(e) => keyutil.keydownWrapper(handleTypeAheadKeyDown)(e)}
onChange={(current: string) => setConnSelected(current)}
value={connSelected}
label="Switch Connection"
label="Switch connection"
onClickBackdrop={() => globalStore.set(changeConnModalAtom, false)}
/>
);
}

View File

@ -20,4 +20,5 @@ export interface BlockFrameProps {
preview: boolean;
numBlocksInTab?: number;
children?: React.ReactNode;
connBtnRef?: React.RefObject<HTMLDivElement>;
}

View File

@ -145,69 +145,67 @@ export const IconButton = React.memo(({ decl, className }: { decl: HeaderIconBut
);
});
interface ConnectionButtonProps {
connection: string;
changeConnModalAtom: jotai.PrimitiveAtom<boolean>;
}
export const ConnectionButton = React.memo(
({
blockId,
connection,
changeConnModalAtom,
}: {
blockId: string;
connection: string;
changeConnModalAtom: jotai.PrimitiveAtom<boolean>;
}) => {
const [connModalOpen, setConnModalOpen] = jotai.useAtom(changeConnModalAtom);
const buttonRef = React.useRef<HTMLDivElement>(null);
const isLocal = util.isBlank(connection) || connection == "local";
const connStatusAtom = getConnStatusAtom(connection);
const connStatus = jotai.useAtomValue(connStatusAtom);
const showDisconnectedSlash = !isLocal && !connStatus?.connected;
let connIconElem: React.ReactNode = null;
let color = "#53b4ea";
const clickHandler = function () {
setConnModalOpen(true);
};
let titleText = null;
if (isLocal) {
color = "var(--grey-text-color)";
titleText = "Connected to Local Machine";
connIconElem = (
<i
className={clsx(util.makeIconClass("laptop", false), "fa-stack-1x")}
style={{ color: color, marginRight: 2 }}
/>
);
} else {
titleText = "Connected to " + connection;
if (!connStatus?.connected) {
React.forwardRef<HTMLDivElement, ConnectionButtonProps>(
({ connection, changeConnModalAtom }: ConnectionButtonProps, ref) => {
const [connModalOpen, setConnModalOpen] = jotai.useAtom(changeConnModalAtom);
const isLocal = util.isBlank(connection) || connection == "local";
const connStatusAtom = getConnStatusAtom(connection);
const connStatus = jotai.useAtomValue(connStatusAtom);
const showDisconnectedSlash = !isLocal && !connStatus?.connected;
let connIconElem: React.ReactNode = null;
let color = "#53b4ea";
const clickHandler = function () {
setConnModalOpen(true);
};
let titleText = null;
if (isLocal) {
color = "var(--grey-text-color)";
titleText = "Disconnected from " + connection;
titleText = "Connected to Local Machine";
connIconElem = (
<i
className={clsx(util.makeIconClass("laptop", false), "fa-stack-1x")}
style={{ color: color, marginRight: 2 }}
/>
);
} else {
titleText = "Connected to " + connection;
if (!connStatus?.connected) {
color = "var(--grey-text-color)";
titleText = "Disconnected from " + connection;
}
connIconElem = (
<i
className={clsx(util.makeIconClass("arrow-right-arrow-left", false), "fa-stack-1x")}
style={{ color: color, marginRight: 2 }}
/>
);
}
connIconElem = (
<i
className={clsx(util.makeIconClass("arrow-right-arrow-left", false), "fa-stack-1x")}
style={{ color: color, marginRight: 2 }}
/>
return (
<div ref={ref} className={clsx("connection-button")} onClick={clickHandler} title={titleText}>
<span className="fa-stack connection-icon-box">
{connIconElem}
<i
className="fa-slash fa-solid fa-stack-1x"
style={{
color: color,
marginRight: "2px",
textShadow: "0 1px black, 0 1.5px black",
opacity: showDisconnectedSlash ? 1 : 0,
}}
/>
</span>
{isLocal ? null : <div className="connection-name">{connection}</div>}
</div>
);
}
return (
<div ref={buttonRef} className={clsx("connection-button")} onClick={clickHandler} title={titleText}>
<span className="fa-stack connection-icon-box">
{connIconElem}
<i
className="fa-slash fa-solid fa-stack-1x"
style={{
color: color,
marginRight: "2px",
textShadow: "0 1px black, 0 1.5px black",
opacity: showDisconnectedSlash ? 1 : 0,
}}
/>
</span>
{isLocal ? null : <div className="connection-name">{connection}</div>}
</div>
);
}
)
);
export const Input = React.memo(

View File

@ -8,6 +8,7 @@
position: relative;
min-height: 32px;
min-width: 100px;
width: 100%;
gap: 6px;
border: 2px solid var(--form-element-border-color);
background: var(--form-element-bg-color);

View File

@ -1,57 +1,39 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
.type-ahead-modal-wrapper {
@import "../mixins.less";
.type-ahead-modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
z-index: var(--zindex-modal-wrapper);
.type-ahead-modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(21, 23, 21, 0.7);
z-index: var(--zindex-modal-backdrop);
}
background-color: transparent;
z-index: var(--zindex-typeahead-modal-backdrop);
}
.type-ahead-modal {
position: relative;
z-index: var(--zindex-modal);
position: fixed;
z-index: var(--zindex-typeahead-modal);
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 16px;
border-radius: 8px;
border: 0.5px solid var(--modal-border-color);
border: 1px solid var(--modal-border-color);
background: var(--modal-bg-color);
box-shadow: 0px 8px 32px 0px rgba(0, 0, 0, 0.25);
height: auto;
.modal-close-btn {
position: absolute;
right: 8px;
top: 8px;
padding: 8px 12px;
i {
font-size: 18px;
}
}
box-shadow: 0px 13px 16px 0px rgba(0, 0, 0, 0.4);
.content-wrapper {
display: flex;
flex-direction: column;
width: 100%;
min-width: 100px;
padding: 6px;
flex-direction: column;
align-items: center;
&.has-suggestions {
gap: 6px;
}
.label {
opacity: 0.5;
@ -59,25 +41,67 @@
white-space: nowrap;
}
input {
width: 100%;
flex-shrink: 0;
.input {
border: none;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
height: 24px;
border-radius: 0;
input {
width: 100%;
flex-shrink: 0;
padding: 4px 6px;
}
.input-decoration.end-position {
margin: 6px;
i {
opacity: 0.3;
}
}
}
.suggestions-wrapper {
width: 100%;
overflow-y: auto;
overflow-x: hidden;
display: flex;
flex-direction: column;
gap: 10px;
.suggestion {
.suggestion-header {
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 12px;
opacity: 0.7;
letter-spacing: 0.11px;
padding: 4px 0px 0px 4px;
}
.suggestion-item {
width: 100%;
cursor: pointer;
padding: 7px 10px;
display: flex;
padding: 8px 6px;
align-items: center;
gap: 8px;
align-self: stretch;
&:hover {
background-color: var(--highlight-bg-color);
border-radius: 4px;
}
.name {
.ellipsis();
display: flex;
gap: 8px;
font-size: 11px;
font-weight: 400;
line-height: 14px;
}
}
}
}

View File

@ -1,101 +1,80 @@
import { Input } from "@/app/element/input";
import { InputDecoration } from "@/app/element/inputdecoration";
import { useDimensions } from "@/app/hook/useDimensions";
import { makeIconClass } from "@/util/util";
import clsx from "clsx";
import React, { forwardRef, useEffect, useRef, useState } from "react";
import ReactDOM from "react-dom";
import "./typeaheadmodal.less";
const dummy: SuggestionType[] = [
{
label: "Apple",
value: "apple",
},
{
label: "Banana",
value: "banana",
},
{
label: "Cherry",
value: "cherry",
},
{
label: "Date",
value: "date",
},
{
label: "Elderberry",
value: "elderberry",
},
{
label: "Apple",
value: "apple",
},
{
label: "Banana",
value: "banana",
},
{
label: "Cherry",
value: "cherry",
},
{
label: "Date",
value: "date",
},
{
label: "Elderberry",
value: "elderberry",
},
{
label: "Apple",
value: "apple",
},
{
label: "Banana",
value: "banana",
},
{
label: "Cherry",
value: "cherry",
},
{
label: "Date",
value: "date",
},
{
label: "Elderberry",
value: "elderberry",
},
];
type ConnStatus = "connected" | "connecting" | "disconnected" | "error";
type SuggestionType = {
interface BaseItem {
label: string;
icon?: string | React.ReactNode;
}
interface FileItem extends BaseItem {
value: string;
icon?: string;
};
}
interface ConnectionItem extends BaseItem {
status: ConnStatus;
iconColor: string;
}
interface ConnectionScope {
headerText?: string;
items: ConnectionItem[];
}
type SuggestionsType = FileItem | ConnectionItem | ConnectionScope;
interface SuggestionsProps {
suggestions?: SuggestionType[];
suggestions?: SuggestionsType[];
onSelect?: (_: string) => void;
}
const Suggestions = forwardRef<HTMLDivElement, SuggestionsProps>(({ suggestions, onSelect }: SuggestionsProps, ref) => {
const renderIcon = (icon: string | React.ReactNode) => {
if (typeof icon === "string") {
return <i className={makeIconClass(icon, false)}></i>;
}
return icon;
};
const renderItem = (item: BaseItem | ConnectionItem, index: number) => (
<div key={index} onClick={() => onSelect(item.label)} className="suggestion-item">
<div className="name">
{item.icon && renderIcon(item.icon)}
{item.label}
</div>
{"status" in item && item.status == "connected" && <i className={makeIconClass("fa-check", false)}></i>}
</div>
);
return (
<div ref={ref} className="suggestions-wrapper" style={{ marginTop: suggestions?.length > 0 ? "8px" : "0" }}>
{suggestions?.map((suggestion, index) => (
<div className="suggestion" key={index} onClick={() => onSelect(suggestion.value)}>
{suggestion.label}
</div>
))}
<div ref={ref} className="suggestions">
{suggestions.map((item, index) => {
if ("headerText" in item) {
return (
<div key={index}>
{item.headerText && <div className="suggestion-header">{item.headerText}</div>}
{item.items.map((subItem, subIndex) => renderItem(subItem, subIndex))}
</div>
);
}
return renderItem(item as BaseItem, index);
})}
</div>
);
});
interface TypeAheadModalProps {
anchor: React.MutableRefObject<HTMLDivElement>;
suggestions?: SuggestionType[];
anchorRef: React.RefObject<HTMLDivElement>;
blockRef?: React.RefObject<HTMLDivElement>;
suggestions?: SuggestionsType[];
label?: string;
className?: string;
value?: string;
@ -107,29 +86,30 @@ interface TypeAheadModalProps {
const TypeAheadModal = ({
className,
suggestions = dummy,
suggestions,
label,
anchor,
anchorRef,
blockRef,
value,
onChange,
onSelect,
onClickBackdrop,
onKeyDown,
onClickBackdrop,
}: TypeAheadModalProps) => {
const { width, height } = useDimensions(anchor);
const { width, height } = useDimensions(blockRef);
const modalRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLDivElement>(null);
const suggestionsWrapperRef = useRef<HTMLDivElement>(null);
const suggestionsRef = useRef<HTMLDivElement>(null);
const [suggestionsHeight, setSuggestionsHeight] = useState<number | undefined>(undefined);
const [modalHeight, setModalHeight] = useState<string | undefined>(undefined);
useEffect(() => {
if (modalRef.current && inputRef.current && suggestionsWrapperRef.current) {
if (modalRef.current && inputRef.current && suggestionsRef.current) {
const modalPadding = 32;
const inputHeight = inputRef.current.getBoundingClientRect().height;
let suggestionsTotalHeight = 0;
const suggestionItems = suggestionsWrapperRef.current.children;
const suggestionItems = suggestionsRef.current.children;
for (let i = 0; i < suggestionItems.length; i++) {
suggestionsTotalHeight += suggestionItems[i].getBoundingClientRect().height;
}
@ -143,7 +123,7 @@ const TypeAheadModal = ({
const padding = 16 * 2;
setSuggestionsHeight(computedHeight - inputHeight - padding);
}
}, [height, suggestions.length]);
}, [height, suggestions]);
const renderBackdrop = (onClick) => <div className="type-ahead-modal-backdrop" onClick={onClick}></div>;
@ -159,10 +139,18 @@ const TypeAheadModal = ({
onSelect && onSelect(value);
};
let modalWidth = width * 0.6;
let modalWidth = 300;
if (modalWidth < 300) {
modalWidth = Math.min(300, width * 0.95);
}
const anchorRect = anchorRef.current.getBoundingClientRect();
const blockRect = blockRef.current.getBoundingClientRect();
// Calculate positions relative to the wrapper
const topPosition = 30; // Adjusting the modal to be just below the anchor
const leftPosition = anchorRect.left - blockRect.left; // Relative left position to the wrapper div
const renderModal = () => (
<div className="type-ahead-modal-wrapper" onKeyDown={handleKeyDown}>
{renderBackdrop(onClickBackdrop)}
@ -170,20 +158,23 @@ const TypeAheadModal = ({
ref={modalRef}
className={clsx("type-ahead-modal", className)}
style={{
top: topPosition,
left: leftPosition,
width: modalWidth,
maxHeight: modalHeight,
}}
>
<div className="content-wrapper">
<div className={clsx("content-wrapper", { "has-suggestions": suggestions?.length })}>
<Input
ref={inputRef}
onChange={handleChange}
value={value}
autoFocus
placeholder={label}
decoration={{
startDecoration: (
<InputDecoration position="start">
<div className="label">{label}</div>
endDecoration: (
<InputDecoration>
<i className="fa-regular fa-magnifying-glass"></i>
</InputDecoration>
),
}}
@ -196,19 +187,21 @@ const TypeAheadModal = ({
overflowY: "auto",
}}
>
<Suggestions ref={suggestionsWrapperRef} suggestions={suggestions} onSelect={handleSelect} />
{suggestions && (
<Suggestions ref={suggestionsRef} suggestions={suggestions} onSelect={handleSelect} />
)}
</div>
</div>
</div>
</div>
);
if (anchor.current == null) {
if (blockRef && blockRef.current == null) {
return null;
}
return ReactDOM.createPortal(renderModal(), anchor.current);
return ReactDOM.createPortal(renderModal(), blockRef.current);
};
export { TypeAheadModal };
export type { SuggestionType };
export type { SuggestionsType };

View File

@ -42,6 +42,8 @@
--zindex-modal: 2;
--zindex-modal-wrapper: 500;
--zindex-modal-backdrop: 1;
--zindex-typeahead-modal: 100;
--zindex-typeahead-modal-backdrop: 90;
--zindex-elem-modal: 100;
--zindex-window-drag: 100;
--zindex-tab-name: 3;
@ -66,7 +68,8 @@
/* modal colors */
--modal-bg-color: #232323;
--modal-header-bottom-border-color: rgba(241, 246, 243, 0.15);
--modal-border-color: rgba(255, 255, 255, 0.12) /* toggle colors */ --toggle-bg-color: var(--border-color);
--modal-border-color: rgba(255, 255, 255, 0.12); /* toggle colors */
--toggle-bg-color: var(--border-color);
--toggle-thumb-color: var(--main-text-color);
--toggle-checked-bg-color: var(--accent-color);

View File

@ -14,7 +14,7 @@ import * as util from "@/util/util";
import clsx from "clsx";
import * as jotai from "jotai";
import { loadable } from "jotai/utils";
import { useCallback, useEffect, useRef } from "react";
import { createRef, useCallback, useEffect, useState } from "react";
import { CenteredDiv } from "../../element/quickelems";
import { CodeEditor } from "../codeeditor/codeeditor";
import { CSVView } from "./csvview";
@ -46,6 +46,7 @@ export class PreviewModel implements ViewModel {
endIconButtons: jotai.Atom<HeaderIconButton[]>;
ceReadOnly: jotai.PrimitiveAtom<boolean>;
isCeView: jotai.PrimitiveAtom<boolean>;
previewTextRef: React.RefObject<HTMLDivElement>;
fileName: jotai.Atom<string>;
connection: jotai.Atom<string>;
@ -70,6 +71,7 @@ export class PreviewModel implements ViewModel {
this.blockId = blockId;
this.showHiddenFiles = jotai.atom(true);
this.refreshVersion = jotai.atom(0);
this.previewTextRef = createRef();
this.ceReadOnly = jotai.atom(true);
this.isCeView = jotai.atom(false);
this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
@ -162,6 +164,7 @@ export class PreviewModel implements ViewModel {
{
elemtype: "text",
text: get(this.fileName),
ref: this.previewTextRef,
},
];
}
@ -241,12 +244,70 @@ export class PreviewModel implements ViewModel {
this.goParentDirectory = this.goParentDirectory.bind(this);
}
goHistory(newPath: string) {
const blockMeta = globalStore.get(this.blockAtom)?.meta;
async resolvePath(filePath, basePath) {
// Handle paths starting with "~" to refer to the home directory
if (filePath.startsWith("~")) {
try {
const conn = globalStore.get(this.connection);
const sf = await services.FileService.StatFile(conn, "~");
basePath = sf.path; // Update basePath to the fetched home directory path
filePath = basePath + filePath.slice(1); // Replace "~" with the fetched home directory path
} catch (error) {
console.error("Error fetching home directory:", error);
return basePath;
}
}
// If filePath is an absolute path, return it directly
if (filePath.startsWith("/")) {
return filePath;
}
const stack = basePath.split("/");
// Ensure no empty segments from trailing slashes
if (stack[stack.length - 1] === "") {
stack.pop();
}
// Process the filePath parts
filePath.split("/").forEach((part) => {
if (part === "..") {
// Go up one level, avoid going above root level
if (stack.length > 1) {
stack.pop();
}
} else if (part === "." || part === "") {
// Ignore current directory marker and empty parts
} else {
// Normal path part, add to the stack
stack.push(part);
}
});
return stack.join("/");
}
async isValidPath(path) {
try {
const conn = globalStore.get(this.connection);
const sf = await services.FileService.StatFile(conn, path);
const isValid = !sf.notfound;
return isValid;
} catch (error) {
console.error("Error checking path validity:", error);
return false;
}
}
async goHistory(newPath, isValidated = false) {
const fileName = globalStore.get(this.fileName);
if (fileName == null) {
return;
}
if (!isValidated) {
newPath = await this.resolvePath(newPath, fileName);
const isValid = await this.isValidPath(newPath);
if (!isValid) {
return;
}
}
const blockMeta = globalStore.get(this.blockAtom)?.meta;
const updateMeta = historyutil.goHistory("file", fileName, newPath, blockMeta);
if (updateMeta == null) {
return;
@ -572,8 +633,17 @@ function iconForFile(mimeType: string, fileName: string): string {
}
}
function PreviewView({ blockId, model }: { blockId: string; model: PreviewModel }) {
const contentRef = useRef<HTMLDivElement>(null);
function PreviewView({
blockId,
blockRef,
contentRef,
model,
}: {
blockId: string;
blockRef: React.RefObject<HTMLDivElement>;
contentRef: React.RefObject<HTMLDivElement>;
model: PreviewModel;
}) {
const fileNameAtom = model.fileName;
const statFileAtom = model.statFile;
const fileMimeTypeAtom = model.fileMimeType;
@ -590,6 +660,10 @@ function PreviewView({ blockId, model }: { blockId: string; model: PreviewModel
const typeAhead = jotai.useAtomValue(atoms.typeAheadModalAtom);
let blockIcon = iconForFile(mimeType, fileName);
const [filePath, setFilePath] = useState("");
const [openFileError, setOpenFileError] = useState("");
const [openFileModal, setOpenFileModal] = useState(false);
// ensure consistent hook calls
const specializedView = (() => {
let view: React.ReactNode = null;
@ -646,23 +720,47 @@ function PreviewView({ blockId, model }: { blockId: string; model: PreviewModel
const handleKeyDown = useCallback(
(waveEvent: WaveKeyboardEvent): boolean => {
if (keyutil.checkKeyPressed(waveEvent, "Cmd:o")) {
globalStore.set(atoms.typeAheadModalAtom, {
...(typeAhead as TypeAheadModalType),
[blockId]: true,
});
return true;
}
if (keyutil.checkKeyPressed(waveEvent, "Cmd:d") || keyutil.checkKeyPressed(waveEvent, "Enter")) {
globalStore.set(atoms.typeAheadModalAtom, {
...(typeAhead as TypeAheadModalType),
[blockId]: false,
});
const updateModalAndError = (isOpen, errorMsg = "") => {
setOpenFileModal(isOpen);
setOpenFileError(errorMsg);
};
const handleEnterPress = async () => {
const newPath = await model.resolvePath(filePath, fileName);
const isValidPath = await model.isValidPath(newPath);
if (isValidPath) {
updateModalAndError(false);
await model.goHistory(newPath, true);
} else {
updateModalAndError(true, "The path you entered does not exist.");
}
model.giveFocus();
return;
}
return isValidPath;
};
const handleCommandOperations = async () => {
if (keyutil.checkKeyPressed(waveEvent, "Cmd:o")) {
updateModalAndError(true);
return true;
}
if (keyutil.checkKeyPressed(waveEvent, "Cmd:d")) {
updateModalAndError(false);
return false;
}
if (keyutil.checkKeyPressed(waveEvent, "Enter")) {
return handleEnterPress();
}
return false;
};
handleCommandOperations().catch((error) => {
console.error("Error handling key down:", error);
updateModalAndError(true, "An error occurred during operation.");
return false;
});
return false;
},
[typeAhead, model, blockId]
[typeAhead, model, blockId, filePath, fileName]
);
const handleFileSuggestionSelect = (value) => {
@ -672,6 +770,10 @@ function PreviewView({ blockId, model }: { blockId: string; model: PreviewModel
});
};
const handleFileSuggestionChange = (value) => {
setFilePath(value);
};
useEffect(() => {
const blockIconOverrideAtom = useBlockAtom<string>(blockId, "blockicon:override", () => {
return jotai.atom<string>(null);
@ -681,12 +783,16 @@ function PreviewView({ blockId, model }: { blockId: string; model: PreviewModel
return (
<>
{typeAhead[blockId] && (
{openFileModal && (
<TypeAheadModal
label="Open file:"
anchor={contentRef}
label="Open file"
suggestions={[]}
blockRef={blockRef}
anchorRef={model.previewTextRef}
onKeyDown={(e) => keyutil.keydownWrapper(handleKeyDown)(e)}
onSelect={handleFileSuggestionSelect}
onChange={handleFileSuggestionChange}
onClickBackdrop={() => setOpenFileModal(false)}
/>
)}
<div

View File

@ -161,6 +161,7 @@ declare global {
type HeaderText = {
elemtype: "text";
text: string;
ref?: React.MutableRefObject<HTMLDivElement>;
};
type HeaderInput = {