From b6f6fe0318327416c722991645f3c8afe0912bad Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Fri, 28 Jun 2024 19:37:08 -0400 Subject: [PATCH] fix selection on iOS! I think! This was a ton of work from Celine and I, eventually the solution we seem to have settled on involves capturing clicks with a block floating on top of the textblock and then triggering a focus into the textblock seperately. We also need to check if the textblock becomes hidden by the keyboard coming up and then scroll if neccessary. --- app/[doc_id]/layout.tsx | 3 - components/Blocks.tsx | 7 +- components/Cards.tsx | 3 +- components/SelectionManager.tsx | 17 ++-- components/TextBlock/index.tsx | 86 ++++++++++----------- components/TextBlock/keymap.ts | 43 +++++++---- components/Toolbar/LinkButton.tsx | 2 +- components/Toolbar/TextBlockTypeButtons.tsx | 2 +- components/Toolbar/TextDecorationButton.tsx | 2 +- package-lock.json | 1 + package.json | 1 + 11 files changed, 88 insertions(+), 79 deletions(-) diff --git a/app/[doc_id]/layout.tsx b/app/[doc_id]/layout.tsx index 88683308..639f0252 100644 --- a/app/[doc_id]/layout.tsx +++ b/app/[doc_id]/layout.tsx @@ -6,9 +6,6 @@ import { usePreventScroll } from "react-aria"; export default function Layout(props: { children: React.ReactNode }) { let viewheight = useViewportSize().height; let difference = useViewportDifference(); - - usePreventScroll(); - return
{props.children}
; } diff --git a/components/Blocks.tsx b/components/Blocks.tsx index e894754d..077c84ec 100644 --- a/components/Blocks.tsx +++ b/components/Blocks.tsx @@ -1,10 +1,6 @@ "use client"; import { Fact, useEntity, useReplicache } from "src/replicache"; -import { - TextBlock, - setEditorState, - useEditorStates, -} from "components/TextBlock"; +import { TextBlock } from "components/TextBlock"; import { generateKeyBetween } from "fractional-indexing"; import { useEffect } from "react"; import { elementId } from "src/utils/elementId"; @@ -16,6 +12,7 @@ import { CardBlock } from "./CardBlock"; import { ExternalLinkBlock } from "./ExternalLinkBlock"; import { BlockOptions } from "./BlockOptions"; import { useBlocks } from "src/hooks/queries/useBlocks"; +import { setEditorState, useEditorStates } from "src/state/useEditorState"; export type Block = { parent: string; diff --git a/components/Cards.tsx b/components/Cards.tsx index 0aa67033..37df3e9d 100644 --- a/components/Cards.tsx +++ b/components/Cards.tsx @@ -77,6 +77,7 @@ function Card(props: { entityID: string; first?: boolean }) { className={` card w-[calc(100vw-12px)] md:w-[calc(50vw-32px)] max-w-prose grow flex flex-col + overscroll-none overflow-y-scroll no-scrollbar rounded-lg border ${isFocused ? "shadow-md border-border" : "border-border-light"} @@ -209,7 +210,7 @@ export async function focusCard( }); if (firstBlock) { - focusBlock(firstBlock, "start", "top"); + focusBlock(firstBlock, { type: "start" }); } } diff --git a/components/SelectionManager.tsx b/components/SelectionManager.tsx index 1cf5b0a1..65f05ff4 100644 --- a/components/SelectionManager.tsx +++ b/components/SelectionManager.tsx @@ -5,8 +5,8 @@ import { useReplicache } from "src/replicache"; import { useUIState } from "src/useUIState"; import { getBlocksAsHTML } from "src/utils/getBlocksAsHTML"; import { scanIndex } from "src/replicache/utils"; -import { useEditorStates } from "./TextBlock"; import { focusBlock } from "./Blocks"; +import { useEditorStates } from "src/state/useEditorState"; export const useSelectingMouse = create(() => ({ start: null as null | { top: number; left: number }, })); @@ -63,8 +63,7 @@ export function SelectionManager() { useUIState.getState().setSelectedBlock(firstBlock); focusBlock( { ...firstBlock, type: type[0].data.value }, - "start", - "top", + { type: "start" }, ); } else { if ( @@ -130,7 +129,10 @@ export function SelectionManager() { ); if (!type?.[0]) return; useUIState.getState().setSelectedBlock(firstBlock); - focusBlock({ ...firstBlock, type: type[0].data.value }, "start", "top"); + focusBlock( + { ...firstBlock, type: type[0].data.value }, + { type: "start" }, + ); } if (e.key === "ArrowRight") { let selectedBlocks = useUIState @@ -144,7 +146,7 @@ export function SelectionManager() { ); if (!type?.[0]) return; useUIState.getState().setSelectedBlock(lastBlock); - focusBlock({ ...lastBlock, type: type[0].data.value }, "end", "top"); + focusBlock({ ...lastBlock, type: type[0].data.value }, { type: "end" }); } if (e.key === "ArrowDown") { let selectedBlocks = useUIState @@ -160,7 +162,10 @@ export function SelectionManager() { ); if (!type?.[0]) return; useUIState.getState().setSelectedBlock(lastBlock); - focusBlock({ ...lastBlock, type: type[0].data.value }, "end", "top"); + focusBlock( + { ...lastBlock, type: type[0].data.value }, + { type: "end" }, + ); } if (e.shiftKey) { if ( diff --git a/components/TextBlock/index.tsx b/components/TextBlock/index.tsx index af5e74b4..ab15e2bf 100644 --- a/components/TextBlock/index.tsx +++ b/components/TextBlock/index.tsx @@ -15,13 +15,12 @@ import { ReplicacheMutators, Fact, } from "src/replicache"; +import { isVisible } from "src/utils/isVisible"; import { EditorState } from "prosemirror-state"; -import { EditorView } from "prosemirror-view"; import { ySyncPlugin } from "y-prosemirror"; import { Replicache } from "replicache"; import { generateKeyBetween } from "fractional-indexing"; -import { create } from "zustand"; import { RenderYJSFragment } from "./RenderYJSFragment"; import { useInitialPageLoad } from "components/InitialPageLoadProvider"; import { addImage } from "src/utils/addImage"; @@ -33,35 +32,8 @@ import { MarkType, DOMParser as ProsemirrorDOMParser } from "prosemirror-model"; import { useAppEventListener } from "src/eventBus"; import { addLinkBlock } from "src/utils/addLinkBlock"; import { BlockOptions } from "components/BlockOptions"; - -export let useEditorStates = create(() => ({ - lastXPosition: 0, - editorStates: {} as { - [entity: string]: - | { - editor: InstanceType; - view?: InstanceType; - } - | undefined; - }, -})); - -export const setEditorState = ( - entityID: string, - s: { - editor: InstanceType; - }, -) => { - useEditorStates.setState((oldState) => { - let existingState = oldState.editorStates[entityID]; - return { - editorStates: { - ...oldState.editorStates, - [entityID]: { ...existingState, ...s }, - }, - }; - }); -}; +import { setEditorState, useEditorStates } from "src/state/useEditorState"; +import { isIOS } from "@react-aria/utils"; export function TextBlock(props: BlockProps & { className: string }) { let initialized = useInitialPageLoad(); @@ -77,12 +49,48 @@ export function TextBlock(props: BlockProps & { className: string }) { /> )}
+
); } +export function IOSBS(props: BlockProps) { + let selected = useUIState((s) => + s.selectedBlock.find((b) => b.value === props.entityID), + ); + if (selected) return null; + return ( +
{ + e.preventDefault(); + let target = e.target; + focusBlock(props, { + type: "coord", + top: e.clientY, + left: e.clientX, + }); + setTimeout(async () => { + let vis = await isVisible(target as Element); + if (!vis) { + let parentEl = document.getElementById( + elementId.card(props.parent).container, + ); + if (!parentEl) return; + parentEl?.scrollBy({ + top: 250, + behavior: "smooth", + }); + } + }, 600); + }} + /> + ); +} + export function RenderedTextBlock(props: { entityID: string; className?: string; @@ -262,8 +270,7 @@ export function BaseTextBlock(props: BlockProps & { className: string }) { parent: propsRef.current.parent, position: p, }, - "end", - "bottom", + { type: "end" }, ); } }, 10); @@ -444,19 +451,6 @@ let SyncView = (props: { entityID: string; parentID: string }) => { let cursorPosY = coords.top; let bottomScrollPadding = 100; if (cursorPosY && parentHeight) { - if (cursorPosY > parentHeight - bottomScrollPadding) { - parentID?.scrollBy({ - top: bottomScrollPadding - (parentHeight - cursorPosY), - behavior: "smooth", - }); - } - if (cursorPosY < 50) { - if (parentID?.scrollTop === 0) return; - parentID?.scrollBy({ - top: cursorPosY - 50, - behavior: "smooth", - }); - } } }, 10); }); diff --git a/components/TextBlock/keymap.ts b/components/TextBlock/keymap.ts index b2e957e1..52b60bae 100644 --- a/components/TextBlock/keymap.ts +++ b/components/TextBlock/keymap.ts @@ -7,9 +7,9 @@ import { MutableRefObject } from "react"; import { Replicache } from "replicache"; import { ReplicacheMutators } from "src/replicache"; import { elementId } from "src/utils/elementId"; -import { setEditorState, useEditorStates } from "."; import { schema } from "./schema"; import { useUIState } from "src/useUIState"; +import { setEditorState, useEditorStates } from "src/state/useEditorState"; export const TextBlockKeymap = ( propsRef: MutableRefObject, @@ -19,12 +19,18 @@ export const TextBlockKeymap = ( "Meta-b": toggleMark(schema.marks.strong), "Meta-u": toggleMark(schema.marks.underline), "Meta-i": toggleMark(schema.marks.em), + Escape: (_state, _dispatch, view) => { + view?.dom.blur(); + + return true; + }, "#": (state, dispatch, view) => { if (state.selection.content().size > 0) return false; if (state.selection.anchor > 1) return false; repRef.current?.mutate.increaseHeadingLevel({ entityID: propsRef.current.entityID, }); + setTimeout( () => focusBlock( @@ -34,8 +40,7 @@ export const TextBlockKeymap = ( position: propsRef.current.position, parent: propsRef.current.parent, }, - "start", - "bottom", + { type: "start" }, ), 10, ); @@ -96,7 +101,7 @@ export const TextBlockKeymap = ( let block = propsRef.current.previousBlock; if (block) { view.dom.blur(); - focusBlock(block, coords.left, "bottom"); + focusBlock(block, { left: coords.left, type: "bottom" }); return true; } return false; @@ -114,7 +119,7 @@ export const TextBlockKeymap = ( let block = propsRef.current.nextBlock; if (block) { view.dom.blur(); - focusBlock(block, coords.left, "top"); + focusBlock(block, { left: coords.left, type: "top" }); return true; } return false; @@ -127,7 +132,7 @@ export const TextBlockKeymap = ( let block = propsRef.current.previousBlock; if (block) { view?.dom.blur(); - focusBlock(block, "end", "top"); + focusBlock(block, { type: "end" }); } return true; }, @@ -137,7 +142,7 @@ export const TextBlockKeymap = ( let block = propsRef.current.nextBlock; if (block) { view?.dom.blur(); - focusBlock(block, "start", "top"); + focusBlock(block, { type: "start" }); } return true; }, @@ -172,8 +177,7 @@ const backspace = position: propsRef.current.position, parent: propsRef.current.parent, }, - "start", - "bottom", + { type: "start" }, ), 10, ); @@ -186,7 +190,7 @@ const backspace = blockEntity: propsRef.current.entityID, }); if (propsRef.current.previousBlock) { - focusBlock(propsRef.current.previousBlock, "end", "bottom"); + focusBlock(propsRef.current.previousBlock, { type: "end" }); } return true; } @@ -234,14 +238,15 @@ const enter = tr.delete(state.selection.anchor, state.doc.content.size); dispatch?.(tr); let newEntityID = crypto.randomUUID(); + let position = generateKeyBetween( + propsRef.current.position, + propsRef.current.nextPosition, + ); repRef.current?.mutate.addBlock({ newEntityID, parent: propsRef.current.parent, type: "text", - position: generateKeyBetween( - propsRef.current.position, - propsRef.current.nextPosition, - ), + position, }); setTimeout(() => { let block = useEditorStates.getState().editorStates[newEntityID]; @@ -255,8 +260,16 @@ const enter = editor: newState, }); } + focusBlock( + { + value: newEntityID, + parent: propsRef.current.parent, + type: "text", + position, + }, + { type: "start" }, + ); } - document.getElementById(elementId.block(newEntityID).text)?.focus(); }, 10); return true; }; diff --git a/components/Toolbar/LinkButton.tsx b/components/Toolbar/LinkButton.tsx index e3e0f76a..74600504 100644 --- a/components/Toolbar/LinkButton.tsx +++ b/components/Toolbar/LinkButton.tsx @@ -1,4 +1,3 @@ -import { setEditorState, useEditorStates } from "components/TextBlock"; import { schema } from "components/TextBlock/schema"; import { EditorState, TextSelection } from "prosemirror-state"; import { useUIState } from "src/useUIState"; @@ -7,6 +6,7 @@ import { CheckTiny, CloseTiny, LinkTextToolbarSmall } from "components/Icons"; import { useState } from "react"; import { Separator } from "components/Layout"; import { MarkType } from "prosemirror-model"; +import { setEditorState, useEditorStates } from "src/state/useEditorState"; export function LinkButton(props: { setToolBarState: (s: "link") => void }) { let focusedBlock = useUIState((s) => s.focusedBlock); diff --git a/components/Toolbar/TextBlockTypeButtons.tsx b/components/Toolbar/TextBlockTypeButtons.tsx index ba103a11..0d669d90 100644 --- a/components/Toolbar/TextBlockTypeButtons.tsx +++ b/components/Toolbar/TextBlockTypeButtons.tsx @@ -5,11 +5,11 @@ import { ParagraphSmall, } from "components/Icons"; import { Separator } from "components/Layout"; -import { setEditorState, useEditorStates } from "components/TextBlock"; import { CloseToolbarButton, ToolbarButton } from "components/Toolbar"; import { TextSelection } from "prosemirror-state"; import { useCallback } from "react"; import { useEntity, useReplicache } from "src/replicache"; +import { setEditorState, useEditorStates } from "src/state/useEditorState"; import { useUIState } from "src/useUIState"; export const TextBlockTypeButtons = (props: { onClose: () => void }) => { let focusedBlock = useUIState((s) => s.focusedBlock); diff --git a/components/Toolbar/TextDecorationButton.tsx b/components/Toolbar/TextDecorationButton.tsx index 31877048..48699987 100644 --- a/components/Toolbar/TextDecorationButton.tsx +++ b/components/Toolbar/TextDecorationButton.tsx @@ -1,10 +1,10 @@ -import { useEditorStates } from "components/TextBlock"; import { MarkType } from "prosemirror-model"; import { useUIState } from "src/useUIState"; import { ToolbarButton } from "."; import { toggleMark } from "prosemirror-commands"; import { TextSelection } from "prosemirror-state"; import { publishAppEvent } from "src/eventBus"; +import { useEditorStates } from "src/state/useEditorState"; export function TextDecorationButton(props: { mark: MarkType; diff --git a/package-lock.json b/package-lock.json index 9b561e41..096cca1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@nytimes/react-prosemirror": "^0.6.1", "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-slider": "^1.1.2", + "@react-aria/utils": "^3.24.1", "@react-spring/web": "^9.7.3", "@supabase/ssr": "^0.3.0", "@supabase/supabase-js": "^2.43.2", diff --git a/package.json b/package.json index 14149211..be6f1082 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@nytimes/react-prosemirror": "^0.6.1", "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-slider": "^1.1.2", + "@react-aria/utils": "^3.24.1", "@react-spring/web": "^9.7.3", "@supabase/ssr": "^0.3.0", "@supabase/supabase-js": "^2.43.2", -- 2.54.0