Skip to content

Commit

Permalink
feat: show active text formats on mobile toolbar
Browse files Browse the repository at this point in the history
  • Loading branch information
amanharwara committed Aug 16, 2023
1 parent f56b73f commit 35b2786
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 166 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,15 @@
*
*/

import { $isCodeHighlightNode } from '@lexical/code'
import { $isLinkNode, $isAutoLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { mergeRegister, $findMatchingParent, $getNearestNodeOfType } from '@lexical/utils'
import { mergeRegister } from '@lexical/utils'
import {
$getSelection,
$isRangeSelection,
$isTextNode,
FORMAT_TEXT_COMMAND,
LexicalEditor,
SELECTION_CHANGE_COMMAND,
$isRootOrShadowRoot,
COMMAND_PRIORITY_CRITICAL,
COMMAND_PRIORITY_LOW,
RangeSelection,
GridSelection,
Expand All @@ -27,17 +23,9 @@ import {
COMMAND_PRIORITY_NORMAL,
createCommand,
} from 'lexical'
import { $isHeadingNode } from '@lexical/rich-text'
import {
INSERT_UNORDERED_LIST_COMMAND,
REMOVE_LIST_COMMAND,
$isListNode,
ListNode,
INSERT_ORDERED_LIST_COMMAND,
} from '@lexical/list'
import { INSERT_UNORDERED_LIST_COMMAND, REMOVE_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND } from '@lexical/list'
import { ComponentPropsWithoutRef, useCallback, useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'

import { getSelectedNode } from '../../Lexical/Utils/getSelectedNode'
import {
BoldIcon,
Expand All @@ -59,21 +47,8 @@ import { getAdjustedStylesForNonPortalPopover } from '@/Components/Popover/Utils
import LinkEditor from '../LinkEditor/LinkEditor'
import LinkTextEditor, { $isLinkTextNode } from '../LinkEditor/LinkTextEditor'
import { URL_REGEX } from '@/Constants/Constants'

const blockTypeToBlockName = {
bullet: 'Bulleted List',
check: 'Check List',
code: 'Code Block',
h1: 'Heading 1',
h2: 'Heading 2',
h3: 'Heading 3',
h4: 'Heading 4',
h5: 'Heading 5',
h6: 'Heading 6',
number: 'Numbered List',
paragraph: 'Normal',
quote: 'Quote',
}
import { useSelectedTextFormatInfo } from './useSelectedTextFormatInfo'
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'

const IconSize = 15

Expand Down Expand Up @@ -464,138 +439,26 @@ function TextFormatFloatingToolbar({
}

function useFloatingTextFormatToolbar(editor: LexicalEditor, anchorElem: HTMLElement): JSX.Element | null {
const [activeEditor, setActiveEditor] = useState(editor)
const [isText, setIsText] = useState(false)
const [isLink, setIsLink] = useState(false)
const [isAutoLink, setIsAutoLink] = useState(false)
const [isLinkText, setIsLinkText] = useState(false)
const [isBold, setIsBold] = useState(false)
const [isItalic, setIsItalic] = useState(false)
const [isUnderline, setIsUnderline] = useState(false)
const [isStrikethrough, setIsStrikethrough] = useState(false)
const [isSubscript, setIsSubscript] = useState(false)
const [isSuperscript, setIsSuperscript] = useState(false)
const [isCode, setIsCode] = useState(false)
const [blockType, setBlockType] = useState<keyof typeof blockTypeToBlockName>('paragraph')

const updatePopup = useCallback(() => {
editor.getEditorState().read(() => {
// Should not to pop up the floating toolbar when using IME input
if (editor.isComposing()) {
return
}
const selection = $getSelection()
const nativeSelection = window.getSelection()
const rootElement = editor.getRootElement()

const isMobile = window.matchMedia('(max-width: 768px)').matches

if (isMobile) {
return
}

if (
nativeSelection !== null &&
(!$isRangeSelection(selection) || rootElement === null || !rootElement.contains(nativeSelection.anchorNode))
) {
setIsText(false)
return
}

if (!$isRangeSelection(selection)) {
return
}

const anchorNode = selection.anchor.getNode()
let element =
anchorNode.getKey() === 'root'
? anchorNode
: $findMatchingParent(anchorNode, (e) => {
const parent = e.getParent()
return parent !== null && $isRootOrShadowRoot(parent)
})

if (element === null) {
element = anchorNode.getTopLevelElementOrThrow()
}

const elementKey = element.getKey()
const elementDOM = activeEditor.getElementByKey(elementKey)

if (elementDOM !== null) {
if ($isListNode(element)) {
const parentList = $getNearestNodeOfType<ListNode>(anchorNode, ListNode)
const type = parentList ? parentList.getListType() : element.getListType()
setBlockType(type)
} else {
const type = $isHeadingNode(element) ? element.getTag() : element.getType()
if (type in blockTypeToBlockName) {
setBlockType(type as keyof typeof blockTypeToBlockName)
}
}
}

const node = getSelectedNode(selection)

// Update text format
setIsBold(selection.hasFormat('bold'))
setIsItalic(selection.hasFormat('italic'))
setIsUnderline(selection.hasFormat('underline'))
setIsStrikethrough(selection.hasFormat('strikethrough'))
setIsSubscript(selection.hasFormat('subscript'))
setIsSuperscript(selection.hasFormat('superscript'))
setIsCode(selection.hasFormat('code'))

// Update links
const parent = node.getParent()
if ($isLinkNode(parent) || $isLinkNode(node)) {
setIsLink(true)
} else {
setIsLink(false)
}
if ($isAutoLinkNode(parent) || $isAutoLinkNode(node)) {
setIsAutoLink(true)
} else {
setIsAutoLink(false)
}
if ($isLinkTextNode(node, selection)) {
setIsLinkText(true)
} else {
setIsLinkText(false)
}

if (!$isCodeHighlightNode(selection.anchor.getNode()) && selection.getTextContent() !== '') {
setIsText($isTextNode(node))
} else {
setIsText(false)
}
})
}, [editor, activeEditor])

useEffect(() => {
return editor.registerCommand(
SELECTION_CHANGE_COMMAND,
(_payload, newEditor) => {
setActiveEditor(newEditor)
updatePopup()
return false
},
COMMAND_PRIORITY_CRITICAL,
)
}, [editor, updatePopup])

useEffect(() => {
return mergeRegister(
editor.registerUpdateListener(() => {
updatePopup()
}),
editor.registerRootListener(() => {
if (editor.getRootElement() === null) {
setIsText(false)
}
}),
)
}, [editor, updatePopup])
const {
isText,
isLink,
isLinkText,
isAutoLink,
isBold,
isItalic,
isStrikethrough,
isSubscript,
isSuperscript,
isUnderline,
isCode,
blockType,
} = useSelectedTextFormatInfo()

const isMobile = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)

if (isMobile) {
return null
}

if (!isText && !isLink) {
return null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { GetRemoteImageBlock } from '../Blocks/RemoteImage'
import { InsertRemoteImageDialog } from '../RemoteImagePlugin/RemoteImagePlugin'
import LinkEditor from '../LinkEditor/LinkEditor'
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
import { useSelectedTextFormatInfo } from './useSelectedTextFormatInfo'

const MobileToolbarPlugin = () => {
const application = useApplication()
Expand Down Expand Up @@ -75,6 +76,9 @@ const MobileToolbarPlugin = () => {
}
}, [editor])

const { isBold, isItalic, isUnderline, isSubscript, isSuperscript, isStrikethrough } = useSelectedTextFormatInfo()
const [isSelectionLink, setIsSelectionLink] = useState(false)

const [canUndo, setCanUndo] = useState(false)
const [canRedo, setCanRedo] = useState(false)
useEffect(() => {
Expand Down Expand Up @@ -103,6 +107,7 @@ const MobileToolbarPlugin = () => {
name: string
iconName: string
keywords?: string[]
active?: boolean
disabled?: boolean
onSelect: () => void
}[] => [
Expand All @@ -128,41 +133,47 @@ const MobileToolbarPlugin = () => {
onSelect: () => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold')
},
active: isBold,
},
{
name: 'Italic',
iconName: 'italic',
onSelect: () => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic')
},
active: isItalic,
},
{
name: 'Underline',
iconName: 'underline',
onSelect: () => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline')
},
active: isUnderline,
},
{
name: 'Strikethrough',
iconName: 'strikethrough',
onSelect: () => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough')
},
active: isStrikethrough,
},
{
name: 'Subscript',
iconName: 'subscript',
onSelect: () => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript')
},
active: isSubscript,
},
{
name: 'Superscript',
iconName: 'superscript',
onSelect: () => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript')
},
active: isSuperscript,
},
{
name: 'Link',
Expand All @@ -172,6 +183,7 @@ const MobileToolbarPlugin = () => {
insertLink()
})
},
active: isSelectionLink,
},
{
name: 'Search',
Expand Down Expand Up @@ -201,7 +213,21 @@ const MobileToolbarPlugin = () => {
GetCollapsibleBlock(editor),
...GetEmbedsBlocks(editor),
],
[application.keyboardService, canRedo, canUndo, editor, insertLink, showModal],
[
application.keyboardService,
canRedo,
canUndo,
editor,
insertLink,
isBold,
isItalic,
isSelectionLink,
isStrikethrough,
isSubscript,
isSuperscript,
isUnderline,
showModal,
],
)

useEffect(() => {
Expand Down Expand Up @@ -272,8 +298,6 @@ const MobileToolbarPlugin = () => {
linkEditor?.removeEventListener('blur', handleLinkEditorBlur)
}
}, [])

const [isSelectionLink, setIsSelectionLink] = useState(false)
const [isSelectionAutoLink, setIsSelectionAutoLink] = useState(false)
const [linkUrl, setLinkUrl] = useState('')
const [isLinkEditMode, setIsLinkEditMode] = useState(false)
Expand Down Expand Up @@ -378,7 +402,10 @@ const MobileToolbarPlugin = () => {
{items.map((item) => {
return (
<button
className="flex items-center justify-center rounded px-3 py-3 disabled:opacity-50 select-none"
className={classNames(
'flex items-center justify-center rounded px-3 py-3 disabled:opacity-50 select-none',
item.active && 'bg-info text-info-contrast',
)}
aria-label={item.name}
onClick={item.onSelect}
onContextMenu={(event) => {
Expand Down
Loading

0 comments on commit 35b2786

Please sign in to comment.