From 2efd635bb094fd7e20131cf42872de6fec496216 Mon Sep 17 00:00:00 2001 From: thecodrr Date: Wed, 29 Jun 2022 16:38:24 +0500 Subject: [PATCH] feat: further simplify editor logic --- apps/web/src/components/editor/header.js | 3 - apps/web/src/components/editor/index.tsx | 122 +++++++++++------- apps/web/src/components/editor/tiptap.tsx | 59 +++++---- apps/web/src/components/editor/title-box.tsx | 38 ++---- apps/web/src/navigation/index.js | 22 +--- apps/web/src/stores/editor-store.js | 8 +- ...seHashLocation.js => use-hash-location.ts} | 18 ++- apps/web/src/utils/use-hash-routes.js | 4 +- apps/web/src/utils/useHashRoute.js | 8 -- 9 files changed, 148 insertions(+), 134 deletions(-) rename apps/web/src/utils/{useHashLocation.js => use-hash-location.ts} (66%) delete mode 100644 apps/web/src/utils/useHashRoute.js diff --git a/apps/web/src/components/editor/header.js b/apps/web/src/components/editor/header.js index a9be1bce5..bb4099460 100644 --- a/apps/web/src/components/editor/header.js +++ b/apps/web/src/components/editor/header.js @@ -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 ( <> - - {!readonly && id && ( {tags?.map((tag) => ( diff --git a/apps/web/src/components/editor/index.tsx b/apps/web/src/components/editor/index.tsx index 92530cfaa..ebd59da4d 100644 --- a/apps/web/src/components/editor/index.tsx +++ b/apps/web/src/components/editor/index.tsx @@ -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(""); + const [title, setTitle] = useState(""); 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 && } toggleProperties(false)} /> {arePropertiesVisible && } - - { - if (!editor) return; - for (let file of e.dataTransfer.files) { - const result = await attachFile(file); - if (!result) continue; - editor.attachFile(result); - } - }} - > - - - - Drop your files here to attach - - - + ); } 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) { }} /> )} + { + const { sessionId, id } = editorstore.get().session; + debouncedOnTitleChange(sessionId, id, title); + }} + title={title} + />
; +}; +function DropZone(props: DropZoneProps) { + const { overlayRef } = props; + const editor = useEditorInstance(); + + return ( + { + if (!editor) return; + for (let file of e.dataTransfer.files) { + const result = await attachFile(file); + if (!result) continue; + editor.attachFile(result); + } + }} + > + + + + Drop your files here to attach + + + + ); +} + function useDragOverlay() { const dropElementRef = useRef(); const overlayRef = useRef(); diff --git a/apps/web/src/components/editor/tiptap.tsx b/apps/web/src/components/editor/tiptap.tsx index ccfca5506..aad46d83c 100644 --- a/apps/web/src/components/editor/tiptap.tsx +++ b/apps/web/src/components/editor/tiptap.tsx @@ -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(); const theme: Theme = useTheme(); const isMobile = useMobile(); const counter = useRef(); @@ -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 ( - + <> - - + ); } +function TiptapWrapper(props: Omit) { + const [isReady, setIsReady] = useState(false); + const editorContainerRef = useRef(); + useEffect(() => { + setIsReady(true); + }, []); + + return ( + + + {isReady && editorContainerRef.current ? ( + + ) : null} + + + + ); +} +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 ( - - - - ); -} -export default TiptapProvider; - function toIEditor(editor: Editor): IEditor { return { focus: () => editor.commands.focus("start"), diff --git a/apps/web/src/components/editor/title-box.tsx b/apps/web/src/components/editor/title-box.tsx index 1a10738f7..eed73d02a 100644 --- a/apps/web/src/components/editor/title-box.tsx +++ b/apps/web/src/components/editor/title-box.tsx @@ -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(); - const [placeholder, setPlaceholder] = useState(); 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 + ); }); diff --git a/apps/web/src/navigation/index.js b/apps/web/src/navigation/index.js index 92969c721..cc12af741 100644 --- a/apps/web/src/navigation/index.js +++ b/apps/web/src/navigation/index.js @@ -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) { diff --git a/apps/web/src/stores/editor-store.js b/apps/web/src/stores/editor-store.js index bfe4c6d74..d547c9579 100644 --- a/apps/web/src/stores/editor-store.js +++ b/apps/web/src/stores/editor-store.js @@ -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) => { diff --git a/apps/web/src/utils/useHashLocation.js b/apps/web/src/utils/use-hash-location.ts similarity index 66% rename from apps/web/src/utils/useHashLocation.js rename to apps/web/src/utils/use-hash-location.ts index 5de311772..98554d924 100644 --- a/apps/web/src/utils/useHashLocation.js +++ b/apps/web/src/utils/use-hash-location.ts @@ -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({ + 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; } diff --git a/apps/web/src/utils/use-hash-routes.js b/apps/web/src/utils/use-hash-routes.js index b87180cd7..6dae05b01 100644 --- a/apps/web/src/utils/use-hash-routes.js +++ b/apps/web/src/utils/use-hash-routes.js @@ -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); diff --git a/apps/web/src/utils/useHashRoute.js b/apps/web/src/utils/useHashRoute.js deleted file mode 100644 index cc5830dd3..000000000 --- a/apps/web/src/utils/useHashRoute.js +++ /dev/null @@ -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); -}