From 8bfd0a30feae81a94a24c32567ab9db310c5f387 Mon Sep 17 00:00:00 2001 From: graphemecluster Date: Wed, 27 Nov 2024 04:07:02 +0800 Subject: [PATCH] feat: Resize option panel by dragging the divider --- patches/monaco-editor+0.52.0.patch | 12 ++++ src/Components/App.tsx | 3 + src/Components/SchemaEditor.tsx | 102 ++++++++++++++++++++++++++--- src/consts.ts | 1 + src/state.ts | 35 ++++------ 5 files changed, 124 insertions(+), 29 deletions(-) diff --git a/patches/monaco-editor+0.52.0.patch b/patches/monaco-editor+0.52.0.patch index 1bf8119..e7b92ee 100644 --- a/patches/monaco-editor+0.52.0.patch +++ b/patches/monaco-editor+0.52.0.patch @@ -370,6 +370,18 @@ index 1e98235..f8ea2c7 100644 - "entry": "vs/basic-languages/yaml/yaml.contribution" } ]; +diff --git a/node_modules/monaco-editor/esm/vs/base/browser/ui/mouseCursor/mouseCursor.css b/node_modules/monaco-editor/esm/vs/base/browser/ui/mouseCursor/mouseCursor.css +index 1d7ede8..23b85be 100644 +--- a/node_modules/monaco-editor/esm/vs/base/browser/ui/mouseCursor/mouseCursor.css ++++ b/node_modules/monaco-editor/esm/vs/base/browser/ui/mouseCursor/mouseCursor.css +@@ -3,6 +3,6 @@ + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +-.monaco-mouse-cursor-text { ++body:not(.dragging) .monaco-mouse-cursor-text { + cursor: text; + } diff --git a/node_modules/monaco-editor/esm/vs/base/common/buffer.js b/node_modules/monaco-editor/esm/vs/base/common/buffer.js index 4534981..024fdcc 100644 --- a/node_modules/monaco-editor/esm/vs/base/common/buffer.js diff --git a/src/Components/App.tsx b/src/Components/App.tsx index f6a36d1..11edf23 100644 --- a/src/Components/App.tsx +++ b/src/Components/App.tsx @@ -25,6 +25,9 @@ injectGlobal` overflow: hidden; touch-action: none; } + body.dragging { + user-select: none; + } :lang(och-Latn-fonipa) { font-family: "CharisSILW", serif; } diff --git a/src/Components/SchemaEditor.tsx b/src/Components/SchemaEditor.tsx index 48fe297..36dacb4 100644 --- a/src/Components/SchemaEditor.tsx +++ b/src/Components/SchemaEditor.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { css } from "@emotion/react"; @@ -139,9 +139,7 @@ const CreateSchemaButton = styled.button` } `; const EditorArea = styled.div` - flex: 1; position: relative; - min-height: calc(6rem + 20vh); `; const ResetButton = styled.button` display: inline-block; @@ -179,11 +177,31 @@ const ParameterErrorHint = styled.p` color: red; `; const Options = styled.form` + flex: 1; padding: 0 1rem; overflow-y: auto; - border-top: 0.2rem solid #c4c6c8; `; -const SeparatorShadow = styled.div` +const Divider = styled.div<{ isDragging: boolean }>` + background-color: #c4c6c8; + height: 0.2rem; + position: relative; + cursor: ns-resize; + &::after { + content: ""; + position: absolute; + top: -0.1rem; + bottom: -0.1rem; + left: 0; + right: 0; + background-color: ${({ isDragging }) => (isDragging ? "#0078e7" : "transparent")}; + transition: background-color 150ms; + } + &:hover::after, + &:focus::after { + background-color: #0078e7; + } +`; +const DividerShadow = styled.div` position: absolute; left: 0; bottom: 0; @@ -481,6 +499,7 @@ export default function SchemaEditor({ state, setState, commonOptions, evaluateH const tabBarRef = useRef(null); function drag(name: string, { clientX: startX }: { clientX: number }, isMouse?: boolean) { + document.body.classList.add("dragging"); if (activeSchemaName !== name) setState(state => ({ ...state, activeSchemaName: name })); const { length } = schemas; if (length <= 1 || tabBarRef.current?.childElementCount !== length + 1) return; @@ -531,6 +550,7 @@ export default function SchemaEditor({ state, setState, commonOptions, evaluateH } if (i !== index) setState(actions.moveSchema(name, i)); + document.body.classList.remove("dragging"); if (isMouse) { document.removeEventListener("mousemove", move); document.removeEventListener("mouseup", end); @@ -646,6 +666,67 @@ export default function SchemaEditor({ state, setState, commonOptions, evaluateH }; }, [addFilesToSchema]); + const [isDividerDragging, setIsDividerDragging] = useState(false); + function dividerDrag({ target, clientY }: { target: EventTarget; clientY: number }, isMouse?: boolean) { + document.body.classList.add("dragging"); + document.body.style.cursor = "ns-resize"; + setIsDividerDragging(true); + const dividerElement = target as HTMLDivElement; + const container = dividerElement.parentElement!; + const editorElement = container.children[2]; + + const offsetY = clientY - dividerElement.getBoundingClientRect().top; + + function move(event: { clientY: number } | TouchEvent) { + clientY = "clientY" in event ? event.clientY : (event.touches?.[0]?.clientY ?? clientY); + const editorTop = editorElement.getBoundingClientRect().top; + const numerator = clientY - offsetY - editorTop; + const denominator = + container.getBoundingClientRect().height - dividerElement.getBoundingClientRect().height - editorTop; + setState(state => ({ ...state, optionPanelHeight: Math.min(Math.max(1 - numerator / denominator, 0.1), 0.9) })); + } + + function end() { + document.body.classList.remove("dragging"); + document.body.style.cursor = ""; + setIsDividerDragging(false); + if (isMouse) { + document.removeEventListener("mousemove", move); + document.removeEventListener("mouseup", end); + } else { + document.removeEventListener("touchmove", move); + document.removeEventListener("touchend", end); + document.removeEventListener("touchcancel", end); + } + } + + if (isMouse) { + document.addEventListener("mousemove", move); + document.addEventListener("mouseup", end); + } else { + document.addEventListener("touchmove", move); + document.addEventListener("touchend", end); + document.addEventListener("touchcancel", end); + } + } + + const [editorArea, setEditorArea] = useState(null); + const [optionPanel, setOptionPanel] = useState(null); + useLayoutEffect(() => { + if (!editorArea || !optionPanel) return; + function setOptionPanelHeight() { + editorArea!.style.height = + (1 - state.optionPanelHeight) * + (editorArea!.getBoundingClientRect().height + optionPanel!.getBoundingClientRect().height) + + "px"; + } + setOptionPanelHeight(); + addEventListener("resize", setOptionPanelHeight); + return () => { + removeEventListener("resize", setOptionPanelHeight); + }; + }, [editorArea, optionPanel, state.optionPanelHeight]); + return ( <> @@ -669,7 +750,7 @@ export default function SchemaEditor({ state, setState, commonOptions, evaluateH - + - + - + dividerDrag(event, true)} + onTouchStart={event => dividerDrag(event.touches[0])} + /> +

