Added text editor

This commit is contained in:
Prozilla 2023-07-29 17:40:47 +02:00
parent 53dca5aae6
commit 1254d03eb6
No known key found for this signature in database
GPG key ID: 5858DFE71CAF31EE
15 changed files with 309 additions and 6 deletions

View 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

View 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>
);
}

View file

@ -0,0 +1,6 @@
.Container {
display: flex;
width: 100%;
height: 1.25rem;
background-color: var(--background-color-a);
}

View file

@ -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;

View 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>
);
}

View file

@ -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;
}

View 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>
)
}

View 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%);
}

View file

@ -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;

View file

@ -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;

View file

@ -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) {

View 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>;
}

View file

@ -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");
});
});