Skip to content

Commit e2a7724

Browse files
committed
Prompt to create file or directory
1 parent 9d5aa2c commit e2a7724

9 files changed

Lines changed: 245 additions & 28 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
.full-app-prompt {
2+
backdrop-filter: brightness(80%) blur(1px);
3+
position: fixed;
4+
z-index: 9999;
5+
display: flex;
6+
justify-content: center;
7+
align-items: center;
8+
9+
&-content {
10+
width: 300px;
11+
display: flex;
12+
flex-direction: column;
13+
box-shadow: 0 0 40px rgb(0, 0, 0, 0.5);
14+
border-radius: 10px;
15+
16+
&__top-box {
17+
padding: 10px;
18+
box-sizing: border-box;
19+
display: flex;
20+
justify-content: space-between;
21+
align-items: center;
22+
}
23+
24+
&__input-area {
25+
padding: 40px;
26+
gap: 10px;
27+
box-sizing: border-box;
28+
border-top: 1px solid rgb(0, 0, 0, 0.5);
29+
display: flex;
30+
flex-direction: column;
31+
justify-content: center;
32+
align-items: center;
33+
}
34+
}
35+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import React, { useEffect, useRef, useState } from "react";
2+
import "./FullAppPrompt.scss";
3+
import { Rect } from "../../hooks/useDragToResize";
4+
import useDetectMouseDownOutside from "../../hooks/useDetectMouseDownOutside";
5+
import useSystemSettings from "../../stores/systemSettingsStore";
6+
7+
interface FullAppPromptProps<Element extends HTMLElement> {
8+
appRef: React.RefObject<Element>;
9+
promptName: string;
10+
fieldName: string;
11+
close: () => void;
12+
submit: (value: string) => void;
13+
validate: (value: string) => string | null;
14+
}
15+
16+
function FullAppPrompt<Element extends HTMLElement>({
17+
appRef,
18+
fieldName,
19+
promptName,
20+
close,
21+
submit,
22+
validate,
23+
}: FullAppPromptProps<Element>) {
24+
const thisRef = useRef<HTMLDivElement>(null);
25+
const settings = useSystemSettings();
26+
const value = useRef<string>("");
27+
const [error, setError] = useState<string | null>(validate(""));
28+
const [rect, setRect] = useState<Rect>({
29+
height: 0,
30+
left: 0,
31+
top: 0,
32+
width: 0,
33+
});
34+
35+
useDetectMouseDownOutside({ elementRef: thisRef, onMouseDown: close });
36+
37+
useEffect(() => {
38+
if (appRef.current) {
39+
const bounds = appRef.current.getBoundingClientRect();
40+
setRect({
41+
height: bounds.height ?? 0,
42+
left: bounds.left ?? 0,
43+
top: bounds.top ?? 0,
44+
width: bounds.width ?? 0,
45+
});
46+
}
47+
}, [appRef]);
48+
49+
function handleCancel() {
50+
close();
51+
}
52+
53+
function handleSubmit() {
54+
if (error) return;
55+
submit(value.current);
56+
close();
57+
}
58+
59+
function handleValueChange(e: React.ChangeEvent<HTMLInputElement>) {
60+
value.current = e.currentTarget.value;
61+
setError(validate(value.current));
62+
}
63+
64+
function handleContextMenu(e: React.MouseEvent) {
65+
e.stopPropagation();
66+
e.preventDefault();
67+
}
68+
69+
return (
70+
<div
71+
className="full-app-prompt"
72+
ref={thisRef}
73+
style={{ ...rect }}
74+
onContextMenu={handleContextMenu}
75+
>
76+
<div
77+
className="full-app-prompt-content"
78+
style={{ backgroundColor: settings.mainColor }}
79+
>
80+
<div className="full-app-prompt-content__top-box">
81+
<button
82+
className="full-app-prompt-content__top-box__cancel"
83+
onClick={handleCancel}
84+
>
85+
Cancel
86+
</button>
87+
<span className="full-app-prompt-content__top-box__title">
88+
{promptName}
89+
</span>
90+
<button
91+
className="full-app-prompt-content__top-box__submit"
92+
onClick={handleSubmit}
93+
disabled={!!error}
94+
>
95+
Submit
96+
</button>
97+
</div>
98+
99+
<div className="full-app-prompt-content__input-area">
100+
<span className="full-app-prompt-content__input-area__name">
101+
{`${fieldName}:`}
102+
</span>
103+
<input
104+
autoFocus
105+
className="full-app-prompt-content__input-area__value"
106+
onChange={handleValueChange}
107+
></input>
108+
<span>{error}</span>
109+
</div>
110+
</div>
111+
</div>
112+
);
113+
}
114+
115+
export default FullAppPrompt;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import FullAppPrompt from "./FullAppPrompt";
2+
3+
export default FullAppPrompt;

src/components/MenuItems/MenuItems.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,11 @@ function MenuItem({
115115

116116
function handleOnClick(e: React.MouseEvent<HTMLDivElement>) {
117117
e.stopPropagation();
118-
if (!items) return;
119-
setOpen(!open);
118+
if (action) {
119+
action();
120+
} else if (items) {
121+
setOpen(!open);
122+
}
120123
}
121124

122125
return (
@@ -127,9 +130,7 @@ function MenuItem({
127130
onMouseEnter={handleOnMouseEnter}
128131
onMouseLeave={handleOnMouseLeave}
129132
>
130-
<span className="menu-item__title" onClick={action}>
131-
{title}
132-
</span>
133+
<span className="menu-item__title">{title}</span>
133134
{items && open && (
134135
<MenuItems
135136
items={items}

src/hooks/useDetectMouseDownOutside.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,19 @@ function useDetectMouseDownOutside<Element extends HTMLElement>({
2424
}: UseDetectMouseDownOutsideProps<Element>) {
2525
useEffect(() => {
2626
function handler(e: MouseEvent) {
27-
// If the element has been clicked on, we dont want to invoke the callback
28-
if (e.target === elementRef.current) return;
29-
30-
for (const child of elementRef.current?.childNodes ?? []) {
31-
// If any of the elements children have been clicked,
32-
// we dont want to invoke the callback
33-
if (e.target === child) return;
27+
const bounds = elementRef.current?.getBoundingClientRect();
28+
if (e.clientX < (bounds?.left ?? 0)) {
29+
return onMouseDown();
30+
}
31+
if (e.clientX > (bounds?.left ?? 0) + (bounds?.width ?? 0)) {
32+
return onMouseDown();
33+
}
34+
if (e.clientY < (bounds?.top ?? 0)) {
35+
return onMouseDown();
36+
}
37+
if (e.clientY > (bounds?.top ?? 0) + (bounds?.height ?? 0)) {
38+
return onMouseDown();
3439
}
35-
36-
// Click must be outside, so invoke callback
37-
onMouseDown();
3840
}
3941

4042
window.addEventListener("mousedown", handler);

src/programs/FileBrowser/FileBrowser.scss

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,10 @@
6161

6262
&__side-bar {
6363
grid-area: side-bar;
64-
// border-right: 1px solid rgb(0, 0, 0, 0.5);
6564
width: 100%;
6665
height: 100%;
6766
&__favorites {
6867
width: 100%;
69-
// padding: 0 10px;
7068
box-sizing: border-box;
7169
}
7270

src/programs/FileBrowser/FileBrowser.tsx

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import React, { useEffect, useRef, useState } from "react";
22
import { ReactComponent as FolderIcon } from "../../assets/icons/folder-icon.svg";
33

4-
import { FSDirectory, FSObject, isFSDirectory } from "../../stores/localFS";
4+
import useLocalFS, {
5+
FSDirectory,
6+
FSObject,
7+
FSObjectType,
8+
isFSDirectory,
9+
} from "../../stores/localFS";
510
import useLocalFSWithHistory from "../../hooks/useLocalFSWithHistory";
611

712
import "./FileBrowser.scss";
813
import AppSideBar from "../../components/AppSideBar";
914
import ContextMenu from "../../ContextMenu/ContextMenu";
1015
import { getMainContentContextItems } from "./contextMenus";
16+
import FullAppPrompt from "../../components/FullAppPrompt";
1117
const defaultPath = "/home/user";
1218

1319
interface FileBrowserProps {
@@ -57,30 +63,47 @@ function TopBar({
5763
interface MainContentProps {
5864
currentDirectory: FSDirectory;
5965
openFSObject: (fsObject: FSObject) => void;
66+
appRef: React.RefObject<HTMLDivElement>;
6067
}
6168

62-
function MainContent({ currentDirectory, openFSObject }: MainContentProps) {
69+
function MainContent({
70+
currentDirectory,
71+
openFSObject,
72+
appRef,
73+
}: MainContentProps) {
6374
const [selected, setSelected] = useState<string>("");
6475
const clickPosition = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
65-
const [open, setOpen] = useState(false);
76+
const [contextMenuOpen, setContextMenuOpen] = useState(false);
77+
const [promptFor, setPromptFor] = useState<FSObjectType | null>(null);
78+
const fs = useLocalFS();
6679

6780
function handleRightClick(e: React.MouseEvent) {
6881
clickPosition.current = { x: e.clientX, y: e.clientY };
6982
e.stopPropagation();
7083
e.preventDefault();
71-
setOpen(true);
84+
setContextMenuOpen(true);
7285
}
7386

7487
return (
7588
<div
7689
className="file-browser__main-content"
7790
onContextMenu={handleRightClick}
7891
>
79-
{open && (
92+
{promptFor && (
93+
<FullAppPrompt
94+
fieldName="Name"
95+
promptName={`Create ${promptFor}`}
96+
appRef={appRef}
97+
close={() => setPromptFor(null)}
98+
submit={(value) => fs.create(promptFor, value, currentDirectory)}
99+
validate={fs.validateFSObjectName}
100+
/>
101+
)}
102+
{contextMenuOpen && (
80103
<ContextMenu
81104
position={clickPosition.current}
82-
items={getMainContentContextItems()}
83-
close={() => setOpen(!open)}
105+
items={getMainContentContextItems(setPromptFor, setContextMenuOpen)}
106+
close={() => setContextMenuOpen(!contextMenuOpen)}
84107
/>
85108
)}
86109
{Object.values<FSObject>(currentDirectory.contents).map((fsObject) => (
@@ -133,6 +156,7 @@ function DirectoryOrFile({
133156

134157
function FileBrowser({ path = defaultPath }: FileBrowserProps) {
135158
const fs = useLocalFSWithHistory(path);
159+
const appRef = useRef<HTMLDivElement>(null);
136160

137161
const [pathSearch, setPathSearch] = useState<string>(
138162
fs.currentDirectory.path
@@ -153,7 +177,7 @@ function FileBrowser({ path = defaultPath }: FileBrowserProps) {
153177
}
154178

155179
return (
156-
<div className="file-browser">
180+
<div className="file-browser" ref={appRef}>
157181
<TopBar
158182
pathSearch={pathSearch}
159183
onPathInputChange={onPathInputChange}
@@ -173,6 +197,7 @@ function FileBrowser({ path = defaultPath }: FileBrowserProps) {
173197
<MainContent
174198
currentDirectory={fs.currentDirectory}
175199
openFSObject={fs.navToObject}
200+
appRef={appRef}
176201
/>
177202
<div className="file-browser__bottom-bar"></div>
178203
</div>

src/programs/FileBrowser/contextMenus.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
11
import { MenuItemProps } from "../../components/MenuItems";
2+
import { FSObjectType } from "../../stores/localFS";
23

3-
export function getMainContentContextItems(): Array<MenuItemProps> {
4+
export function getMainContentContextItems(
5+
setShowPrompt: (type: FSObjectType) => void,
6+
setContextMenuOpen: (open: boolean) => void
7+
): Array<MenuItemProps> {
48
return [
59
{
610
title: "Create Directory",
11+
action: () => {
12+
setShowPrompt("directory");
13+
setContextMenuOpen(false);
14+
},
715
},
816
{
917
title: "Create File",
18+
action: () => {
19+
setShowPrompt("file");
20+
setContextMenuOpen(false);
21+
},
1022
},
1123
{
1224
title: "Open in Terminal",

0 commit comments

Comments
 (0)