1
0
This commit is contained in:
Zef Hemel 2022-02-23 14:09:26 +01:00
parent 6158713dd1
commit aa7929ea29
6 changed files with 226 additions and 169 deletions

View File

@ -1,4 +1,6 @@
Home page Home page
[[Great Parenting]] [[Great Parenting]]
[[1:1s]] [[1:1s]]
Sup

View File

@ -18,7 +18,7 @@ import ReactDOM from "react-dom";
import * as commands from "./commands"; import * as commands from "./commands";
import { CommandPalette } from "./components/commandpalette"; import { CommandPalette } from "./components/commandpalette";
import { NoteNavigator } from "./components/notenavigator"; import { NoteNavigator } from "./components/notenavigator";
import { HttpFileSystem } from "./fs"; import { FileSystem, HttpFileSystem } from "./fs";
import { lineWrapper } from "./lineWrapper"; import { lineWrapper } from "./lineWrapper";
import { markdown } from "./markdown"; import { markdown } from "./markdown";
import customMarkDown from "./parser"; import customMarkDown from "./parser";
@ -30,10 +30,7 @@ import { syntaxTree } from "@codemirror/language";
import * as util from "./util"; import * as util from "./util";
import { NoteMeta } from "./types"; import { NoteMeta } from "./types";
const fs = new HttpFileSystem("http://localhost:2222/fs");
const initialViewState: AppViewState = { const initialViewState: AppViewState = {
currentNote: "",
isSaved: false, isSaved: false,
showNoteNavigator: false, showNoteNavigator: false,
showCommandPalette: false, showCommandPalette: false,
@ -41,26 +38,44 @@ const initialViewState: AppViewState = {
}; };
import { CompletionContext, CompletionResult } from "@codemirror/autocomplete"; import { CompletionContext, CompletionResult } from "@codemirror/autocomplete";
import { NavigationBar } from "./components/navigation_bar";
import { StatusBar } from "./components/status_bar";
class NoteState {
editorState: EditorState;
scrollTop: number;
constructor(editorState: EditorState, scrollTop: number) {
this.editorState = editorState;
this.scrollTop = scrollTop;
}
}
class Editor { class Editor {
view: EditorView; editorView?: EditorView;
currentNote: string; viewState: AppViewState;
dispatch: React.Dispatch<Action>; viewDispatch: React.Dispatch<Action>;
allNotes: NoteMeta[]; $hashChange?: () => void;
openNotes: Map<string, NoteState>;
fs: FileSystem;
constructor( constructor(fs: FileSystem, parent: Element) {
parent: Element, this.fs = fs;
currentNote: string, this.viewState = initialViewState;
text: string, this.viewDispatch = () => {};
dispatch: React.Dispatch<Action> this.render(parent);
) { this.editorView = new EditorView({
this.view = new EditorView({ state: this.createEditorState(""),
state: this.createEditorState(text), parent: document.getElementById("editor")!,
parent: parent,
}); });
this.currentNote = currentNote; this.addListeners();
this.dispatch = dispatch; this.loadNoteList();
this.allNotes = []; this.openNotes = new Map();
this.$hashChange!();
}
get currentNote(): string | undefined {
return this.viewState.currentNote;
} }
createEditorState(text: string): EditorState { createEditorState(text: string): EditorState {
@ -141,7 +156,7 @@ class Editor {
key: "Ctrl-p", key: "Ctrl-p",
mac: "Cmd-p", mac: "Cmd-p",
run: (target): boolean => { run: (target): boolean => {
this.dispatch({ type: "start-navigate" }); this.viewDispatch({ type: "start-navigate" });
return true; return true;
}, },
}, },
@ -149,7 +164,7 @@ class Editor {
key: "Ctrl-.", key: "Ctrl-.",
mac: "Cmd-.", mac: "Cmd-.",
run: (target): boolean => { run: (target): boolean => {
this.dispatch({ type: "show-palette" }); this.viewDispatch({ type: "show-palette" });
return true; return true;
}, },
}, },
@ -177,7 +192,7 @@ class Editor {
// TODO: put something in the cm-completionIcon-note style // TODO: put something in the cm-completionIcon-note style
return { return {
from: prefix.from + 2, from: prefix.from + 2,
options: this.allNotes.map((noteMeta) => ({ options: this.viewState.allNotes.map((noteMeta) => ({
label: noteMeta.name, label: noteMeta.name,
type: "note", type: "note",
})), })),
@ -186,7 +201,7 @@ class Editor {
update(value: null, transaction: Transaction): null { update(value: null, transaction: Transaction): null {
if (transaction.docChanged) { if (transaction.docChanged) {
this.dispatch({ this.viewDispatch({
type: "note-updated", type: "note-updated",
}); });
} }
@ -194,11 +209,6 @@ class Editor {
return null; return null;
} }
load(name: string, text: string) {
this.currentNote = name;
this.view.setState(this.createEditorState(text));
}
click(event: MouseEvent, view: EditorView) { click(event: MouseEvent, view: EditorView) {
if (event.metaKey || event.ctrlKey) { if (event.metaKey || event.ctrlKey) {
let coords = view.posAtCoords(event)!; let coords = view.posAtCoords(event)!;
@ -224,11 +234,26 @@ class Editor {
} }
async save() { async save() {
const created = await fs.writeNote( const editorState = this.editorView!.state;
if (!this.currentNote) {
return;
}
// Write to file system
const created = await this.fs.writeNote(
this.currentNote, this.currentNote,
this.view.state.sliceDoc() editorState.sliceDoc()
); );
this.dispatch({ type: "note-saved" });
// Update in open note cache
this.openNotes.set(
this.currentNote,
new NoteState(editorState, this.editorView!.scrollDOM.scrollTop)
);
// Dispatch update to view
this.viewDispatch({ type: "note-saved" });
// If a new note was created, let's refresh the note list // If a new note was created, let's refresh the note list
if (created) { if (created) {
await this.loadNoteList(); await this.loadNoteList();
@ -236,153 +261,146 @@ class Editor {
} }
async loadNoteList() { async loadNoteList() {
let notesMeta = await fs.listNotes(); let notesMeta = await this.fs.listNotes();
this.allNotes = notesMeta; this.viewDispatch({
this.dispatch({
type: "notes-listed", type: "notes-listed",
notes: notesMeta, notes: notesMeta,
}); });
} }
focus() { focus() {
this.view.focus(); this.editorView!.focus();
} }
navigate(name: string) { async navigate(name: string) {
location.hash = encodeURIComponent(name); location.hash = encodeURIComponent(name);
} }
}
let editor: Editor | null; hashChange() {
Promise.resolve()
.then(async () => {
await this.save();
const noteName = decodeURIComponent(location.hash.substring(1));
console.log("Now navigating to", noteName);
function NavigationBar({ if (!this.editorView) {
currentNote, return;
onClick, }
}: {
currentNote: string;
onClick: () => void;
}) {
return (
<div id="top">
<div className="current-note" onClick={onClick}>
» {currentNote}
</div>
</div>
);
}
function StatusBar({ isSaved }: { isSaved: boolean }) { let noteState = this.openNotes.get(noteName);
let wordCount = 0, if (!noteState) {
readingTime = 0; let text = await this.fs.readNote(noteName);
if (editor) { noteState = new NoteState(this.createEditorState(text), 0);
let text = editor.view.state.sliceDoc(); }
wordCount = util.countWords(text); this.openNotes.set(noteName, noteState!);
readingTime = util.readingTime(wordCount); this.editorView!.setState(noteState!.editorState);
} this.editorView.scrollDOM.scrollTop = noteState!.scrollTop;
return (
<div id="bottom">
{wordCount} words | {readingTime} min | {isSaved ? "Saved" : "Edited"}
</div>
);
}
function AppView() { this.viewDispatch({
const editorRef = useRef<HTMLDivElement>(null); type: "note-loaded",
const [appState, dispatch] = useReducer(reducer, initialViewState); name: noteName,
useEffect(() => {
editor = new Editor(editorRef.current!, "", "", dispatch);
editor.focus();
// @ts-ignore
window.editor = editor;
if (!location.hash) {
editor.navigate("start");
}
}, []);
useEffect(() => {
editor?.loadNoteList();
}, []);
// Auto save
useEffect(() => {
const id = setTimeout(() => {
if (!appState.isSaved) {
editor?.save();
}
}, 2000);
return () => {
clearTimeout(id);
};
}, [appState.isSaved]);
useEffect(() => {
function hashChange() {
const noteName = decodeURIComponent(location.hash.substring(1));
console.log("Now navigating to", noteName);
fs.readNote(noteName)
.then((text) => {
editor!.load(noteName, text);
dispatch({
type: "note-loaded",
name: noteName,
});
})
.catch((e) => {
console.error("Error loading note", e);
}); });
} })
hashChange(); .catch((e) => {
window.addEventListener("hashchange", hashChange); console.error(e);
return () => { });
window.removeEventListener("hashchange", hashChange); }
};
}, []);
return ( addListeners() {
<> this.$hashChange = this.hashChange.bind(this);
{appState.showNoteNavigator && ( window.addEventListener("hashchange", this.$hashChange);
<NoteNavigator }
allNotes={appState.allNotes}
onNavigate={(note) => { dispose() {
dispatch({ type: "stop-navigate" }); if (this.$hashChange) {
editor!.focus(); window.removeEventListener("hashchange", this.$hashChange);
if (note) { }
editor }
?.save()
.then(() => { ViewComponent(): React.ReactElement {
editor!.navigate(note); const [viewState, dispatch] = useReducer(reducer, initialViewState);
}) this.viewState = viewState;
.catch((e) => { this.viewDispatch = dispatch;
alert("Could not save note, not switching");
}); useEffect(() => {
} if (!location.hash) {
this.navigate("start");
}
}, []);
// Auto save
useEffect(() => {
const id = setTimeout(() => {
if (!viewState.isSaved) {
this.save();
}
}, 2000);
return () => {
clearTimeout(id);
};
}, [viewState.isSaved]);
let editor = this;
useEffect(() => {}, []);
return (
<>
{viewState.showNoteNavigator && (
<NoteNavigator
allNotes={viewState.allNotes}
onNavigate={(note) => {
dispatch({ type: "stop-navigate" });
editor!.focus();
if (note) {
editor
?.save()
.then(() => {
editor!.navigate(note);
})
.catch((e) => {
alert("Could not save note, not switching");
});
}
}}
/>
)}
{viewState.showCommandPalette && (
<CommandPalette
onTrigger={(cmd) => {
dispatch({ type: "hide-palette" });
editor!.focus();
if (cmd) {
console.log("Run", cmd);
}
}}
commands={[{ name: "My command", run: () => {} }]}
/>
)}
<NavigationBar
currentNote={viewState.currentNote}
onClick={() => {
dispatch({ type: "start-navigate" });
}} }}
/> />
)} <div id="editor"></div>
{appState.showCommandPalette && ( <StatusBar isSaved={viewState.isSaved} editorView={this.editorView} />
<CommandPalette </>
onTrigger={(cmd) => { );
dispatch({ type: "hide-palette" }); }
editor!.focus();
if (cmd) { render(container: ReactDOM.Container) {
console.log("Run", cmd); const ViewComponent = this.ViewComponent.bind(this);
} ReactDOM.render(<ViewComponent />, container);
}} }
commands={[{ name: "My command", run: () => {} }]}
/>
)}
<NavigationBar
currentNote={appState.currentNote}
onClick={() => {
dispatch({ type: "start-navigate" });
}}
/>
<div id="editor" ref={editorRef}></div>
<StatusBar isSaved={appState.isSaved} />
</>
);
} }
ReactDOM.render(<AppView />, document.getElementById("root")); let ed = new Editor(
new HttpFileSystem("http://localhost:2222/fs"),
document.getElementById("root")!
);
ed.focus();
// @ts-ignore
window.editor = ed;

View File

@ -0,0 +1,15 @@
export function NavigationBar({
currentNote,
onClick,
}: {
currentNote?: string;
onClick: () => void;
}) {
return (
<div id="top">
<div className="current-note" onClick={onClick}>
» {currentNote}
</div>
</div>
);
}

View File

@ -0,0 +1,23 @@
import { EditorView } from "@codemirror/view";
import * as util from "../util";
export function StatusBar({
isSaved,
editorView,
}: {
isSaved: boolean;
editorView?: EditorView;
}) {
let wordCount = 0,
readingTime = 0;
if (editorView) {
let text = editorView.state.sliceDoc();
wordCount = util.countWords(text);
readingTime = util.readingTime(wordCount);
}
return (
<div id="bottom">
{wordCount} words | {readingTime} min | {isSaved ? "Saved" : "Edited"}
</div>
);
}

2
webapp/src/index.ts Normal file
View File

@ -0,0 +1,2 @@

View File

@ -1,4 +1,3 @@
export type NoteMeta = { export type NoteMeta = {
name: string; name: string;
}; };
@ -6,10 +5,10 @@ export type NoteMeta = {
export type AppCommand = { export type AppCommand = {
name: string; name: string;
run: () => void; run: () => void;
} };
export type AppViewState = { export type AppViewState = {
currentNote: string; currentNote?: string;
isSaved: boolean; isSaved: boolean;
showNoteNavigator: boolean; showNoteNavigator: boolean;
showCommandPalette: boolean; showCommandPalette: boolean;
@ -24,6 +23,4 @@ export type Action =
| { type: "start-navigate" } | { type: "start-navigate" }
| { type: "stop-navigate" } | { type: "stop-navigate" }
| { type: "show-palette" } | { type: "show-palette" }
| { type: "hide-palette" } | { type: "hide-palette" };
;