feat: further simplify editor logic

This commit is contained in:
thecodrr
2022-06-29 16:38:24 +05:00
parent 03b8e56f6c
commit 2efd635bb0
9 changed files with 148 additions and 134 deletions

View File

@@ -1,5 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import TitleBox from "./title-box";
import { useStore } from "../../stores/editor-store";
import { Input } from "@rebass/forms";
import * as Icon from "../icons";
@@ -18,8 +17,6 @@ function Header({ readonly }) {
return (
<>
<TitleBox readonly={readonly} />
{!readonly && id && (
<Flex alignItems="center" flexWrap="wrap" sx={{ lineHeight: 2.5 }}>
{tags?.map((tag) => (

View File

@@ -22,6 +22,7 @@ import { downloadAttachment } from "../../common/attachments";
import { EV, EVENTS } from "notes-core/common";
import { db } from "../../common/db";
import useMobile from "../../utils/use-mobile";
import Titlebox from "./title-box";
function updateWordCount(counter?: CharacterCounter) {
AppEventManager.publish(
@@ -38,8 +39,16 @@ function onEditorChange(noteId: string, sessionId: string, content: string) {
data: content,
});
}
function onTitleChange(noteId: string, title: string) {
if (!title) return;
editorstore.get().setTitle(noteId, title);
}
const debouncedUpdateWordCount = debounce(updateWordCount, 1000);
const debouncedOnEditorChange = debounceWithId(onEditorChange, 100);
const debouncedOnTitleChange = debounceWithId(onTitleChange, 100);
export default function EditorManager({
noteId,
@@ -52,6 +61,7 @@ export default function EditorManager({
const isOldSession = !nonce && !!noteId;
const [content, setContent] = useState<string>("");
const [title, setTitle] = useState<string>("");
const arePropertiesVisible = useStore((store) => store.arePropertiesVisible);
const toggleProperties = useStore((store) => store.toggleProperties);
const isPreviewMode = useStore(
@@ -61,7 +71,6 @@ export default function EditorManager({
(store) => store.session.readonly || isPreviewMode
);
const [dropRef, overlayRef] = useDragOverlay();
const editor = useEditorInstance();
// TODO move this somewhere more appropriate
// const init = useStore((store) => store.init);
@@ -70,6 +79,7 @@ export default function EditorManager({
(async function () {
await editorstore.newSession(nonce);
setContent("");
setTitle("");
})();
}, [isNewSession, nonce]);
@@ -87,13 +97,13 @@ export default function EditorManager({
if (!isOldSession) return;
(async function () {
await editorstore.openSession(noteId);
await editorstore.get().openSession(noteId);
let content = await editorstore.get().getSessionContent();
const { getSessionContent, session } = editorstore.get();
const content = await getSessionContent();
setTitle(session.title);
setContent(content?.data);
// editorstore.set(
// (state: any) => (state.session.state = SESSION_STATES.stale)
// );
if (noteId && content) await db.attachments?.downloadImages(noteId);
})();
}, [noteId, isOldSession]);
@@ -112,60 +122,26 @@ export default function EditorManager({
>
{isPreviewMode && <PreviewModeNotice />}
<Editor
title={title}
content={content}
readonly={isReadonly}
onRequestFocus={() => toggleProperties(false)}
/>
{arePropertiesVisible && <Properties />}
<Box
ref={overlayRef}
id="drag-overlay"
sx={{
position: "absolute",
width: "100%",
height: "100%",
bg: "overlay",
zIndex: 3,
alignItems: "center",
justifyContent: "center",
display: "none",
}}
onDrop={async (e) => {
if (!editor) return;
for (let file of e.dataTransfer.files) {
const result = await attachFile(file);
if (!result) continue;
editor.attachFile(result);
}
}}
>
<Flex
sx={{
border: "2px dashed var(--fontTertiary)",
borderRadius: "default",
p: 70,
flexDirection: "column",
pointerEvents: "none",
}}
>
<Attachment size={72} />
<Text variant={"heading"} sx={{ color: "icon", mt: 2 }}>
Drop your files here to attach
</Text>
</Flex>
</Box>
<DropZone overlayRef={overlayRef} />
</Flex>
);
}
type EditorProps = {
title: string;
readonly?: boolean;
focusMode?: boolean;
content: string;
onRequestFocus?: () => void;
};
function Editor({ content, readonly, focusMode, onRequestFocus }: EditorProps) {
function Editor(props: EditorProps) {
const { content, readonly, focusMode, onRequestFocus, title } = props;
const editor = useEditorInstance();
const isMobile = useMobile();
@@ -238,6 +214,14 @@ function Editor({ content, readonly, focusMode, onRequestFocus }: EditorProps) {
}}
/>
)}
<Titlebox
readonly={readonly || false}
setTitle={(title) => {
const { sessionId, id } = editorstore.get().session;
debouncedOnTitleChange(sessionId, id, title);
}}
title={title}
/>
<Header readonly={readonly} />
<Tiptap
readonly={readonly}
@@ -329,6 +313,54 @@ function PreviewModeNotice() {
);
}
type DropZoneProps = {
overlayRef: React.MutableRefObject<HTMLElement | undefined>;
};
function DropZone(props: DropZoneProps) {
const { overlayRef } = props;
const editor = useEditorInstance();
return (
<Box
ref={overlayRef}
id="drag-overlay"
sx={{
position: "absolute",
width: "100%",
height: "100%",
bg: "overlay",
zIndex: 3,
alignItems: "center",
justifyContent: "center",
display: "none",
}}
onDrop={async (e) => {
if (!editor) return;
for (let file of e.dataTransfer.files) {
const result = await attachFile(file);
if (!result) continue;
editor.attachFile(result);
}
}}
>
<Flex
sx={{
border: "2px dashed var(--fontTertiary)",
borderRadius: "default",
p: 70,
flexDirection: "column",
pointerEvents: "none",
}}
>
<Attachment size={72} />
<Text variant={"heading"} sx={{ color: "icon", mt: 2 }}>
Drop your files here to attach
</Text>
</Flex>
</Box>
);
}
function useDragOverlay() {
const dropElementRef = useRef<HTMLElement>();
const overlayRef = useRef<HTMLElement>();

View File

@@ -3,7 +3,7 @@ import { useTheme } from "emotion-theming";
import { Toolbar, useTiptap, PortalProvider, Editor } from "notesnook-editor";
import { Box, Flex } from "rebass";
import "notesnook-editor/dist/styles.css";
import { PropsWithChildren, useEffect, useRef } from "react";
import { PropsWithChildren, useEffect, useRef, useState } from "react";
import useMobile from "../../utils/use-mobile";
import { Attachment } from "./plugins/picker";
import { CharacterCounter, IEditor } from "./types";
@@ -13,6 +13,7 @@ import { AttachmentType } from "notesnook-editor/dist/extensions/attachment";
import { getCurrentPreset } from "../../common/toolbar-config";
type TipTapProps = {
editorContainer: HTMLElement;
onChange?: (content: string, counter?: CharacterCounter) => void;
onInsertAttachment?: (type: AttachmentType) => void;
onDownloadAttachment?: (attachment: Attachment) => void;
@@ -30,10 +31,10 @@ function TipTap(props: TipTapProps) {
onFocus = () => {},
content,
toolbarContainerId,
editorContainer,
readonly,
} = props;
const editorContentRef = useRef<HTMLDivElement>();
const theme: Theme = useTheme();
const isMobile = useMobile();
const counter = useRef<CharacterCounter>();
@@ -43,7 +44,7 @@ function TipTap(props: TipTapProps) {
const editor = useTiptap(
{
element: editorContentRef.current,
element: editorContainer,
editable: !readonly,
content,
autofocus: "start",
@@ -84,7 +85,7 @@ function TipTap(props: TipTapProps) {
return true;
},
},
[content, readonly, theme]
[content, readonly]
);
useEffect(() => {
@@ -98,7 +99,7 @@ function TipTap(props: TipTapProps) {
}, [toggleSearch, editor?.storage.searchreplace?.isSearching]);
return (
<Flex sx={{ flex: 1, flexDirection: "column" }}>
<>
<Portal containerId={toolbarContainerId}>
<Toolbar
editor={editor}
@@ -108,19 +109,38 @@ function TipTap(props: TipTapProps) {
tools={toolbarConfig}
/>
</Portal>
<Box
className="selectable"
ref={editorContentRef}
style={{
flex: 1,
cursor: "text",
color: theme.colors.text,
}}
/>
</Flex>
</>
);
}
function TiptapWrapper(props: Omit<TipTapProps, "editorContainer">) {
const [isReady, setIsReady] = useState(false);
const editorContainerRef = useRef<HTMLDivElement>();
useEffect(() => {
setIsReady(true);
}, []);
return (
<PortalProvider>
<Flex sx={{ flex: 1, flexDirection: "column" }}>
{isReady && editorContainerRef.current ? (
<TipTap {...props} editorContainer={editorContainerRef.current} />
) : null}
<Box
ref={editorContainerRef}
className="selectable"
style={{
flex: 1,
cursor: "text",
color: "var(--text)", // TODO!
}}
/>
</Flex>
</PortalProvider>
);
}
export default TiptapWrapper;
function Portal(props: PropsWithChildren<{ containerId?: string }>) {
const { containerId, children } = props;
const container = containerId && document.getElementById(containerId);
@@ -131,15 +151,6 @@ function Portal(props: PropsWithChildren<{ containerId?: string }>) {
);
}
function TiptapProvider(props: TipTapProps) {
return (
<PortalProvider>
<TipTap {...props} />
</PortalProvider>
);
}
export default TiptapProvider;
function toIEditor(editor: Editor): IEditor {
return {
focus: () => editor.commands.focus("start"),

View File

@@ -1,36 +1,20 @@
import React, { useEffect, useRef, useState } from "react";
import React, { useEffect, useState } from "react";
import { Input } from "@rebass/forms";
import { useStore, store, SESSION_STATES } from "../../stores/editor-store";
type TitleBoxProps = {
readonly: boolean;
title: string;
setTitle: (title: string) => void;
};
function TitleBox(props: TitleBoxProps) {
const { readonly } = props;
const state = useStore((store) => store.session.state);
const sessionId = useStore((store) => store.session.id);
const title = useStore((store) => store.session.title);
const setTitle = useStore((store) => store.setTitle);
const { readonly, setTitle, title } = props;
const [currentTitle, setCurrentTitle] = useState<string>();
const [placeholder, setPlaceholder] = useState<string>();
useEffect(() => {
const noteTitle = store.get().session.title;
if (state === SESSION_STATES.new && noteTitle !== currentTitle) {
setCurrentTitle("");
setPlaceholder("");
} else if (state === SESSION_STATES.stale) {
setCurrentTitle(noteTitle);
}
// We do not want to update when currentTitle changes.
}, [state, sessionId]);
useEffect(() => {
if (currentTitle !== title) setPlaceholder(title);
// if (currentTitle !== title) setPlaceholder(title);
// We do not want to update when currentTitle changes.
setCurrentTitle(title);
}, [title]);
return (
@@ -39,7 +23,7 @@ function TitleBox(props: TitleBoxProps) {
variant="clean"
data-test-id="editor-title"
className="editorTitle"
placeholder={placeholder || "Note title"}
placeholder={"Note title"}
width="100%"
readOnly={readonly}
sx={{
@@ -50,12 +34,16 @@ function TitleBox(props: TitleBoxProps) {
}}
onChange={(e) => {
setCurrentTitle(e.target.value);
setTitle(sessionId, e.target.value);
setTitle(e.target.value);
}}
/>
);
}
export default React.memo(TitleBox, (prevProps, nextProps) => {
return prevProps.readonly === nextProps.readonly;
return (
prevProps.readonly === nextProps.readonly &&
prevProps.title === nextProps.title &&
prevProps.setTitle === nextProps.setTitle
);
});

View File

@@ -35,26 +35,10 @@ export function hashNavigate(
if (addNonce) url += `/${++last}`;
window.history[`${replace ? "replace" : "push"}State`](null, null, `#${url}`);
if (notify) dispatchEvent(new HashChangeEvent("hashchange"));
// if (typeof url !== "string") {
// throw new Error(`"url" must be a string, was provided a(n) ${typeof url}`);
// }
// if (Array.isArray(replaceOrQuery)) {
// throw new Error(
// '"replaceOrQuery" must be boolean, object, or URLSearchParams'
// );
// }
// if (replaceOrQuery !== null && typeof replaceOrQuery === "object") {
// url += "?" + new URLSearchParams(replaceOrQuery).toString();
// } else if (replace === undefined && replaceOrQuery !== undefined) {
// replace = replaceOrQuery;
// } else if (replace === undefined && replaceOrQuery === undefined) {
// replace = false;
// }
// window.history[`${replace ? "replace" : "push"}State`](null, null, url);
// dispatchEvent(new PopStateEvent("popstate", null));
const event = new HashChangeEvent("hashchange");
event.notify = notify;
dispatchEvent(event);
}
export function useQueryParams(parseFn = parseQuery) {

View File

@@ -237,12 +237,12 @@ class EditorStore extends BaseStore {
appStore.setIsEditorOpen(false);
};
setTitle = (sessionId, title) => {
return this.saveSession(sessionId, { title });
setTitle = (noteId, title) => {
return this.saveSession(noteId, { title });
};
toggle = (sessionId, name, value) => {
return this.saveSession(sessionId, { [name]: value });
toggle = (noteId, name, value) => {
return this.saveSession(noteId, { [name]: value });
};
saveSessionContent = (noteId, sessionId, content) => {

View File

@@ -5,7 +5,7 @@ import { hashNavigate } from "../navigation";
// (excluding the leading '#' symbol)
const currentLocation = () => {
const location = window.location.hash.replace(/^#/, "") || "/";
let end = location.indexOf("?");
let end: number | undefined = location.indexOf("?");
if (end <= -1) end = undefined;
return location.substring(0, end);
};
@@ -17,14 +17,22 @@ const currentQuery = () => {
);
};
type HashLocation = { location: string; update: boolean };
export default function useHashLocation() {
const [loc, setLoc] = useState(currentLocation());
const [loc, setLoc] = useState<HashLocation>({
location: currentLocation(),
update: true,
});
const [queryParams, setQueryParams] = useState(currentQuery());
useEffect(() => {
// this function is called whenever the hash changes
const handler = () => {
setLoc(currentLocation());
const handler = (e: HashChangeEvent) => {
const notify = (e as any).notify === undefined ? true : (e as any).notify;
setLoc({
location: currentLocation(),
update: notify,
});
setQueryParams(currentQuery());
};
@@ -32,5 +40,5 @@ export default function useHashLocation() {
window.addEventListener("hashchange", handler);
return () => window.removeEventListener("hashchange", handler);
}, []);
return [loc, queryParams, hashNavigate];
return [loc, queryParams, hashNavigate] as const;
}

View File

@@ -3,7 +3,9 @@ import useHashLocation from "./use-hash-location";
var lastRoute = null;
export default function useHashRoutes(routes) {
const [location] = useHashLocation();
const [{ location, update }] = useHashLocation();
if (!update) return lastRoute;
const matcher = makeMatcher();
for (var key in routes) {
const [match, params] = matcher(key, location);

View File

@@ -1,8 +0,0 @@
import makeMatcher from "wouter/matcher";
import useHashLocation from "./use-hash-location";
export default function useHashRoute(pattern) {
const [location] = useHashLocation();
const matcher = makeMatcher();
return matcher(pattern, location);
}