Added text editor
This commit is contained in:
parent
53dca5aae6
commit
1254d03eb6
15 changed files with 309 additions and 6 deletions
57
public/media/applications/icons/text-editor.svg
Normal file
57
public/media/applications/icons/text-editor.svg
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_77_54)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M27 11C17.0589 11 9 19.0589 9 29V171C9 180.941 17.0589 189 27 189H121C130.941 189 139 180.941 139 171V72L78 11H27Z" fill="#C8D6E5"/>
|
||||
<g filter="url(#filter0_d_77_54)">
|
||||
<g clip-path="url(#clip1_77_54)">
|
||||
<path d="M77.9997 72.0001H139L77.9997 11.0001V72.0001Z" fill="#8395A7"/>
|
||||
</g>
|
||||
</g>
|
||||
<rect x="28" y="91" width="91" height="11" rx="5.5" fill="#1B2631"/>
|
||||
<rect x="28" y="119" width="91" height="11" rx="5.5" fill="#1B2631"/>
|
||||
<rect x="28" y="147" width="91" height="11" rx="5.5" fill="#1B2631"/>
|
||||
<g filter="url(#filter1_d_77_54)">
|
||||
<path d="M109.723 149.257L106.872 136.905C106.393 134.831 106.76 132.652 107.893 130.85L153.286 58.6C155.634 54.8637 160.563 53.7336 164.304 56.0743L179.053 65.3036C182.788 67.6408 183.931 72.558 181.608 76.3022L136.619 148.835C135.495 150.645 133.696 151.933 131.62 152.413L119.318 155.253C115.013 156.247 110.717 153.562 109.723 149.257Z" fill="#576574"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_d_77_54)">
|
||||
<path d="M173.714 42.5976L186.988 50.9041C191.132 53.4941 192.393 58.958 189.804 63.1023L182.716 74.4616L154.442 56.7695L161.531 45.4102C164.116 41.2667 169.573 40.0068 173.714 42.5976Z" fill="#FF9F43"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_77_54" x="58" y="-109" width="201" height="201" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="10"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_77_54"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_77_54" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_d_77_54" x="86.6665" y="34.855" width="116.144" height="140.605" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="10"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_77_54"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_77_54" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter2_d_77_54" x="134.442" y="21.2509" width="76.707" height="73.2107" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="10"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_77_54"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_77_54" result="shape"/>
|
||||
</filter>
|
||||
<clipPath id="clip0_77_54">
|
||||
<rect width="200" height="200" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_77_54">
|
||||
<rect x="78" y="-89" width="161" height="161" rx="18" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
36
src/components/applications/.common/HeaderMenu.jsx
Normal file
36
src/components/applications/.common/HeaderMenu.jsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { DropdownButton } from "../../utils/DropdownButton.jsx";
|
||||
import styles from "./HeaderMenu.module.css";
|
||||
|
||||
/**
|
||||
* @param {Object} props
|
||||
* @param {Function} props.onNew
|
||||
* @param {Function} props.onOpen
|
||||
* @param {Function} props.onSave
|
||||
* @param {Function} props.onSaveAs
|
||||
* @param {Function} props.onExit
|
||||
*/
|
||||
export function HeaderMenu({ onNew, onOpen, onSave, onSaveAs, onExit }) {
|
||||
return (
|
||||
<div className={styles.Container}>
|
||||
<DropdownButton label="File" options={{
|
||||
"New": () => {
|
||||
onNew?.();
|
||||
},
|
||||
"Open": () => {
|
||||
onOpen?.();
|
||||
},
|
||||
"Save": () => {
|
||||
onSave?.();
|
||||
},
|
||||
"Save as": () => {
|
||||
onSaveAs?.();
|
||||
},
|
||||
"Exit": () => {
|
||||
onExit?.();
|
||||
},
|
||||
}}/>
|
||||
<DropdownButton label="Edit"/>
|
||||
<DropdownButton label="View"/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
.Container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 1.25rem;
|
||||
background-color: var(--background-color-a);
|
||||
}
|
||||
|
|
@ -18,6 +18,9 @@ function FilePreview({ file }) {
|
|||
case "png":
|
||||
preview = <FontAwesomeIcon icon={faImage}/>
|
||||
break;
|
||||
case "txt":
|
||||
preview = <FontAwesomeIcon icon={faFileLines}/>
|
||||
break;
|
||||
default:
|
||||
preview = <FontAwesomeIcon icon={faFile}/>
|
||||
break;
|
||||
|
|
|
|||
56
src/components/applications/text-editor/TextEditor.jsx
Normal file
56
src/components/applications/text-editor/TextEditor.jsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { useEffect, useState } from "react";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { VirtualFile } from "../../../features/virtual-drive/virtual-file.js";
|
||||
import styles from "./TextEditor.module.css";
|
||||
import { HeaderMenu } from "../.common/HeaderMenu.jsx";
|
||||
|
||||
/**
|
||||
* @param {Object} props
|
||||
* @param {VirtualFile} props.file
|
||||
*/
|
||||
export function TextEditor({ file }) {
|
||||
const [currentFile, setCurrentFile] = useState(file);
|
||||
const [content, setContent] = useState(file?.content);
|
||||
|
||||
useEffect(() => {
|
||||
setContent(currentFile?.content ?? "");
|
||||
}, [currentFile]);
|
||||
|
||||
const newText = () => {
|
||||
setCurrentFile(null);
|
||||
}
|
||||
|
||||
const saveTextAs = () => {
|
||||
|
||||
}
|
||||
|
||||
const saveText = () => {
|
||||
if (currentFile == null)
|
||||
return saveTextAs();
|
||||
|
||||
currentFile.content = content;
|
||||
}
|
||||
|
||||
const onChange = (event) => {
|
||||
const value = event.target.value;
|
||||
return setContent(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.Container}>
|
||||
<HeaderMenu
|
||||
onNew={newText}
|
||||
onSave={saveText}
|
||||
onSaveAs={saveTextAs}
|
||||
/>
|
||||
<textarea
|
||||
className={styles.View}
|
||||
value={content}
|
||||
onChange={onChange}
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
.Container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.View {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 1rem;
|
||||
color: var(--foreground-color-a);
|
||||
background-color: var(--background-color-c);
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 1rem;
|
||||
font-family: var(--body-font-family);
|
||||
resize: none;
|
||||
}
|
||||
36
src/components/utils/DropdownButton.jsx
Normal file
36
src/components/utils/DropdownButton.jsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { useState } from "react";
|
||||
import styles from "./DropdownButton.module.css";
|
||||
import OutsideClickListener from "../../hooks/utils/outsideClick.js";
|
||||
|
||||
/**
|
||||
* @param {Object} props
|
||||
* @param {String} props.label
|
||||
* @param {Object.<string, Function>} props.options
|
||||
* @returns
|
||||
*/
|
||||
export function DropdownButton({ label, options }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<OutsideClickListener onOutsideClick={() => { setOpen(false); }}>
|
||||
<div className={styles.Container}>
|
||||
<button className={styles.Button} onClick={() => { setOpen(!open) }}>
|
||||
{label}
|
||||
</button>
|
||||
{open && options
|
||||
? (<div className={styles.Dropdown}>
|
||||
{Object.entries(options).map(([label, callback]) =>
|
||||
<button key={label} onClick={() => {
|
||||
setOpen(false);
|
||||
callback();
|
||||
}}>
|
||||
{label}
|
||||
</button>
|
||||
)}
|
||||
</div>)
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
</OutsideClickListener>
|
||||
)
|
||||
}
|
||||
44
src/components/utils/DropdownButton.module.css
Normal file
44
src/components/utils/DropdownButton.module.css
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
.Container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.Button {
|
||||
display: block;
|
||||
width: auto;
|
||||
height: 100%;
|
||||
padding: 0 0.5rem;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Button:hover {
|
||||
background-color: rgba(255, 255, 255, 5%);
|
||||
}
|
||||
|
||||
.Dropdown {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
background-color: var(--background-color-b);
|
||||
}
|
||||
|
||||
.Dropdown button {
|
||||
width: 100%;
|
||||
padding: 0.125rem 0.4rem;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 0.8rem;
|
||||
text-align: start;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Dropdown button:hover {
|
||||
background-color: rgba(255, 255, 255, 5%);
|
||||
}
|
||||
|
|
@ -7,8 +7,8 @@
|
|||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 200px;
|
||||
min-height: 50px;
|
||||
min-width: 300px;
|
||||
min-height: 150px;
|
||||
background-color: var(--background-color-c);
|
||||
resize: both;
|
||||
overflow: hidden;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
/* eslint-disable eqeqeq */
|
||||
import { FileExplorer } from "../../components/applications/file-explorer/FileExplorer.jsx";
|
||||
import { MediaViewer } from "../../components/applications/media-viewer/MediaViewer.jsx";
|
||||
import { WebView } from "../../components/applications/templates/WebView.jsx";
|
||||
import { WebView } from "../../components/applications/.templates/WebView.jsx";
|
||||
import { Terminal } from "../../components/applications/terminal/Terminal.jsx";
|
||||
import { TextEditor } from "../../components/applications/text-editor/TextEditor.jsx";
|
||||
import Application from "./application.js";
|
||||
|
||||
export default class ApplicationsManager {
|
||||
static APPLICATIONS = [
|
||||
new Application("Terminal", "terminal", Terminal),
|
||||
// new Application("Browser", "browser"),
|
||||
new Application("Text Editor", "text-editor", TextEditor),
|
||||
new Application("Code Editor", "code-editor"),
|
||||
new Application("File Explorer", "file-explorer", FileExplorer),
|
||||
new Application("Media Viewer", "media-viewer", MediaViewer),
|
||||
|
|
@ -47,6 +49,9 @@ export default class ApplicationsManager {
|
|||
case "png":
|
||||
app = this.getApplication("media-viewer");
|
||||
break;
|
||||
case "txt":
|
||||
app = this.getApplication("text-editor");
|
||||
break;
|
||||
}
|
||||
|
||||
return app;
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@ export default class WindowsManager {
|
|||
*/
|
||||
openFile(file) {
|
||||
const app = ApplicationsManager.getFileApplication(file.extension);
|
||||
this.open(app.id, { file });
|
||||
if (app != null)
|
||||
this.open(app.id, { file });
|
||||
}
|
||||
|
||||
close(windowId) {
|
||||
|
|
|
|||
38
src/hooks/utils/outsideClick.js
Normal file
38
src/hooks/utils/outsideClick.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* https://stackoverflow.com/a/42234988
|
||||
*/
|
||||
|
||||
import React, { useRef, useEffect } from "react";
|
||||
|
||||
/**
|
||||
* Hook that alerts clicks outside of the passed ref
|
||||
*/
|
||||
function useOutsideClickListener(ref, callback) {
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Alert if clicked on outside of element
|
||||
*/
|
||||
function handleClickOutside(event) {
|
||||
if (ref.current && !ref.current.contains(event.target)) {
|
||||
callback(event);
|
||||
}
|
||||
}
|
||||
// Bind the event listener
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
// Unbind the event listener on clean up
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [ref, callback]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} props
|
||||
* @param {Function} props.onOutsideClick
|
||||
*/
|
||||
export default function OutsideClickListener({ onOutsideClick, children }) {
|
||||
const wrapperRef = useRef(null);
|
||||
useOutsideClickListener(wrapperRef, onOutsideClick);
|
||||
|
||||
return <div ref={wrapperRef}>{children}</div>;
|
||||
}
|
||||
|
|
@ -46,9 +46,13 @@ function initVirtualRoot(virtualRoot) {
|
|||
file.setSource("/media/wallpapers/wallpaper3.png")
|
||||
}).createFile("Wallpaper_4", "png", (file) => {
|
||||
file.setSource("/media/wallpapers/wallpaper4.png")
|
||||
})
|
||||
});
|
||||
})
|
||||
.createFolder("Documents", (folder) => {
|
||||
folder.createFile("text", "txt", (file) => {
|
||||
file.setContent("Hello world!");
|
||||
});
|
||||
})
|
||||
.createFolder("Documents")
|
||||
.createFolder("Desktop");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue