chore: combine super link editor with mobile toolbar

This commit is contained in:
Aman Harwara
2023-07-24 14:27:16 +05:30
parent c52d8e7309
commit 8ad9b8ae2a
28 changed files with 307 additions and 201 deletions

View File

@@ -15,7 +15,7 @@
"lint": "eslint src/javascripts && yarn tsc",
"lint:fix": "eslint src/javascripts --fix",
"start": "webpack-dev-server --config web.webpack.dev.js",
"start-secure": "yarn start --server-type https",
"start-secure": "yarn start --https",
"test": "jest --config jest.config.js",
"tsc": "tsc --project tsconfig.json",
"upgrade:snjs": "ncu -u '@standardnotes/*'",
@@ -28,7 +28,7 @@
"@babel/plugin-transform-react-jsx": "^7.19.0",
"@babel/preset-env": "*",
"@babel/preset-typescript": "^7.21.5",
"@lexical/react": "0.11.2",
"@lexical/react": "0.11.3",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
"@simplewebauthn/browser": "^7.1.0",
"@standardnotes/authenticator": "^2.4.0",
@@ -76,7 +76,7 @@
"identity-obj-proxy": "^3.0.0",
"jest": "^29.3.1",
"jest-environment-jsdom": "^29.3.1",
"lexical": "0.11.2",
"lexical": "0.11.3",
"lint-staged": ">=13",
"mini-css-extract-plugin": "^2.7.2",
"minimatch": "^5.1.1",
@@ -109,7 +109,7 @@
},
"dependencies": {
"@ariakit/react": "^0.2.8",
"@lexical/headless": "0.11.2",
"@lexical/headless": "0.11.3",
"@radix-ui/react-slot": "^1.0.1",
"fast-diff": "^1.3.0"
}

View File

@@ -20,7 +20,6 @@ import CollapsiblePlugin from './Plugins/CollapsiblePlugin'
import DraggableBlockPlugin from './Plugins/DraggableBlockPlugin'
import CodeHighlightPlugin from './Plugins/CodeHighlightPlugin'
import FloatingTextFormatToolbarPlugin from './Plugins/FloatingTextFormatToolbarPlugin'
import FloatingLinkEditorPlugin from './Plugins/FloatingLinkEditorPlugin'
import { TabIndentationPlugin } from './Plugins/TabIndentationPlugin'
import { handleEditorChange } from './Utils'
import { SuperEditorContentId } from './Constants'
@@ -109,7 +108,6 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
{!readonly && floatingAnchorElem && (
<>
<FloatingTextFormatToolbarPlugin anchorElem={floatingAnchorElem} />
<FloatingLinkEditorPlugin anchorElem={floatingAnchorElem} />
<DraggableBlockPlugin anchorElem={floatingAnchorElem} />
</>
)}

View File

