diff --git a/components/container/edit-container.tsx b/components/container/edit-container.tsx index 8e59a4af8..63323dafb 100644 --- a/components/container/edit-container.tsx +++ b/components/container/edit-container.tsx @@ -1,7 +1,7 @@ import NoteState from 'libs/web/state/note' import { has } from 'lodash' import router, { useRouter } from 'next/router' -import { useCallback, useEffect } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import NoteTreeState from 'libs/web/state/tree' import NoteNav from 'components/note-nav' import UIState from 'libs/web/state/ui' @@ -10,6 +10,7 @@ import useSettingsAPI from 'libs/web/api/settings' import dynamic from 'next/dynamic' import { useToast } from 'libs/web/hooks/use-toast' import DeleteAlert from 'components/editor/delete-alert' +import useRouterWarning from 'libs/web/state/ui/useRouterWarning' const MainEditor = dynamic(() => import('components/editor/main-editor')) @@ -27,11 +28,13 @@ export const EditContainer = () => { note, } = NoteState.useContainer() const { query } = useRouter() + const [isSaved, setSaved] = useState(true) const pid = query.pid as string const id = query.id as string const isNew = has(query, 'new') const { mutate: mutateSettings } = useSettingsAPI() const toast = useToast() + const saveRef = useRef<() => void>() const loadNoteById = useCallback( async (id: string) => { @@ -94,18 +97,32 @@ export const EditContainer = () => { useEffect(() => { abortFindNote() loadNoteById(id) + setSaved(true) }, [loadNoteById, abortFindNote, id]) useEffect(() => { - updateTitle(note?.title) - }, [note?.title, updateTitle]) + updateTitle(`${!isSaved && settings.explicitSave ? '*' : ''}${note?.title}`) + }, [isSaved, note?.title, settings.explicitSave, updateTitle]) + + useRouterWarning(!isSaved && settings.explicitSave, () => { + return confirm('Warning! You have unsaved changes.') + }) return ( <> - +
- +
) diff --git a/components/editor/editor.tsx b/components/editor/editor.tsx index 5fa5733ec..57a9ce755 100644 --- a/components/editor/editor.tsx +++ b/components/editor/editor.tsx @@ -1,4 +1,4 @@ -import { FC, useEffect, useState } from 'react' +import { FC, MutableRefObject, useEffect, useState } from 'react' import { use100vh } from 'react-div-100vh' import MarkdownEditor, { Props } from 'rich-markdown-editor' import { useEditorTheme } from './theme' @@ -12,9 +12,18 @@ import { useEmbeds } from './embeds' export interface EditorProps extends Pick { isPreview?: boolean + explicitSave?: boolean + saveState?: (state: boolean) => void + saveRef?: MutableRefObject<(() => void) | undefined> } -const Editor: FC = ({ readOnly, isPreview }) => { +const Editor: FC = ({ + readOnly, + isPreview, + explicitSave, + saveState, + saveRef, +}) => { const { onSearchLink, onCreateLink, @@ -39,6 +48,16 @@ const Editor: FC = ({ readOnly, isPreview }) => { setHasMinHeight((backlinks?.length ?? 0) <= 0) }, [backlinks, isPreview]) + useEffect(() => { + const handleKeyDown = () => { + if (editorEl.current?.value) { + onEditorChange(editorEl.current?.value) + saveState && saveState(true) + } + } + if (saveRef) (saveRef as MutableRefObject<() => void>).current = handleKeyDown + }, [saveState, saveRef, editorEl, onEditorChange]) + return ( <> = ({ readOnly, isPreview }) => { id={note?.id} ref={editorEl} value={mounted ? note?.content : ''} - onChange={onEditorChange} + onChange={ + explicitSave ? () => saveState && saveState(false) : onEditorChange + } + onSave={explicitSave ? () => (saveRef as MutableRefObject<() => void>)?.current?.() : undefined} placeholder={dictionary.editorPlaceholder} theme={editorTheme} uploadImage={(file) => onUploadImage(file, note?.id)} diff --git a/components/icon-button.tsx b/components/icon-button.tsx index 48ebfc48a..ee73d0599 100644 --- a/components/icon-button.tsx +++ b/components/icon-button.tsx @@ -18,7 +18,8 @@ import { ExternalLinkIcon, BookmarkAltIcon, PuzzleIcon, - ChevronDoubleUpIcon + ChevronDoubleUpIcon, + SaveIcon, } from '@heroicons/react/outline' export const ICONS = { @@ -40,6 +41,7 @@ export const ICONS = { BookmarkAlt: BookmarkAltIcon, Puzzle: PuzzleIcon, ChevronDoubleUp: ChevronDoubleUpIcon, + Save: SaveIcon, } const IconButton = forwardRef< diff --git a/components/note-nav.tsx b/components/note-nav.tsx index bfcd884ef..e79e4dc80 100644 --- a/components/note-nav.tsx +++ b/components/note-nav.tsx @@ -1,7 +1,7 @@ import classNames from 'classnames' import NoteState from 'libs/web/state/note' import UIState from 'libs/web/state/ui' -import { useCallback, MouseEvent } from 'react' +import { useCallback, MouseEvent, MutableRefObject } from 'react' import { CircularProgress, Tooltip } from '@material-ui/core' import NoteTreeState from 'libs/web/state/tree' import { Breadcrumbs } from '@material-ui/core' @@ -33,8 +33,13 @@ const MenuButton = () => { > ) } +type Props = { + explicitSave?: boolean + isSaved?: boolean + saveRef?: MutableRefObject<(() => void) | undefined> +} -const NoteNav = () => { +const NoteNav = ({ explicitSave, isSaved, saveRef }: Props) => { const { t } = useI18n() const { note, loading } = NoteState.useContainer() const { ua } = UIState.useContainer() @@ -61,9 +66,11 @@ const NoteNav = () => { const handleClickOpenInTree = useCallback(() => { if (!note) return - showItem(note); + showItem(note) }, [note, showItem]) + const saveNote = useCallback(() => saveRef?.current?.(), [saveRef]) + return (