From 00e6c806edba1ec4076e97ec738d5f2cc18a4f7f Mon Sep 17 00:00:00 2001 From: monodyle Date: Wed, 1 Sep 2021 21:26:09 +0700 Subject: [PATCH 1/6] feat: drop file to open --- src/app/app.tsx | 4 +++- src/app/state/file-drop.ts | 43 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 src/app/state/file-drop.ts diff --git a/src/app/app.tsx b/src/app/app.tsx index 2a0c856..17fcee8 100644 --- a/src/app/app.tsx +++ b/src/app/app.tsx @@ -7,6 +7,7 @@ import { usePrefs } from "~src/components/prefs/state"; import s from "./app.module.css"; import { useEditorTheme } from "./state/editor-theme"; import { useFileDirty } from "./state/file-dirty"; +import { useFileDrop } from "./state/file-drop"; import { useFileLoad } from "./state/file-load"; import { useToolbarAutohide } from "./state/toolbar-autohide"; import { AppTitle } from "./title"; @@ -21,9 +22,10 @@ export const App = () => { useFileLoad({ editor, file }); const toolbar = useToolbarAutohide({ editor }); useEditorTheme({ editor, prefs }); + const dropRef = useFileDrop({ file }); return ( -
+
=> { + const dropRef = useRef(null); + + const handleDragOver = (e: DragEvent) => e.preventDefault(); + const handleDrop = async (e: DragEvent) => { + e.preventDefault(); + if (!e.dataTransfer) return; + const { items } = e.dataTransfer; + if (1 < items.length) throw Error("Only one file can be upload at a time"); + if (Array.from(items).some((item) => item.kind !== "file")) + throw Error("Support upload single file only"); + if (items && items.length) { + const file = await items[0].getAsFileSystemHandle(); + if (!file) return; + try { + params.file.setHandle(file as FileSystemFileHandle); + params.file.setDirty(false); + } catch (e) { + throw Error(e); + } + } + }; + + useEffect(() => { + const { current } = dropRef; + if (!current) return; + current.addEventListener("dragover", handleDragOver); + current.addEventListener("drop", handleDrop); + return () => { + current.removeEventListener("dragover", handleDragOver); + current.removeEventListener("drop", handleDrop); + }; + }); + + return dropRef; +} \ No newline at end of file From 7e51c59502db88fa40e2eb88348e58dc4db6dbae Mon Sep 17 00:00:00 2001 From: monodyle Date: Wed, 1 Sep 2021 22:21:53 +0700 Subject: [PATCH 2/6] fix: request change issues --- src/app/state/file-drop.ts | 32 ++++++++++++++------------------ src/components/file/state.ts | 8 +++++++- src/components/toolbar/open.tsx | 3 +-- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/app/state/file-drop.ts b/src/app/state/file-drop.ts index d0eab8a..67c3670 100644 --- a/src/app/state/file-drop.ts +++ b/src/app/state/file-drop.ts @@ -5,39 +5,35 @@ interface Params { file: FileState; } -export const useFileDrop = (params: Params): React.RefObject => { +const handleDragOver = (e: DragEvent) => e.preventDefault(); + +export const useFileDrop = ( + params: Params +): React.RefObject => { const dropRef = useRef(null); - const handleDragOver = (e: DragEvent) => e.preventDefault(); const handleDrop = async (e: DragEvent) => { e.preventDefault(); if (!e.dataTransfer) return; + const { items } = e.dataTransfer; - if (1 < items.length) throw Error("Only one file can be upload at a time"); - if (Array.from(items).some((item) => item.kind !== "file")) - throw Error("Support upload single file only"); - if (items && items.length) { - const file = await items[0].getAsFileSystemHandle(); - if (!file) return; - try { - params.file.setHandle(file as FileSystemFileHandle); - params.file.setDirty(false); - } catch (e) { - throw Error(e); - } - } + if (1 < items.length || items[0].kind !== "file") + throw Error("Only a single file upload is supported."); + + const file = await items[0].getAsFileSystemHandle(); + if (file) params.file.setFile(file as FileSystemFileHandle); }; useEffect(() => { const { current } = dropRef; - if (!current) return; + if (!current) throw Error("Drop ref is null"); current.addEventListener("dragover", handleDragOver); current.addEventListener("drop", handleDrop); return () => { current.removeEventListener("dragover", handleDragOver); current.removeEventListener("drop", handleDrop); }; - }); + }, [dropRef]); return dropRef; -} \ No newline at end of file +}; diff --git a/src/components/file/state.ts b/src/components/file/state.ts index e327630..72002e2 100644 --- a/src/components/file/state.ts +++ b/src/components/file/state.ts @@ -9,6 +9,7 @@ export interface FileState { setHandle: SetState; dirty: boolean; setDirty: SetState; + setFile: (handle: FileHandle | null) => void; } const useSaveHandle = (handle: FileHandle | null): void => { @@ -23,5 +24,10 @@ export const useFile = (): FileState => { useSaveHandle(handle); - return { handle, setHandle, dirty, setDirty }; + const setFile = (handle: FileHandle | null): void => { + setHandle(handle); + setDirty(false); + }; + + return { handle, setHandle, dirty, setDirty, setFile }; }; diff --git a/src/components/toolbar/open.tsx b/src/components/toolbar/open.tsx index 9fdf184..a127e37 100644 --- a/src/components/toolbar/open.tsx +++ b/src/components/toolbar/open.tsx @@ -11,8 +11,7 @@ interface Props { } const setFile = async (props: Props, handle: FileHandle): Promise => { - props.file.setHandle(handle); - props.file.setDirty(false); + props.file.setFile(handle); }; const openFile = async (props: Props): Promise => { From 8498423fb3b05b0353373c78d66be4cae7a316e4 Mon Sep 17 00:00:00 2001 From: monodyle Date: Wed, 1 Sep 2021 23:15:47 +0700 Subject: [PATCH 3/6] wip: dragging state --- src/app/app.module.css | 14 +++++++ src/app/app.tsx | 9 ++++- src/app/state/file-drop.ts | 38 +++++++++++++++---- src/components/prefs/theme/styles/bushido.css | 1 + .../prefs/theme/styles/serika-dark.css | 1 + 5 files changed, 53 insertions(+), 10 deletions(-) diff --git a/src/app/app.module.css b/src/app/app.module.css index 22b5691..b7e2418 100644 --- a/src/app/app.module.css +++ b/src/app/app.module.css @@ -22,3 +22,17 @@ .toolbar.muted { pointer-events: none; } + +.dragging { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1000; + background-color: rgba(var(--sub-color-rgb), 0.5); + display: flex; + justify-content: center; + align-items: center; + pointer-events: none; +} diff --git a/src/app/app.tsx b/src/app/app.tsx index 17fcee8..661cdf6 100644 --- a/src/app/app.tsx +++ b/src/app/app.tsx @@ -22,10 +22,10 @@ export const App = () => { useFileLoad({ editor, file }); const toolbar = useToolbarAutohide({ editor }); useEditorTheme({ editor, prefs }); - const dropRef = useFileDrop({ file }); + const drop = useFileDrop({ file }); return ( -
+
{
+ {drop.dragging && ( +
+ drop file here +
+ )}
); }; diff --git a/src/app/state/file-drop.ts b/src/app/state/file-drop.ts index 67c3670..a379599 100644 --- a/src/app/state/file-drop.ts +++ b/src/app/state/file-drop.ts @@ -1,19 +1,24 @@ -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import { FileState } from "~src/components/file/state"; +interface AppDropState { + ref: React.RefObject; + dragging: boolean; +} + interface Params { file: FileState; } const handleDragOver = (e: DragEvent) => e.preventDefault(); -export const useFileDrop = ( - params: Params -): React.RefObject => { - const dropRef = useRef(null); +export const useFileDrop = (params: Params): AppDropState => { + const [dragging, setDragging] = useState(false); + const ref = useRef(null); const handleDrop = async (e: DragEvent) => { e.preventDefault(); + setDragging(false); if (!e.dataTransfer) return; const { items } = e.dataTransfer; @@ -24,16 +29,33 @@ export const useFileDrop = ( if (file) params.file.setFile(file as FileSystemFileHandle); }; + const handleDragEnter = (e: DragEvent) => { + e.preventDefault(); + if (e.dataTransfer?.items && e.dataTransfer.items.length > 0) + setDragging(true); + }; + + const handleDragLeave = (e: DragEvent) => { + e.preventDefault(); + setDragging(false); + }; + useEffect(() => { - const { current } = dropRef; + const { current } = ref; if (!current) throw Error("Drop ref is null"); + // Handle drag and drop events current.addEventListener("dragover", handleDragOver); current.addEventListener("drop", handleDrop); + // Handle drag in and out + current.addEventListener("dragenter", handleDragEnter); + current.addEventListener("dragleave", handleDragLeave); return () => { current.removeEventListener("dragover", handleDragOver); current.removeEventListener("drop", handleDrop); + current.removeEventListener("dragenter", handleDragEnter); + current.removeEventListener("dragleave", handleDragLeave); }; - }, [dropRef]); + }, [ref]); - return dropRef; + return { ref, dragging }; }; diff --git a/src/components/prefs/theme/styles/bushido.css b/src/components/prefs/theme/styles/bushido.css index 3ef9559..a882487 100644 --- a/src/components/prefs/theme/styles/bushido.css +++ b/src/components/prefs/theme/styles/bushido.css @@ -4,6 +4,7 @@ html.theme-bushido { --main-color: #ec4c56; --caret-color: #ec4c56; --sub-color: #596172; + --sub-color-rgb: 89, 97, 114; --text-color: #f6f0e9; --error-color: #ec4c56; } diff --git a/src/components/prefs/theme/styles/serika-dark.css b/src/components/prefs/theme/styles/serika-dark.css index fec96cb..a7a05ec 100644 --- a/src/components/prefs/theme/styles/serika-dark.css +++ b/src/components/prefs/theme/styles/serika-dark.css @@ -4,6 +4,7 @@ html.theme-serika-dark { --main-color: #e2b714; --caret-color: #e2b714; --sub-color: #646669; + --sub-color-rgb: 100, 102, 105; --text-color: #d1d0c5; --error-color: #ca4754; } From 6cba36550b2084d62c3f2515b4438728b5d70bb7 Mon Sep 17 00:00:00 2001 From: Thien Do Date: Thu, 2 Sep 2021 10:26:12 +0700 Subject: [PATCH 4/6] Refactor: Extract AppDrop indicator --- src/app/app.module.css | 14 -------------- src/app/drop/drop.module.css | 16 ++++++++++++++++ src/app/drop/drop.tsx | 7 +++++++ 3 files changed, 23 insertions(+), 14 deletions(-) create mode 100644 src/app/drop/drop.module.css create mode 100644 src/app/drop/drop.tsx diff --git a/src/app/app.module.css b/src/app/app.module.css index b7e2418..22b5691 100644 --- a/src/app/app.module.css +++ b/src/app/app.module.css @@ -22,17 +22,3 @@ .toolbar.muted { pointer-events: none; } - -.dragging { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 1000; - background-color: rgba(var(--sub-color-rgb), 0.5); - display: flex; - justify-content: center; - align-items: center; - pointer-events: none; -} diff --git a/src/app/drop/drop.module.css b/src/app/drop/drop.module.css new file mode 100644 index 0000000..d6f47e6 --- /dev/null +++ b/src/app/drop/drop.module.css @@ -0,0 +1,16 @@ +.container { + position: absolute; + z-index: 1000; + top: 0; + left: 0; + width: 100%; + height: 100%; + + background-color: rgba(var(--bg-color-rgb), 0.5); + backdrop-filter: blur(20px); + + display: grid; + place-items: center; + + pointer-events: none; +} diff --git a/src/app/drop/drop.tsx b/src/app/drop/drop.tsx new file mode 100644 index 0000000..8543822 --- /dev/null +++ b/src/app/drop/drop.tsx @@ -0,0 +1,7 @@ +import s from "./drop.module.css"; + +export const AppDrop = (): JSX.Element => ( +
+

Drop file here to open

+
+); From c8ac6ddbe02ee8bafa4819f87d67ec42f2195020 Mon Sep 17 00:00:00 2001 From: Thien Do Date: Thu, 2 Sep 2021 10:26:49 +0700 Subject: [PATCH 5/6] Refactor: Use React for event handler in AppDrop --- src/app/app.tsx | 14 ++-- src/app/drop/state.ts | 66 +++++++++++++++++++ src/app/state/file-drop.ts | 61 ----------------- src/components/file/state.ts | 6 +- src/components/prefs/theme/styles/bushido.css | 1 - .../prefs/theme/styles/serika-dark.css | 1 - 6 files changed, 75 insertions(+), 74 deletions(-) create mode 100644 src/app/drop/state.ts delete mode 100644 src/app/state/file-drop.ts diff --git a/src/app/app.tsx b/src/app/app.tsx index ed0eb21..38f8991 100644 --- a/src/app/app.tsx +++ b/src/app/app.tsx @@ -1,3 +1,4 @@ +import { useRef, useState } from "react"; import { useEditor } from "~/src/components/editor/state/state"; import { useFile } from "~/src/components/file/state"; import { Layout } from "~/src/components/layout/layout"; @@ -5,9 +6,10 @@ import { useLayout } from "~/src/components/layout/state"; import { Toolbar } from "~/src/components/toolbar/toolbar"; import { usePrefs } from "~src/components/prefs/state"; import s from "./app.module.css"; +import { AppDrop } from "./drop/drop"; +import { useAppDrop } from "./drop/state"; import { useEditorTheme } from "./state/editor-theme"; import { useFileDirty } from "./state/file-dirty"; -import { useFileDrop } from "./state/file-drop"; import { useFileLoad } from "./state/file-load"; import { useToolbarAutohide } from "./state/toolbar-autohide"; import { AppTitle } from "./title"; @@ -22,10 +24,10 @@ export const App = (): JSX.Element => { useFileLoad({ editor, file }); const toolbar = useToolbarAutohide({ editor }); useEditorTheme({ editor, prefs }); - const drop = useFileDrop({ file }); + const drop = useAppDrop({ file }); return ( -
+
{
- {drop.dragging && ( -
- drop file here -
- )} + {drop.dragging && }
); }; diff --git a/src/app/drop/state.ts b/src/app/drop/state.ts new file mode 100644 index 0000000..a1733a1 --- /dev/null +++ b/src/app/drop/state.ts @@ -0,0 +1,66 @@ +import * as React from "react"; +import { FileState } from "~src/components/file/state"; + +interface AppDropState { + dragging: boolean; + handlers: Pick< + React.DOMAttributes, + "onDrop" | "onDragOver" | "onDragEnter" | "onDragLeave" + >; +} + +interface Params { + file: FileState; +} + +export const useAppDrop = (params: Params): AppDropState => { + // Keep a counter to reliably detect when the drag is still there. We can + // not mute pointer-events on App's children (editor and toolbar). + // https://stackoverflow.com/a/21002544 + const counter = React.useRef(0); + // Since ref does not trigger re-render, we still need a state, but we will + // only set this state when counter is changed between 0 and 1 to avoid + // unnecessary re-renders + const [dragging, setDragging] = React.useState(false); + + const onDrop = async (event: React.DragEvent) => { + event.preventDefault(); + setDragging(false); + counter.current = 0; + + const { items } = event.dataTransfer; + if (1 < items.length || items[0].kind !== "file") + throw Error("Only a single file upload is supported."); + + const file = await items[0].getAsFileSystemHandle(); + if (file) params.file.setFile(file as FileSystemFileHandle); + }; + + const onDragOver = (event: React.DragEvent): void => { + // Specifying this as a drop target + // https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Drag_operations#droptargets + event.preventDefault(); + }; + + const onDragEnter = (event: React.DragEvent): void => { + // Specifying this as a drop target + // https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Drag_operations#droptargets + event.preventDefault(); + if (counter.current === 0) setDragging(true); + counter.current = counter.current + 1; + }; + + const onDragLeave = (_event: React.DragEvent): void => { + if (counter.current === 1) setDragging(false); + counter.current = counter.current - 1; + }; + + const handlers: AppDropState["handlers"] = { + onDrop, + onDragEnter, + onDragLeave, + onDragOver, + }; + + return { handlers, dragging }; +}; diff --git a/src/app/state/file-drop.ts b/src/app/state/file-drop.ts deleted file mode 100644 index a379599..0000000 --- a/src/app/state/file-drop.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import { FileState } from "~src/components/file/state"; - -interface AppDropState { - ref: React.RefObject; - dragging: boolean; -} - -interface Params { - file: FileState; -} - -const handleDragOver = (e: DragEvent) => e.preventDefault(); - -export const useFileDrop = (params: Params): AppDropState => { - const [dragging, setDragging] = useState(false); - const ref = useRef(null); - - const handleDrop = async (e: DragEvent) => { - e.preventDefault(); - setDragging(false); - if (!e.dataTransfer) return; - - const { items } = e.dataTransfer; - if (1 < items.length || items[0].kind !== "file") - throw Error("Only a single file upload is supported."); - - const file = await items[0].getAsFileSystemHandle(); - if (file) params.file.setFile(file as FileSystemFileHandle); - }; - - const handleDragEnter = (e: DragEvent) => { - e.preventDefault(); - if (e.dataTransfer?.items && e.dataTransfer.items.length > 0) - setDragging(true); - }; - - const handleDragLeave = (e: DragEvent) => { - e.preventDefault(); - setDragging(false); - }; - - useEffect(() => { - const { current } = ref; - if (!current) throw Error("Drop ref is null"); - // Handle drag and drop events - current.addEventListener("dragover", handleDragOver); - current.addEventListener("drop", handleDrop); - // Handle drag in and out - current.addEventListener("dragenter", handleDragEnter); - current.addEventListener("dragleave", handleDragLeave); - return () => { - current.removeEventListener("dragover", handleDragOver); - current.removeEventListener("drop", handleDrop); - current.removeEventListener("dragenter", handleDragEnter); - current.removeEventListener("dragleave", handleDragLeave); - }; - }, [ref]); - - return { ref, dragging }; -}; diff --git a/src/components/file/state.ts b/src/components/file/state.ts index 72002e2..c743482 100644 --- a/src/components/file/state.ts +++ b/src/components/file/state.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { SetState } from "~src/utils/state/type"; import { set } from "idb-keyval"; @@ -24,10 +24,10 @@ export const useFile = (): FileState => { useSaveHandle(handle); - const setFile = (handle: FileHandle | null): void => { + const setFile = useCallback((handle: FileHandle | null): void => { setHandle(handle); setDirty(false); - }; + }, []); return { handle, setHandle, dirty, setDirty, setFile }; }; diff --git a/src/components/prefs/theme/styles/bushido.css b/src/components/prefs/theme/styles/bushido.css index a882487..3ef9559 100644 --- a/src/components/prefs/theme/styles/bushido.css +++ b/src/components/prefs/theme/styles/bushido.css @@ -4,7 +4,6 @@ html.theme-bushido { --main-color: #ec4c56; --caret-color: #ec4c56; --sub-color: #596172; - --sub-color-rgb: 89, 97, 114; --text-color: #f6f0e9; --error-color: #ec4c56; } diff --git a/src/components/prefs/theme/styles/serika-dark.css b/src/components/prefs/theme/styles/serika-dark.css index a7a05ec..fec96cb 100644 --- a/src/components/prefs/theme/styles/serika-dark.css +++ b/src/components/prefs/theme/styles/serika-dark.css @@ -4,7 +4,6 @@ html.theme-serika-dark { --main-color: #e2b714; --caret-color: #e2b714; --sub-color: #646669; - --sub-color-rgb: 100, 102, 105; --text-color: #d1d0c5; --error-color: #ca4754; } From a0c58eeb0f0f7c8218eb9ac54c65c43c27399842 Mon Sep 17 00:00:00 2001 From: Thien Do Date: Thu, 2 Sep 2021 10:29:51 +0700 Subject: [PATCH 6/6] Chore: Fix linting --- .gitignore | 2 -- .prettierignore | 8 +++++--- src/app/app.tsx | 1 - 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index e0b363e..c63fda5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,5 @@ /node_modules /.parcel-cache /dist - .DS_Store - .vercel diff --git a/.prettierignore b/.prettierignore index 16c6af0..c63fda5 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,5 @@ -node_modules/ -dist/ -public/ +/node_modules +/.parcel-cache +/dist +.DS_Store +.vercel diff --git a/src/app/app.tsx b/src/app/app.tsx index 38f8991..8caa2e1 100644 --- a/src/app/app.tsx +++ b/src/app/app.tsx @@ -1,4 +1,3 @@ -import { useRef, useState } from "react"; import { useEditor } from "~/src/components/editor/state/state"; import { useFile } from "~/src/components/file/state"; import { Layout } from "~/src/components/layout/layout";