@@ -11,11 +11,15 @@ import {
CAN_UNDO_COMMAND,
COMMAND_PRIORITY_CRITICAL,
FORMAT_TEXT_COMMAND,
GridSelection,
NodeSelection,
REDO_COMMAND,
RangeSelection,
SELECTION_CHANGE_COMMAND,
UNDO_COMMAND,
} from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'
import { $isLinkNode, $isAutoLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { GetAlignmentBlocks } from '../Blocks/Alignment'
import { GetBulletedListBlock } from '../Blocks/BulletedList'
@@ -38,6 +42,7 @@ import { SUPER_TOGGLE_SEARCH } from '@standardnotes/ui-services'
import { useApplication } from '@/Components/ApplicationProvider'
import { GetRemoteImageBlock } from '../Blocks/RemoteImage'
import { InsertRemoteImageDialog } from '../RemoteImagePlugin/RemoteImagePlugin'
import LinkEditor from '../FloatingLinkEditorPlugin/LinkEditor'
const MobileToolbarPlugin = () => {
const application = useApplication()
@@ -45,10 +50,12 @@ const MobileToolbarPlugin = () => {
const [modal, showModal] = useModal()
const [isInEditor, setIsInEditor] = useState(false)
const [isInLinkEditor, setIsInLinkEditor] = useState(false)
const [isInToolbar, setIsInToolbar] = useState(false)
const isMobile = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
const toolbarRef = useRef<HTMLDivElement>(null)
const linkEditorRef = useRef<HTMLDivElement>(null)
const backspaceButtonRef = useRef<HTMLButtonElement>(null)
const insertLink = useCallback(() => {
@@ -214,8 +221,10 @@ const MobileToolbarPlugin = () => {
const handleBlur = (event: FocusEvent) => {
const elementToBeFocused = event.relatedTarget as Node
const toolbarContainsElementToFocus = toolbarRef.current && toolbarRef.current.contains(elementToBeFocused)
const linkEditorContainsElementToFocus =
linkEditorRef.current && linkEditorRef.current.contains(elementToBeFocused)
const willFocusBackspaceButton = backspaceButtonRef.current && elementToBeFocused === backspaceButtonRef.current
if (toolbarContainsElementToFocus || willFocusBackspaceButton) {
if (toolbarContainsElementToFocus || linkEditorContainsElementToFocus || willFocusBackspaceButton) {
return
}
setIsInEditor(false)
@@ -231,31 +240,116 @@ const MobileToolbarPlugin = () => {
}, [editor])
useEffect(() => {
if (!toolbarRef.current) {
return
}
const toolbar = toolbarRef.current
const linkEditor = linkEditorRef.current
const handleFocus = () => setIsInToolbar(true)
const handleBlur = (event: FocusEvent) => {
const handleToolbarFocus = () => setIsInToolbar(true)
const handleLinkEditorFocus = () => setIsInLinkEditor(true)
const handleToolbarBlur = (event: FocusEvent) => {
const elementToBeFocused = event.relatedTarget as Node
if (elementToBeFocused === backspaceButtonRef.current) {
return
}
setIsInToolbar(false)
}
const handleLinkEditorBlur = (event: FocusEvent) => {
const elementToBeFocused = event.relatedTarget as Node
if (elementToBeFocused === backspaceButtonRef.current) {
return
}
setIsInLinkEditor(false)
}
toolbar.addEventListener('focus', handleFocus)
toolbar.addEventListener('blur', handleBlur)
if (toolbar) {
toolbar.addEventListener('focus', handleToolbarFocus)
toolbar.addEventListener('blur', handleToolbarBlur)
}
if (linkEditor) {
linkEditor.addEventListener('focus', handleLinkEditorFocus)
linkEditor.addEventListener('blur', handleLinkEditorBlur)
}
return () => {
toolbar?.removeEventListener('focus', handleFocus)
toolbar?.removeEventListener('blur', handleBlur)
toolbar?.removeEventListener('focus', handleToolbarFocus)
toolbar?.removeEventListener('blur', handleToolbarBlur)
linkEditor?.removeEventListener('focus', handleLinkEditorFocus)
linkEditor?.removeEventListener('blur', handleLinkEditorBlur)
}
}, [])
const isFocusInEditorOrToolbar = isInEditor || isInToolbar
const [isSelectionLink, setIsSelectionLink] = useState(false)
const [isSelectionAutoLink, setIsSelectionAutoLink] = useState(false)
const [linkUrl, setLinkUrl] = useState('')
const [isLinkEditMode, setIsLinkEditMode] = useState(false)
const [lastSelection, setLastSelection] = useState<RangeSelection | GridSelection | NodeSelection | null>(null)
const updateEditorSelection = useCallback(() => {
editor.getEditorState().read(() => {
const selection = $getSelection()
const nativeSelection = window.getSelection()
const activeElement = document.activeElement
const rootElement = editor.getRootElement()
if (!$isRangeSelection(selection)) {
return
}
const node = getSelectedNode(selection)
const parent = node.getParent()
if ($isLinkNode(parent) || $isLinkNode(node)) {
setIsSelectionLink(true)
} else {
setIsSelectionLink(false)
}
if ($isAutoLinkNode(parent) || $isAutoLinkNode(node)) {
setIsSelectionAutoLink(true)
} else {
setIsSelectionAutoLink(false)
}
if ($isLinkNode(parent)) {
setLinkUrl(parent.getURL())
} else if ($isLinkNode(node)) {
setLinkUrl(node.getURL())
} else {
setLinkUrl('')
}
if (
selection !== null &&
nativeSelection !== null &&
rootElement !== null &&
rootElement.contains(nativeSelection.anchorNode)
) {
setLastSelection(selection)
} else if (!activeElement || activeElement.id !== 'link-input') {
setLastSelection(null)
setIsLinkEditMode(false)
setLinkUrl('')
}
})
}, [editor])
useEffect(() => {
return mergeRegister(
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
() => {
updateEditorSelection()
return false
},
COMMAND_PRIORITY_CRITICAL,
),
editor.registerUpdateListener(() => {
updateEditorSelection()
}),
)
}, [editor, updateEditorSelection])
const isFocusInEditorOrToolbar = isInEditor || isInToolbar || isInLinkEditor
if (!isMobile || !isFocusInEditorOrToolbar) {
return null
}
@@ -263,32 +357,46 @@ const MobileToolbarPlugin = () => {
return (
<>
{modal}
<div className="flex w-full flex-shrink-0 border-t border-border bg-contrast">
<div
tabIndex={-1}
className={classNames('flex items-center gap-1 overflow-x-auto', '[&::-webkit-scrollbar]:h-0')}
ref={toolbarRef}
>
{items.map((item) => {
return (
<button
className="flex items-center justify-center rounded px-3 py-3 disabled:opacity-50"
aria-label={item.name}
onClick={item.onSelect}
key={item.name}
disabled={item.disabled}
>
<Icon type={item.iconName} size="medium" className="!text-current [&>path]:!text-current" />
</button>
)
})}
<div className="bg-contrast">
{isSelectionLink && (
<div className="border-t border-border px-2" ref={linkEditorRef}>
<LinkEditor
linkUrl={linkUrl}
isEditMode={isLinkEditMode}
setEditMode={setIsLinkEditMode}
isAutoLink={isSelectionAutoLink}
editor={editor}
lastSelection={lastSelection}
/>
</div>
)}
<div className="flex w-full flex-shrink-0 border-t border-border bg-contrast">
<div
tabIndex={-1}
className={classNames('flex items-center gap-1 overflow-x-auto', '[&::-webkit-scrollbar]:h-0')}
ref={toolbarRef}
>
{items.map((item) => {
return (
<button
className="flex items-center justify-center rounded px-3 py-3 disabled:opacity-50"
aria-label={item.name}
onClick={item.onSelect}
key={item.name}
disabled={item.disabled}
>
<Icon type={item.iconName} size="medium" className="!text-current [&>path]:!text-current" />
</button>
)
})}
</div>
<button
className="flex flex-shrink-0 items-center justify-center rounded border-l border-border px-3 py-3"
aria-label="Dismiss keyboard"
>
<Icon type="keyboard-close" size="medium" />
</button>
</div>
<button
className="flex flex-shrink-0 items-center justify-center rounded border-l border-border px-3 py-3"
aria-label="Dismiss keyboard"
>
<Icon type="keyboard-close" size="medium" />
</button>
</div>
</>
)