選項 {activeSchema?.parameters.size || activeSchema?.parameters.errors.length ? ( diff --git a/src/consts.ts b/src/consts.ts index 1cd3019..0c372f8 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -67,6 +67,7 @@ export type MainState = Readonly<{ convertVariant: boolean; syncCharPosition: boolean; activeSchemaName: string; + optionPanelHeight: number; }>; export type SchemaState = Readonly<{ diff --git a/src/state.ts b/src/state.ts index bb13482..936471d 100644 --- a/src/state.ts +++ b/src/state.ts @@ -5,11 +5,24 @@ import type { MainState } from "./consts"; export const stateStorageLocation = "autoderiver/0.2/state"; +function defaultState(): MainState { + return { + schemas: [], + article: defaultArticle, + option: "convertArticle", + convertVariant: false, + syncCharPosition: true, + activeSchemaName: "", + optionPanelHeight: 0.5, + }; +} + export default function initialState(): MainState { const state = localStorage.getItem(stateStorageLocation); if (state) { const result: MainState = JSON.parse(state); return { + ...defaultState(), ...result, schemas: result.schemas.map(schema => ({ ...schema, @@ -19,25 +32,5 @@ export default function initialState(): MainState { })), }; } - - /* - return { - schemas: [], - article: defaultArticle, - option: "convertArticle", - convertVariant: false, - autocomplete: true, - syncCharPosition: true, - activeSchemaName: "", - }; - */ - - return { - schemas: [], - article: defaultArticle, - option: "convertArticle", - convertVariant: false, - syncCharPosition: true, - activeSchemaName: "", - }; + return defaultState(); }