fix: Fixed performance regression in Super notes and improved toolbar (#2590)
This commit is contained in:
3
packages/icons/src/Icons/ic-details-block.svg
Normal file
3
packages/icons/src/Icons/ic-details-block.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path fill="currentColor" fill-rule="evenodd" d="M0 4.13v1.428a.5.5 0 0 0 .725.446l.886-.446l.377-.19L2 5.362l1.404-.708l.07-.036l.662-.333l.603-.304a.5.5 0 0 0 0-.893l-.603-.305l-.662-.333l-.07-.036L2 1.706l-.012-.005l-.377-.19l-.886-.447A.5.5 0 0 0 0 1.51v2.62ZM7.25 2a.75.75 0 0 0 0 1.5h7a.25.25 0 0 1 .25.25v8.5a.25.25 0 0 1-.25.25h-9.5a.25.25 0 0 1-.25-.25V6.754a.75.75 0 0 0-1.5 0v5.496c0 .966.784 1.75 1.75 1.75h9.5A1.75 1.75 0 0 0 16 12.25v-8.5A1.75 1.75 0 0 0 14.25 2h-7Zm-.5 4a.75.75 0 0 0 0 1.5h5.5a.75.75 0 0 0 0-1.5h-5.5ZM6 9.25a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5A.75.75 0 0 1 6 9.25Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 705 B |
@@ -213,6 +213,7 @@ import UserSwitch from './ic-user-switch.svg'
|
||||
import ViewIcon from './ic-view.svg'
|
||||
import WarningIcon from './ic-warning.svg'
|
||||
import WindowIcon from './ic-window.svg'
|
||||
import DetailsBlockIcon from './ic-details-block.svg'
|
||||
|
||||
export {
|
||||
AccessibilityIcon,
|
||||
@@ -267,6 +268,7 @@ export {
|
||||
CopyIcon,
|
||||
CreateAccountIllustration,
|
||||
DashboardIcon,
|
||||
DetailsBlockIcon,
|
||||
DiamondFilledIcon,
|
||||
DiamondIcon,
|
||||
DownloadIcon,
|
||||
|
||||
@@ -189,3 +189,4 @@ export type IconType =
|
||||
| 'view'
|
||||
| 'warning'
|
||||
| 'window'
|
||||
| 'details-block'
|
||||
|
||||
@@ -24,6 +24,8 @@ export const IconNameToSvgMapping = {
|
||||
'chevron-up': icons.ChevronUpIcon,
|
||||
'clear-circle-filled': icons.ClearCircleFilledIcon,
|
||||
'cloud-off': icons.CloudOffIcon,
|
||||
'code-tags': icons.CodeTagsIcon,
|
||||
'details-block': icons.DetailsBlockIcon,
|
||||
'diamond-filled': icons.DiamondFilledIcon,
|
||||
'email-filled': icons.EmailFilledIcon,
|
||||
'eye-off': icons.EyeOffIcon,
|
||||
|
||||
@@ -3,18 +3,7 @@ import { useEffect, useRef, useState } from 'react'
|
||||
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import { ApplicationEvent, PrefKey, PrefDefaults } from '@standardnotes/snjs'
|
||||
|
||||
function getScrollParent(node: HTMLElement | null): HTMLElement | null {
|
||||
if (!node) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (node.scrollHeight > node.clientHeight || node.scrollWidth > node.clientWidth) {
|
||||
return node
|
||||
} else {
|
||||
return getScrollParent(node.parentElement)
|
||||
}
|
||||
}
|
||||
import { getScrollParent } from '@/Utils'
|
||||
|
||||
const supportsPassive = (() => {
|
||||
let supportsPassive = false
|
||||
|
||||
@@ -6,6 +6,8 @@ import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/u
|
||||
import { getPositionedPopoverStyles } from '../Popover/GetPositionedPopoverStyles'
|
||||
import { getAdjustedStylesForNonPortalPopover } from '../Popover/Utils/getAdjustedStylesForNonPortal'
|
||||
import { useLongPressEvent } from '@/Hooks/useLongPress'
|
||||
import { PopoverSide } from '../Popover/Types'
|
||||
import { getScrollParent } from '@/Utils'
|
||||
|
||||
const StyledTooltip = ({
|
||||
children,
|
||||
@@ -15,6 +17,7 @@ const StyledTooltip = ({
|
||||
showOnHover = true,
|
||||
interactive = false,
|
||||
type = 'label',
|
||||
side,
|
||||
...props
|
||||
}: {
|
||||
children: ReactNode
|
||||
@@ -24,6 +27,7 @@ const StyledTooltip = ({
|
||||
showOnHover?: boolean
|
||||
interactive?: boolean
|
||||
type?: TooltipStoreProps['type']
|
||||
side?: PopoverSide
|
||||
} & Partial<TooltipOptions>) => {
|
||||
const [forceOpen, setForceOpen] = useState<boolean | undefined>()
|
||||
|
||||
@@ -66,6 +70,27 @@ const StyledTooltip = ({
|
||||
onClick: () => setForceOpen(false),
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const anchor = anchorRef.current
|
||||
if (!anchor) {
|
||||
return
|
||||
}
|
||||
|
||||
const scrollParent = getScrollParent(anchor)
|
||||
if (!scrollParent) {
|
||||
return
|
||||
}
|
||||
|
||||
const handleScroll = () => {
|
||||
tooltip.hide()
|
||||
}
|
||||
|
||||
scrollParent.addEventListener('scroll', handleScroll)
|
||||
return () => {
|
||||
scrollParent.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
}, [tooltip])
|
||||
|
||||
if (isMobile && !showOnMobile) {
|
||||
return <>{children}</>
|
||||
}
|
||||
@@ -110,7 +135,7 @@ const StyledTooltip = ({
|
||||
|
||||
const styles = getPositionedPopoverStyles({
|
||||
align: 'center',
|
||||
side: 'bottom',
|
||||
side: side || 'bottom',
|
||||
anchorRect,
|
||||
popoverRect,
|
||||
documentRect,
|
||||
|
||||
@@ -6,27 +6,30 @@ import useModal from '../../Lexical/Hooks/useModal'
|
||||
import { InsertTableDialog } from '../../Plugins/TablePlugin'
|
||||
import { BlockPickerOption } from './BlockPickerOption'
|
||||
import { BlockPickerMenuItem } from './BlockPickerMenuItem'
|
||||
import { GetNumberedListBlockOption } from './Options/NumberedList'
|
||||
import { GetBulletedListBlockOption } from './Options/BulletedList'
|
||||
import { GetChecklistBlockOption } from './Options/Checklist'
|
||||
import { GetDividerBlockOption } from './Options/Divider'
|
||||
import { GetCollapsibleBlockOption } from './Options/Collapsible'
|
||||
import { GetDynamicPasswordBlocks, GetPasswordBlockOption } from './Options/Password'
|
||||
import { GetParagraphBlockOption } from './Options/Paragraph'
|
||||
import { GetHeadingsBlockOptions } from './Options/Headings'
|
||||
import { GetQuoteBlockOption } from './Options/Quote'
|
||||
import { GetAlignmentBlockOptions } from './Options/Alignment'
|
||||
import { GetCodeBlockOption } from './Options/Code'
|
||||
import { GetEmbedsBlockOptions } from './Options/Embeds'
|
||||
import { GetDynamicTableBlocks, GetTableBlockOption } from './Options/Table'
|
||||
import { GetDynamicPasswordBlocks, GetPasswordBlockOption } from '../Blocks/Password'
|
||||
import { GetDynamicTableBlocks, GetTableBlockOption } from '../Blocks/Table'
|
||||
import Popover from '@/Components/Popover/Popover'
|
||||
import { PopoverClassNames } from '../ClassNames'
|
||||
import { GetDatetimeBlockOptions } from './Options/DateTime'
|
||||
import { GetDatetimeBlockOptions } from '../Blocks/DateTime'
|
||||
import { isMobileScreen } from '@/Utils'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
import { GetIndentOutdentBlockOptions } from './Options/IndentOutdent'
|
||||
import { GetRemoteImageBlockOption } from './Options/RemoteImage'
|
||||
import { GetRemoteImageBlockOption } from '../Blocks/RemoteImage'
|
||||
import { InsertRemoteImageDialog } from '../RemoteImagePlugin/RemoteImagePlugin'
|
||||
import { GetParagraphBlockOption } from '../Blocks/Paragraph'
|
||||
import { GetH1BlockOption, GetH2BlockOption, GetH3BlockOption } from '../Blocks/Headings'
|
||||
import { GetIndentBlockOption, GetOutdentBlockOption } from '../Blocks/IndentOutdent'
|
||||
import {
|
||||
GetCenterAlignBlockOption,
|
||||
GetJustifyAlignBlockOption,
|
||||
GetLeftAlignBlockOption,
|
||||
GetRightAlignBlockOption,
|
||||
} from '../Blocks/Alignment'
|
||||
import { GetNumberedListBlockOption, GetBulletedListBlockOption, GetChecklistBlockOption } from '../Blocks/List'
|
||||
import { GetCodeBlockOption } from '../Blocks/Code'
|
||||
import { GetQuoteBlockOption } from '../Blocks/Quote'
|
||||
import { GetDividerBlockOption } from '../Blocks/Divider'
|
||||
import { GetCollapsibleBlockOption } from '../Blocks/Collapsible'
|
||||
import { GetEmbedsBlockOptions } from '../Blocks/Embeds'
|
||||
|
||||
export default function BlockPickerMenuPlugin(): JSX.Element {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
@@ -39,11 +42,15 @@ export default function BlockPickerMenuPlugin(): JSX.Element {
|
||||
})
|
||||
|
||||
const options = useMemo(() => {
|
||||
const indentOutdentOptions = application.isNativeMobileWeb() ? GetIndentOutdentBlockOptions(editor) : []
|
||||
const indentOutdentOptions = application.isNativeMobileWeb()
|
||||
? [GetIndentBlockOption(editor), GetOutdentBlockOption(editor)]
|
||||
: []
|
||||
|
||||
const baseOptions = [
|
||||
GetParagraphBlockOption(editor),
|
||||
...GetHeadingsBlockOptions(editor),
|
||||
GetH1BlockOption(editor),
|
||||
GetH2BlockOption(editor),
|
||||
GetH3BlockOption(editor),
|
||||
...indentOutdentOptions,
|
||||
GetTableBlockOption(() =>
|
||||
showModal('Insert Table', (onClose) => <InsertTableDialog activeEditor={editor} onClose={onClose} />),
|
||||
@@ -58,7 +65,10 @@ export default function BlockPickerMenuPlugin(): JSX.Element {
|
||||
GetCodeBlockOption(editor),
|
||||
GetDividerBlockOption(editor),
|
||||
...GetDatetimeBlockOptions(editor),
|
||||
...GetAlignmentBlockOptions(editor),
|
||||
GetLeftAlignBlockOption(editor),
|
||||
GetCenterAlignBlockOption(editor),
|
||||
GetRightAlignBlockOption(editor),
|
||||
GetJustifyAlignBlockOption(editor),
|
||||
GetPasswordBlockOption(editor),
|
||||
GetCollapsibleBlockOption(editor),
|
||||
...GetEmbedsBlockOptions(editor),
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { GetAlignmentBlocks } from '../../Blocks/Alignment'
|
||||
|
||||
export function GetAlignmentBlockOptions(editor: LexicalEditor) {
|
||||
return GetAlignmentBlocks(editor).map(
|
||||
(block) =>
|
||||
new BlockPickerOption(block.name, {
|
||||
iconName: block.iconName,
|
||||
keywords: block.keywords,
|
||||
onSelect: block.onSelect,
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { GetBulletedListBlock } from '../../Blocks/BulletedList'
|
||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||
|
||||
export function GetBulletedListBlockOption(editor: LexicalEditor) {
|
||||
const block = GetBulletedListBlock(editor)
|
||||
return new BlockPickerOption(block.name, {
|
||||
iconName: block.iconName as LexicalIconName,
|
||||
keywords: block.keywords,
|
||||
onSelect: block.onSelect,
|
||||
})
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { GetChecklistBlock } from '../../Blocks/Checklist'
|
||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||
|
||||
export function GetChecklistBlockOption(editor: LexicalEditor) {
|
||||
const block = GetChecklistBlock(editor)
|
||||
return new BlockPickerOption(block.name, {
|
||||
iconName: block.iconName as LexicalIconName,
|
||||
keywords: block.keywords,
|
||||
onSelect: block.onSelect,
|
||||
})
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { GetCodeBlock } from '../../Blocks/Code'
|
||||
|
||||
export function GetCodeBlockOption(editor: LexicalEditor) {
|
||||
const block = GetCodeBlock(editor)
|
||||
return new BlockPickerOption(block.name, {
|
||||
iconName: block.iconName,
|
||||
keywords: block.keywords,
|
||||
onSelect: block.onSelect,
|
||||
})
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { GetCollapsibleBlock } from '../../Blocks/Collapsible'
|
||||
|
||||
export function GetCollapsibleBlockOption(editor: LexicalEditor) {
|
||||
const block = GetCollapsibleBlock(editor)
|
||||
return new BlockPickerOption(block.name, {
|
||||
iconName: block.iconName,
|
||||
keywords: block.keywords,
|
||||
onSelect: block.onSelect,
|
||||
})
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { GetDatetimeBlocks } from '../../Blocks/DateTime'
|
||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||
|
||||
export function GetDatetimeBlockOptions(editor: LexicalEditor) {
|
||||
return GetDatetimeBlocks(editor).map(
|
||||
(block) =>
|
||||
new BlockPickerOption(block.name, {
|
||||
iconName: block.iconName as LexicalIconName,
|
||||
keywords: block.keywords,
|
||||
onSelect: block.onSelect,
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { GetDividerBlock } from '../../Blocks/Divider'
|
||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||
|
||||
export function GetDividerBlockOption(editor: LexicalEditor) {
|
||||
const block = GetDividerBlock(editor)
|
||||
return new BlockPickerOption(block.name, {
|
||||
iconName: block.iconName as LexicalIconName,
|
||||
keywords: block.keywords,
|
||||
onSelect: block.onSelect,
|
||||
})
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||
import { GetEmbedsBlocks } from '../../Blocks/Embeds'
|
||||
|
||||
export function GetEmbedsBlockOptions(editor: LexicalEditor) {
|
||||
return GetEmbedsBlocks(editor).map(
|
||||
(block) =>
|
||||
new BlockPickerOption(block.name, {
|
||||
iconName: block.iconName as LexicalIconName,
|
||||
keywords: block.keywords,
|
||||
onSelect: block.onSelect,
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||
import { GetHeadingsBlocks } from '../../Blocks/Headings'
|
||||
|
||||
export function GetHeadingsBlockOptions(editor: LexicalEditor) {
|
||||
return GetHeadingsBlocks(editor).map(
|
||||
(block) =>
|
||||
new BlockPickerOption(block.name, {
|
||||
iconName: block.iconName as LexicalIconName,
|
||||
keywords: block.keywords,
|
||||
onSelect: block.onSelect,
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { GetIndentOutdentBlocks } from '../../Blocks/IndentOutdent'
|
||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||
|
||||
export function GetIndentOutdentBlockOptions(editor: LexicalEditor) {
|
||||
return GetIndentOutdentBlocks(editor).map(
|
||||
(block) =>
|
||||
new BlockPickerOption(block.name, {
|
||||
iconName: block.iconName as LexicalIconName,
|
||||
keywords: block.keywords,
|
||||
onSelect: block.onSelect,
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { GetNumberedListBlock } from '../../Blocks/NumberedList'
|
||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||
|
||||
export function GetNumberedListBlockOption(editor: LexicalEditor) {
|
||||
const block = GetNumberedListBlock(editor)
|
||||
return new BlockPickerOption(block.name, {
|
||||
iconName: block.iconName as LexicalIconName,
|
||||
keywords: block.keywords,
|
||||
onSelect: block.onSelect,
|
||||
})
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { GetParagraphBlock } from '../../Blocks/Paragraph'
|
||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||
|
||||
export function GetParagraphBlockOption(editor: LexicalEditor) {
|
||||
const block = GetParagraphBlock(editor)
|
||||
return new BlockPickerOption(block.name, {
|
||||
iconName: block.iconName as LexicalIconName,
|
||||
keywords: block.keywords,
|
||||
onSelect: block.onSelect,
|
||||
})
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { INSERT_PASSWORD_COMMAND } from '../../Commands'
|
||||
import { GetPasswordBlock } from '../../Blocks/Password'
|
||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||
|
||||
const MIN_PASSWORD_LENGTH = 8
|
||||
|
||||
export function GetPasswordBlockOption(editor: LexicalEditor) {
|
||||
const block = GetPasswordBlock(editor)
|
||||
return new BlockPickerOption(block.name, {
|
||||
iconName: block.iconName as LexicalIconName,
|
||||
keywords: block.keywords,
|
||||
onSelect: block.onSelect,
|
||||
})
|
||||
}
|
||||
|
||||
export function GetDynamicPasswordBlocks(editor: LexicalEditor, queryString: string) {
|
||||
if (queryString == null) {
|
||||
return []
|
||||
}
|
||||
|
||||
const lengthRegex = /^\d+$/
|
||||
const match = lengthRegex.exec(queryString)
|
||||
|
||||
if (!match) {
|
||||
return []
|
||||
}
|
||||
|
||||
const length = parseInt(match[0], 10)
|
||||
if (length < MIN_PASSWORD_LENGTH) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [
|
||||
new BlockPickerOption(`Generate ${length}-character cryptographically secure password`, {
|
||||
iconName: 'password',
|
||||
keywords: ['password', 'secure'],
|
||||
onSelect: () => editor.dispatchCommand(INSERT_PASSWORD_COMMAND, length.toString()),
|
||||
}),
|
||||
]
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { GetQuoteBlock } from '../../Blocks/Quote'
|
||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||
|
||||
export function GetQuoteBlockOption(editor: LexicalEditor) {
|
||||
const block = GetQuoteBlock(editor)
|
||||
return new BlockPickerOption(block.name, {
|
||||
iconName: block.iconName as LexicalIconName,
|
||||
keywords: block.keywords,
|
||||
onSelect: block.onSelect,
|
||||
})
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||
import { GetRemoteImageBlock } from '../../Blocks/RemoteImage'
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
|
||||
export function GetRemoteImageBlockOption(onSelect: () => void) {
|
||||
const block = GetRemoteImageBlock(onSelect)
|
||||
return new BlockPickerOption(block.name, {
|
||||
iconName: block.iconName as LexicalIconName,
|
||||
keywords: block.keywords,
|
||||
onSelect: block.onSelect,
|
||||
})
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { BlockPickerOption } from '../BlockPickerOption'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { INSERT_TABLE_COMMAND } from '@lexical/table'
|
||||
import { GetTableBlock } from '../../Blocks/Table'
|
||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||
|
||||
export function GetTableBlockOption(onSelect: () => void) {
|
||||
const block = GetTableBlock(onSelect)
|
||||
return new BlockPickerOption(block.name, {
|
||||
iconName: block.iconName as LexicalIconName,
|
||||
keywords: block.keywords,
|
||||
onSelect: block.onSelect,
|
||||
})
|
||||
}
|
||||
|
||||
export function GetDynamicTableBlocks(editor: LexicalEditor, queryString: string) {
|
||||
const options: Array<BlockPickerOption> = []
|
||||
|
||||
if (queryString == null) {
|
||||
return options
|
||||
}
|
||||
|
||||
const fullTableRegex = new RegExp(/^([1-9]|10)x([1-9]|10)$/)
|
||||
const partialTableRegex = new RegExp(/^([1-9]|10)x?$/)
|
||||
|
||||
const fullTableMatch = fullTableRegex.exec(queryString)
|
||||
const partialTableMatch = partialTableRegex.exec(queryString)
|
||||
|
||||
if (fullTableMatch) {
|
||||
const [rows, columns] = fullTableMatch[0].split('x').map((n: string) => parseInt(n, 10))
|
||||
|
||||
options.push(
|
||||
new BlockPickerOption(`${rows}x${columns} Table`, {
|
||||
iconName: 'table',
|
||||
keywords: ['table'],
|
||||
onSelect: () => editor.dispatchCommand(INSERT_TABLE_COMMAND, { columns: String(columns), rows: String(rows) }),
|
||||
}),
|
||||
)
|
||||
} else if (partialTableMatch) {
|
||||
const rows = parseInt(partialTableMatch[0], 10)
|
||||
|
||||
options.push(
|
||||
...Array.from({ length: 5 }, (_, i) => i + 1).map(
|
||||
(columns) =>
|
||||
new BlockPickerOption(`${rows}x${columns} Table`, {
|
||||
iconName: 'table',
|
||||
keywords: ['table'],
|
||||
onSelect: () =>
|
||||
editor.dispatchCommand(INSERT_TABLE_COMMAND, { columns: String(columns), rows: String(rows) }),
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
@@ -1,11 +1,63 @@
|
||||
import { FORMAT_ELEMENT_COMMAND, LexicalEditor, ElementFormatType } from 'lexical'
|
||||
import { FORMAT_ELEMENT_COMMAND, LexicalEditor } from 'lexical'
|
||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||
import { BlockPickerOption } from '../BlockPickerPlugin/BlockPickerOption'
|
||||
|
||||
export function GetAlignmentBlocks(editor: LexicalEditor) {
|
||||
return ['left', 'center', 'right', 'justify'].map((alignment) => ({
|
||||
name: `Align ${alignment}`,
|
||||
iconName: `align-${alignment}` as LexicalIconName,
|
||||
keywords: ['align', 'justify', alignment],
|
||||
onSelect: () => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, alignment as ElementFormatType),
|
||||
}))
|
||||
export const LeftAlignBlock = {
|
||||
name: 'Align left',
|
||||
iconName: 'align-left',
|
||||
keywords: ['align', 'justify', 'left'],
|
||||
onSelect: (editor: LexicalEditor) => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'left'),
|
||||
}
|
||||
|
||||
export const CenterAlignBlock = {
|
||||
name: 'Align center',
|
||||
iconName: 'align-center',
|
||||
keywords: ['align', 'justify', 'center'],
|
||||
onSelect: (editor: LexicalEditor) => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'center'),
|
||||
}
|
||||
|
||||
export const RightAlignBlock = {
|
||||
name: 'Align right',
|
||||
iconName: 'align-right',
|
||||
keywords: ['align', 'justify', 'right'],
|
||||
onSelect: (editor: LexicalEditor) => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'right'),
|
||||
}
|
||||
|
||||
export const JustifyAlignBlock = {
|
||||
name: 'Align justify',
|
||||
iconName: 'align-justify',
|
||||
keywords: ['align', 'justify', 'justify'],
|
||||
onSelect: (editor: LexicalEditor) => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'justify'),
|
||||
}
|
||||
|
||||
export function GetLeftAlignBlockOption(editor: LexicalEditor) {
|
||||
return new BlockPickerOption(LeftAlignBlock.name, {
|
||||
iconName: LeftAlignBlock.iconName as LexicalIconName,
|
||||
keywords: LeftAlignBlock.keywords,
|
||||
onSelect: () => LeftAlignBlock.onSelect(editor),
|
||||
})
|
||||
}
|
||||
|
||||
export function GetCenterAlignBlockOption(editor: LexicalEditor) {
|
||||
return new BlockPickerOption(CenterAlignBlock.name, {
|
||||
iconName: CenterAlignBlock.iconName as LexicalIconName,
|
||||
keywords: CenterAlignBlock.keywords,
|
||||
onSelect: () => CenterAlignBlock.onSelect(editor),
|
||||
})
|
||||
}
|
||||
|
||||
export function GetRightAlignBlockOption(editor: LexicalEditor) {
|
||||
return new BlockPickerOption(RightAlignBlock.name, {
|
||||
iconName: RightAlignBlock.iconName as LexicalIconName,
|
||||
keywords: RightAlignBlock.keywords,
|
||||
onSelect: () => RightAlignBlock.onSelect(editor),
|
||||
})
|
||||
}
|
||||
|
||||
export function GetJustifyAlignBlockOption(editor: LexicalEditor) {
|
||||
return new BlockPickerOption(JustifyAlignBlock.name, {
|
||||
iconName: JustifyAlignBlock.iconName as LexicalIconName,
|
||||
keywords: JustifyAlignBlock.keywords,
|
||||
onSelect: () => JustifyAlignBlock.onSelect(editor),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { INSERT_UNORDERED_LIST_COMMAND } from '@lexical/list'
|
||||
|
||||
export function GetBulletedListBlock(editor: LexicalEditor, isActive = false) {
|
||||
return {
|
||||
name: 'Bulleted List',
|
||||
iconName: 'list-bulleted',
|
||||
keywords: ['bulleted list', 'unordered list', 'ul'],
|
||||
onSelect: () => editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined),
|
||||
active: isActive,
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { INSERT_CHECK_LIST_COMMAND } from '@lexical/list'
|
||||
|
||||
export function GetChecklistBlock(editor: LexicalEditor) {
|
||||
return {
|
||||
name: 'Check List',
|
||||
iconName: 'check',
|
||||
keywords: ['check list', 'todo list'],
|
||||
onSelect: () => editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined),
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,33 @@
|
||||
import { $wrapNodes } from '@lexical/selection'
|
||||
import { $setBlocksType } from '@lexical/selection'
|
||||
import { $getSelection, $isRangeSelection, LexicalEditor } from 'lexical'
|
||||
import { $createCodeNode } from '@lexical/code'
|
||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||
import { BlockPickerOption } from '../BlockPickerPlugin/BlockPickerOption'
|
||||
|
||||
export function GetCodeBlock(editor: LexicalEditor) {
|
||||
return {
|
||||
name: 'Code',
|
||||
iconName: 'code' as LexicalIconName,
|
||||
keywords: ['javascript', 'python', 'js', 'codeblock'],
|
||||
onSelect: () =>
|
||||
editor.update(() => {
|
||||
const selection = $getSelection()
|
||||
if ($isRangeSelection(selection)) {
|
||||
if (selection.isCollapsed()) {
|
||||
$wrapNodes(selection, () => $createCodeNode())
|
||||
} else {
|
||||
const textContent = selection.getTextContent()
|
||||
const codeNode = $createCodeNode()
|
||||
selection.insertNodes([codeNode])
|
||||
selection.insertRawText(textContent)
|
||||
}
|
||||
export const CodeBlock = {
|
||||
name: 'Code Block',
|
||||
iconName: 'code' as LexicalIconName,
|
||||
keywords: ['javascript', 'python', 'js', 'codeblock'],
|
||||
onSelect: (editor: LexicalEditor) =>
|
||||
editor.update(() => {
|
||||
const selection = $getSelection()
|
||||
if ($isRangeSelection(selection)) {
|
||||
if (selection.isCollapsed()) {
|
||||
$setBlocksType(selection, () => $createCodeNode())
|
||||
} else {
|
||||
const textContent = selection.getTextContent()
|
||||
const codeNode = $createCodeNode()
|
||||
selection.insertNodes([codeNode])
|
||||
selection.insertRawText(textContent)
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
export function GetCodeBlockOption(editor: LexicalEditor) {
|
||||
return new BlockPickerOption(CodeBlock.name, {
|
||||
iconName: CodeBlock.iconName,
|
||||
keywords: CodeBlock.keywords,
|
||||
onSelect: () => CodeBlock.onSelect(editor),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { INSERT_COLLAPSIBLE_COMMAND } from '../../Plugins/CollapsiblePlugin'
|
||||
import { IconType } from '@standardnotes/snjs'
|
||||
import { BlockPickerOption } from '../BlockPickerPlugin/BlockPickerOption'
|
||||
|
||||
export function GetCollapsibleBlock(editor: LexicalEditor) {
|
||||
return {
|
||||
name: 'Collapsible',
|
||||
iconName: 'caret-right' as IconType,
|
||||
keywords: ['collapse', 'collapsible', 'toggle'],
|
||||
onSelect: () => editor.dispatchCommand(INSERT_COLLAPSIBLE_COMMAND, undefined),
|
||||
}
|
||||
export const CollapsibleBlock = {
|
||||
name: 'Collapsible',
|
||||
iconName: 'details-block' as IconType,
|
||||
keywords: ['collapse', 'collapsible', 'toggle'],
|
||||
onSelect: (editor: LexicalEditor) => editor.dispatchCommand(INSERT_COLLAPSIBLE_COMMAND, undefined),
|
||||
}
|
||||
|
||||
export function GetCollapsibleBlockOption(editor: LexicalEditor) {
|
||||
return new BlockPickerOption(CollapsibleBlock.name, {
|
||||
iconName: CollapsibleBlock.iconName,
|
||||
keywords: CollapsibleBlock.keywords,
|
||||
onSelect: () => CollapsibleBlock.onSelect(editor),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { BlockPickerOption } from '../BlockPickerPlugin/BlockPickerOption'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||
import { INSERT_DATETIME_COMMAND, INSERT_DATE_COMMAND, INSERT_TIME_COMMAND } from '../Commands'
|
||||
|
||||
export function GetDatetimeBlocks(editor: LexicalEditor) {
|
||||
@@ -23,3 +25,14 @@ export function GetDatetimeBlocks(editor: LexicalEditor) {
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export function GetDatetimeBlockOptions(editor: LexicalEditor) {
|
||||
return GetDatetimeBlocks(editor).map(
|
||||
(block) =>
|
||||
new BlockPickerOption(block.name, {
|
||||
iconName: block.iconName as LexicalIconName,
|
||||
keywords: block.keywords,
|
||||
onSelect: block.onSelect,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/react/LexicalHorizontalRuleNode'
|
||||
import { BlockPickerOption } from '../BlockPickerPlugin/BlockPickerOption'
|
||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||
|
||||
export function GetDividerBlock(editor: LexicalEditor) {
|
||||
return {
|
||||
name: 'Divider',
|
||||
iconName: 'horizontal-rule',
|
||||
keywords: ['horizontal rule', 'divider', 'hr'],
|
||||
onSelect: () => editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined),
|
||||
}
|
||||
export const DividerBlock = {
|
||||
name: 'Divider',
|
||||
iconName: 'horizontal-rule',
|
||||
keywords: ['horizontal rule', 'divider', 'hr'],
|
||||
onSelect: (editor: LexicalEditor) => editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined),
|
||||
}
|
||||
|
||||
export function GetDividerBlockOption(editor: LexicalEditor) {
|
||||
return new BlockPickerOption(DividerBlock.name, {
|
||||
iconName: DividerBlock.iconName as LexicalIconName,
|
||||
keywords: DividerBlock.keywords,
|
||||
onSelect: () => DividerBlock.onSelect(editor),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { LexicalEditor } from 'lexical'
|
||||
import { INSERT_EMBED_COMMAND } from '@lexical/react/LexicalAutoEmbedPlugin'
|
||||
import { EmbedConfigs } from '../AutoEmbedPlugin'
|
||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||
import { BlockPickerOption } from '../BlockPickerPlugin/BlockPickerOption'
|
||||
|
||||
export function GetEmbedsBlocks(editor: LexicalEditor) {
|
||||
return EmbedConfigs.map((embedConfig) => ({
|
||||
@@ -11,3 +12,14 @@ export function GetEmbedsBlocks(editor: LexicalEditor) {
|
||||
onSelect: () => editor.dispatchCommand(INSERT_EMBED_COMMAND, embedConfig.type),
|
||||
}))
|
||||
}
|
||||
|
||||
export function GetEmbedsBlockOptions(editor: LexicalEditor) {
|
||||
return GetEmbedsBlocks(editor).map(
|
||||
(block) =>
|
||||
new BlockPickerOption(block.name, {
|
||||
iconName: block.iconName as LexicalIconName,
|
||||
keywords: block.keywords,
|
||||
onSelect: block.onSelect,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,19 +1,68 @@
|
||||
import { $wrapNodes } from '@lexical/selection'
|
||||
import { $setBlocksType } from '@lexical/selection'
|
||||
import { $getSelection, $isRangeSelection, LexicalEditor } from 'lexical'
|
||||
import { $createHeadingNode, HeadingTagType } from '@lexical/rich-text'
|
||||
import { $createHeadingNode } from '@lexical/rich-text'
|
||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||
import { BlockPickerOption } from '../BlockPickerPlugin/BlockPickerOption'
|
||||
|
||||
export function GetHeadingsBlocks(editor: LexicalEditor) {
|
||||
return Array.from({ length: 3 }, (_, i) => i + 1).map((n) => ({
|
||||
name: `Heading ${n}`,
|
||||
iconName: `h${n}` as LexicalIconName,
|
||||
keywords: ['heading', 'header', `h${n}`],
|
||||
onSelect: () =>
|
||||
editor.update(() => {
|
||||
const selection = $getSelection()
|
||||
if ($isRangeSelection(selection)) {
|
||||
$wrapNodes(selection, () => $createHeadingNode(`h${n}` as HeadingTagType))
|
||||
}
|
||||
}),
|
||||
}))
|
||||
export const H1Block = {
|
||||
name: 'Heading 1',
|
||||
iconName: 'h1',
|
||||
keywords: ['heading', 'header', 'h1'],
|
||||
onSelect: (editor: LexicalEditor) =>
|
||||
editor.update(() => {
|
||||
const selection = $getSelection()
|
||||
if ($isRangeSelection(selection)) {
|
||||
$setBlocksType(selection, () => $createHeadingNode('h1'))
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
export function GetH1BlockOption(editor: LexicalEditor) {
|
||||
return new BlockPickerOption(H1Block.name, {
|
||||
iconName: H1Block.iconName as LexicalIconName,
|
||||
keywords: H1Block.keywords,
|
||||
onSelect: () => H1Block.onSelect(editor),
|
||||
})
|
||||
}
|
||||
|
||||
export const H2Block = {
|
||||
name: 'Heading 2',
|
||||
iconName: 'h2',
|
||||
keywords: ['heading', 'header', 'h2'],
|
||||
onSelect: (editor: LexicalEditor) =>
|
||||
editor.update(() => {
|
||||
const selection = $getSelection()
|
||||
if ($isRangeSelection(selection)) {
|
||||
$setBlocksType(selection, () => $createHeadingNode('h2'))
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
export function GetH2BlockOption(editor: LexicalEditor) {
|
||||
return new BlockPickerOption(H2Block.name, {
|
||||
iconName: H2Block.iconName as LexicalIconName,
|
||||
keywords: H2Block.keywords,
|
||||
onSelect: () => H2Block.onSelect(editor),
|
||||
})
|
||||
}
|
||||
|
||||
export const H3Block = {
|
||||
name: 'Heading 3',
|
||||
iconName: 'h3',
|
||||
keywords: ['heading', 'header', 'h3'],
|
||||
onSelect: (editor: LexicalEditor) =>
|
||||
editor.update(() => {
|
||||
const selection = $getSelection()
|
||||
if ($isRangeSelection(selection)) {
|
||||
$setBlocksType(selection, () => $createHeadingNode('h3'))
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
export function GetH3BlockOption(editor: LexicalEditor) {
|
||||
return new BlockPickerOption(H3Block.name, {
|
||||
iconName: H3Block.iconName as LexicalIconName,
|
||||
keywords: H3Block.keywords,
|
||||
onSelect: () => H3Block.onSelect(editor),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
import { INDENT_CONTENT_COMMAND, OUTDENT_CONTENT_COMMAND, LexicalEditor } from 'lexical'
|
||||
import { BlockPickerOption } from '../BlockPickerPlugin/BlockPickerOption'
|
||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||
|
||||
export function GetIndentOutdentBlocks(editor: LexicalEditor) {
|
||||
return [
|
||||
{
|
||||
name: 'Indent',
|
||||
iconName: 'indent',
|
||||
keywords: ['indent'],
|
||||
onSelect: () => editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined),
|
||||
},
|
||||
{
|
||||
name: 'Outdent',
|
||||
iconName: 'outdent',
|
||||
keywords: ['outdent'],
|
||||
onSelect: () => editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined),
|
||||
},
|
||||
]
|
||||
export const IndentBlock = {
|
||||
name: 'Indent',
|
||||
iconName: 'indent',
|
||||
keywords: ['indent'],
|
||||
onSelect: (editor: LexicalEditor) => editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined),
|
||||
}
|
||||
|
||||
export const OutdentBlock = {
|
||||
name: 'Outdent',
|
||||
iconName: 'outdent',
|
||||
keywords: ['outdent'],
|
||||
onSelect: (editor: LexicalEditor) => editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined),
|
||||
}
|
||||
|
||||
export function GetIndentBlockOption(editor: LexicalEditor) {
|
||||
return new BlockPickerOption(IndentBlock.name, {
|
||||
iconName: IndentBlock.iconName as LexicalIconName,
|
||||
keywords: IndentBlock.keywords,
|
||||
onSelect: () => IndentBlock.onSelect(editor),
|
||||
})
|
||||
}
|
||||
|
||||
export function GetOutdentBlockOption(editor: LexicalEditor) {
|
||||
return new BlockPickerOption(OutdentBlock.name, {
|
||||
iconName: OutdentBlock.iconName as LexicalIconName,
|
||||
keywords: OutdentBlock.keywords,
|
||||
onSelect: () => OutdentBlock.onSelect(editor),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { INSERT_UNORDERED_LIST_COMMAND, INSERT_CHECK_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND } from '@lexical/list'
|
||||
import { BlockPickerOption } from '../BlockPickerPlugin/BlockPickerOption'
|
||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||
|
||||
export const BulletedListBlock = {
|
||||
name: 'Bulleted List',
|
||||
iconName: 'list-bulleted' as LexicalIconName,
|
||||
keywords: ['bulleted list', 'unordered list', 'ul'],
|
||||
onSelect: (editor: LexicalEditor) => editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined),
|
||||
}
|
||||
|
||||
export const ChecklistBlock = {
|
||||
name: 'Check List',
|
||||
iconName: 'check' as LexicalIconName,
|
||||
keywords: ['check list', 'todo list'],
|
||||
onSelect: (editor: LexicalEditor) => editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined),
|
||||
}
|
||||
|
||||
export const NumberedListBlock = {
|
||||
name: 'Numbered List',
|
||||
iconName: 'list-numbered' as LexicalIconName,
|
||||
keywords: ['numbered list', 'ordered list', 'ol'],
|
||||
onSelect: (editor: LexicalEditor) => editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined),
|
||||
}
|
||||
|
||||
export function GetBulletedListBlockOption(editor: LexicalEditor) {
|
||||
return new BlockPickerOption(BulletedListBlock.name, {
|
||||
iconName: BulletedListBlock.iconName,
|
||||
keywords: BulletedListBlock.keywords,
|
||||
onSelect: () => BulletedListBlock.onSelect(editor),
|
||||
})
|
||||
}
|
||||
|
||||
export function GetChecklistBlockOption(editor: LexicalEditor) {
|
||||
return new BlockPickerOption(ChecklistBlock.name, {
|
||||
iconName: ChecklistBlock.iconName,
|
||||
keywords: ChecklistBlock.keywords,
|
||||
onSelect: () => ChecklistBlock.onSelect(editor),
|
||||
})
|
||||
}
|
||||
|
||||
export function GetNumberedListBlockOption(editor: LexicalEditor) {
|
||||
return new BlockPickerOption(NumberedListBlock.name, {
|
||||
iconName: NumberedListBlock.iconName,
|
||||
keywords: NumberedListBlock.keywords,
|
||||
onSelect: () => NumberedListBlock.onSelect(editor),
|
||||
})
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { INSERT_ORDERED_LIST_COMMAND } from '@lexical/list'
|
||||
|
||||
export function GetNumberedListBlock(editor: LexicalEditor, isActive = false) {
|
||||
return {
|
||||
name: 'Numbered List',
|
||||
iconName: 'list-numbered',
|
||||
keywords: ['numbered list', 'ordered list', 'ol'],
|
||||
onSelect: () => editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined),
|
||||
active: isActive,
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,25 @@
|
||||
import { $wrapNodes } from '@lexical/selection'
|
||||
import { $setBlocksType } from '@lexical/selection'
|
||||
import { $createParagraphNode, $getSelection, $isRangeSelection, LexicalEditor } from 'lexical'
|
||||
import { BlockPickerOption } from '../BlockPickerPlugin/BlockPickerOption'
|
||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||
|
||||
export function GetParagraphBlock(editor: LexicalEditor) {
|
||||
return {
|
||||
name: 'Paragraph',
|
||||
iconName: 'paragraph',
|
||||
keywords: ['normal', 'paragraph', 'p', 'text'],
|
||||
onSelect: () =>
|
||||
editor.update(() => {
|
||||
const selection = $getSelection()
|
||||
if ($isRangeSelection(selection)) {
|
||||
$wrapNodes(selection, () => $createParagraphNode())
|
||||
}
|
||||
}),
|
||||
}
|
||||
export const ParagraphBlock = {
|
||||
name: 'Paragraph',
|
||||
iconName: 'paragraph',
|
||||
keywords: ['normal', 'paragraph', 'p', 'text'],
|
||||
onSelect: (editor: LexicalEditor) =>
|
||||
editor.update(() => {
|
||||
const selection = $getSelection()
|
||||
if ($isRangeSelection(selection)) {
|
||||
$setBlocksType(selection, () => $createParagraphNode())
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
export function GetParagraphBlockOption(editor: LexicalEditor) {
|
||||
return new BlockPickerOption(ParagraphBlock.name, {
|
||||
iconName: ParagraphBlock.iconName as LexicalIconName,
|
||||
keywords: ParagraphBlock.keywords,
|
||||
onSelect: () => ParagraphBlock.onSelect(editor),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,13 +1,48 @@
|
||||
import { BlockPickerOption } from '../BlockPickerPlugin/BlockPickerOption'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { INSERT_PASSWORD_COMMAND } from '../Commands'
|
||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||
|
||||
const MIN_PASSWORD_LENGTH = 8
|
||||
const DEFAULT_PASSWORD_LENGTH = 16
|
||||
|
||||
export function GetPasswordBlock(editor: LexicalEditor) {
|
||||
return {
|
||||
name: 'Generate cryptographically secure password',
|
||||
iconName: 'password',
|
||||
keywords: ['password', 'secure'],
|
||||
onSelect: () => editor.dispatchCommand(INSERT_PASSWORD_COMMAND, String(DEFAULT_PASSWORD_LENGTH)),
|
||||
}
|
||||
export const PasswordBlock = {
|
||||
name: 'Generate cryptographically secure password',
|
||||
iconName: 'password',
|
||||
keywords: ['password', 'secure'],
|
||||
onSelect: (editor: LexicalEditor) => editor.dispatchCommand(INSERT_PASSWORD_COMMAND, String(DEFAULT_PASSWORD_LENGTH)),
|
||||
}
|
||||
|
||||
export function GetPasswordBlockOption(editor: LexicalEditor) {
|
||||
return new BlockPickerOption(PasswordBlock.name, {
|
||||
iconName: PasswordBlock.iconName as LexicalIconName,
|
||||
keywords: PasswordBlock.keywords,
|
||||
onSelect: () => PasswordBlock.onSelect(editor),
|
||||
})
|
||||
}
|
||||
|
||||
export function GetDynamicPasswordBlocks(editor: LexicalEditor, queryString: string) {
|
||||
if (queryString == null) {
|
||||
return []
|
||||
}
|
||||
|
||||
const lengthRegex = /^\d+$/
|
||||
const match = lengthRegex.exec(queryString)
|
||||
|
||||
if (!match) {
|
||||
return []
|
||||
}
|
||||
|
||||
const length = parseInt(match[0], 10)
|
||||
if (length < MIN_PASSWORD_LENGTH) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [
|
||||
new BlockPickerOption(`Generate ${length}-character cryptographically secure password`, {
|
||||
iconName: 'password',
|
||||
keywords: ['password', 'secure'],
|
||||
onSelect: () => editor.dispatchCommand(INSERT_PASSWORD_COMMAND, length.toString()),
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
import { $wrapNodes } from '@lexical/selection'
|
||||
import { $setBlocksType } from '@lexical/selection'
|
||||
import { $getSelection, $isRangeSelection, LexicalEditor } from 'lexical'
|
||||
import { $createQuoteNode } from '@lexical/rich-text'
|
||||
import { BlockPickerOption } from '../BlockPickerPlugin/BlockPickerOption'
|
||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||
|
||||
export function GetQuoteBlock(editor: LexicalEditor) {
|
||||
return {
|
||||
name: 'Quote',
|
||||
iconName: 'quote',
|
||||
keywords: ['block quote'],
|
||||
onSelect: () =>
|
||||
editor.update(() => {
|
||||
const selection = $getSelection()
|
||||
if ($isRangeSelection(selection)) {
|
||||
$wrapNodes(selection, () => $createQuoteNode())
|
||||
}
|
||||
}),
|
||||
}
|
||||
export const QuoteBlock = {
|
||||
name: 'Quote',
|
||||
iconName: 'quote' as LexicalIconName,
|
||||
keywords: ['block quote'],
|
||||
onSelect: (editor: LexicalEditor) =>
|
||||
editor.update(() => {
|
||||
const selection = $getSelection()
|
||||
if ($isRangeSelection(selection)) {
|
||||
$setBlocksType(selection, () => $createQuoteNode())
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
export function GetQuoteBlockOption(editor: LexicalEditor) {
|
||||
return new BlockPickerOption(QuoteBlock.name, {
|
||||
iconName: QuoteBlock.iconName,
|
||||
keywords: QuoteBlock.keywords,
|
||||
onSelect: () => QuoteBlock.onSelect(editor),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
export function GetRemoteImageBlock(onSelect: () => void) {
|
||||
return {
|
||||
name: 'Image from URL',
|
||||
iconName: 'image',
|
||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||
import { BlockPickerOption } from '../BlockPickerPlugin/BlockPickerOption'
|
||||
|
||||
export function GetRemoteImageBlockOption(onSelect: () => void) {
|
||||
return new BlockPickerOption('Image from URL', {
|
||||
iconName: 'image' as LexicalIconName,
|
||||
keywords: ['image', 'url'],
|
||||
onSelect,
|
||||
}
|
||||
onSelect: onSelect,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,3 +1,54 @@
|
||||
export function GetTableBlock(onSelect: () => void) {
|
||||
return { name: 'Table', iconName: 'table', keywords: ['table', 'grid', 'spreadsheet', 'rows', 'columns'], onSelect }
|
||||
import { BlockPickerOption } from '../BlockPickerPlugin/BlockPickerOption'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { INSERT_TABLE_COMMAND } from '@lexical/table'
|
||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||
|
||||
export function GetTableBlockOption(onSelect: () => void) {
|
||||
return new BlockPickerOption('Table', {
|
||||
iconName: 'table' as LexicalIconName,
|
||||
keywords: ['table', 'grid', 'spreadsheet', 'rows', 'columns'],
|
||||
onSelect: onSelect,
|
||||
})
|
||||
}
|
||||
|
||||
export function GetDynamicTableBlocks(editor: LexicalEditor, queryString: string) {
|
||||
const options: Array<BlockPickerOption> = []
|
||||
|
||||
if (queryString == null) {
|
||||
return options
|
||||
}
|
||||
|
||||
const fullTableRegex = new RegExp(/^([1-9]|10)x([1-9]|10)$/)
|
||||
const partialTableRegex = new RegExp(/^([1-9]|10)x?$/)
|
||||
|
||||
const fullTableMatch = fullTableRegex.exec(queryString)
|
||||
const partialTableMatch = partialTableRegex.exec(queryString)
|
||||
|
||||
if (fullTableMatch) {
|
||||
const [rows, columns] = fullTableMatch[0].split('x').map((n: string) => parseInt(n, 10))
|
||||
|
||||
options.push(
|
||||
new BlockPickerOption(`${rows}x${columns} Table`, {
|
||||
iconName: 'table',
|
||||
keywords: ['table'],
|
||||
onSelect: () => editor.dispatchCommand(INSERT_TABLE_COMMAND, { columns: String(columns), rows: String(rows) }),
|
||||
}),
|
||||
)
|
||||
} else if (partialTableMatch) {
|
||||
const rows = parseInt(partialTableMatch[0], 10)
|
||||
|
||||
options.push(
|
||||
...Array.from({ length: 5 }, (_, i) => i + 1).map(
|
||||
(columns) =>
|
||||
new BlockPickerOption(`${rows}x${columns} Table`, {
|
||||
iconName: 'table',
|
||||
keywords: ['table'],
|
||||
onSelect: () =>
|
||||
editor.dispatchCommand(INSERT_TABLE_COMMAND, { columns: String(columns), rows: String(rows) }),
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { KeyboardKey } from '@standardnotes/ui-services'
|
||||
import { sanitizeUrl } from '../../Lexical/Utils/sanitizeUrl'
|
||||
import { TOGGLE_LINK_COMMAND } from '@lexical/link'
|
||||
import { useCallback, useState, useRef, useEffect } from 'react'
|
||||
import { GridSelection, LexicalEditor, NodeSelection, RangeSelection } from 'lexical'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip'
|
||||
|
||||
@@ -12,21 +12,18 @@ type Props = {
|
||||
isEditMode: boolean
|
||||
setEditMode: (isEditMode: boolean) => void
|
||||
editor: LexicalEditor
|
||||
lastSelection: RangeSelection | GridSelection | NodeSelection | null
|
||||
isAutoLink: boolean
|
||||
}
|
||||
|
||||
const LinkEditor = ({ linkUrl, isEditMode, setEditMode, editor, lastSelection, isAutoLink }: Props) => {
|
||||
const LinkEditor = ({ linkUrl, isEditMode, setEditMode, editor, isAutoLink }: Props) => {
|
||||
const [editedLinkUrl, setEditedLinkUrl] = useState('')
|
||||
const editModeContainer = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleLinkSubmission = () => {
|
||||
if (lastSelection !== null) {
|
||||
if (editedLinkUrl !== '') {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl(editedLinkUrl))
|
||||
}
|
||||
setEditMode(false)
|
||||
if (editedLinkUrl !== '') {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl(editedLinkUrl))
|
||||
}
|
||||
setEditMode(false)
|
||||
}
|
||||
|
||||
const focusInput = useCallback((input: HTMLInputElement | null) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||
import { $isRangeSelection, $isTextNode, GridSelection, LexicalEditor, NodeSelection, RangeSelection } from 'lexical'
|
||||
import { $getSelection, $isRangeSelection, $isTextNode, LexicalEditor, RangeSelection } from 'lexical'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { VisuallyHidden } from '@ariakit/react'
|
||||
import { getSelectedNode } from '../../Lexical/Utils/getSelectedNode'
|
||||
@@ -10,7 +10,6 @@ import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip'
|
||||
type Props = {
|
||||
linkText: string
|
||||
editor: LexicalEditor
|
||||
lastSelection: RangeSelection | GridSelection | NodeSelection | null
|
||||
isEditMode: boolean
|
||||
setEditMode: (isEditMode: boolean) => void
|
||||
}
|
||||
@@ -20,7 +19,7 @@ export const $isLinkTextNode = (node: ReturnType<typeof getSelectedNode>, select
|
||||
return $isLinkNode(parent) && $isTextNode(node) && selection.anchor.getNode() === selection.focus.getNode()
|
||||
}
|
||||
|
||||
const LinkTextEditor = ({ linkText, editor, isEditMode, setEditMode, lastSelection }: Props) => {
|
||||
const LinkTextEditor = ({ linkText, editor, isEditMode, setEditMode }: Props) => {
|
||||
const [editedLinkText, setEditedLinkText] = useState(() => linkText)
|
||||
const editModeContainer = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -36,13 +35,15 @@ const LinkTextEditor = ({ linkText, editor, isEditMode, setEditMode, lastSelecti
|
||||
|
||||
const handleLinkTextSubmission = () => {
|
||||
editor.update(() => {
|
||||
if ($isRangeSelection(lastSelection)) {
|
||||
const node = getSelectedNode(lastSelection)
|
||||
if (!$isLinkTextNode(node, lastSelection)) {
|
||||
return
|
||||
}
|
||||
node.setTextContent(editedLinkText)
|
||||
const selection = $getSelection()
|
||||
if (!$isRangeSelection(selection)) {
|
||||
return
|
||||
}
|
||||
const node = getSelectedNode(selection)
|
||||
if (!$isLinkTextNode(node, selection)) {
|
||||
return
|
||||
}
|
||||
node.setTextContent(editedLinkText)
|
||||
})
|
||||
setEditMode(false)
|
||||
}
|
||||
|
||||
@@ -9,51 +9,61 @@ import {
|
||||
CAN_REDO_COMMAND,
|
||||
CAN_UNDO_COMMAND,
|
||||
COMMAND_PRIORITY_CRITICAL,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
COMMAND_PRIORITY_NORMAL,
|
||||
FORMAT_TEXT_COMMAND,
|
||||
GridSelection,
|
||||
KEY_MODIFIER_COMMAND,
|
||||
NodeSelection,
|
||||
REDO_COMMAND,
|
||||
RangeSelection,
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
UNDO_COMMAND,
|
||||
createCommand,
|
||||
$isRootOrShadowRoot,
|
||||
ElementFormatType,
|
||||
$isElementNode,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
} from 'lexical'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'
|
||||
import { ComponentPropsWithoutRef, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { GetAlignmentBlocks } from '../Blocks/Alignment'
|
||||
import { GetBulletedListBlock } from '../Blocks/BulletedList'
|
||||
import { GetChecklistBlock } from '../Blocks/Checklist'
|
||||
import { GetCodeBlock } from '../Blocks/Code'
|
||||
import { GetCollapsibleBlock } from '../Blocks/Collapsible'
|
||||
import { GetDatetimeBlocks } from '../Blocks/DateTime'
|
||||
import { GetDividerBlock } from '../Blocks/Divider'
|
||||
import { GetEmbedsBlocks } from '../Blocks/Embeds'
|
||||
import { GetHeadingsBlocks } from '../Blocks/Headings'
|
||||
import { GetIndentOutdentBlocks } from '../Blocks/IndentOutdent'
|
||||
import { GetNumberedListBlock } from '../Blocks/NumberedList'
|
||||
import { GetParagraphBlock } from '../Blocks/Paragraph'
|
||||
import { GetPasswordBlock } from '../Blocks/Password'
|
||||
import { GetQuoteBlock } from '../Blocks/Quote'
|
||||
import { GetTableBlock } from '../Blocks/Table'
|
||||
import { mergeRegister, $findMatchingParent, $getNearestNodeOfType } from '@lexical/utils'
|
||||
import { $isLinkNode, $isAutoLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'
|
||||
import { $isListNode, ListNode } from '@lexical/list'
|
||||
import { $isHeadingNode } from '@lexical/rich-text'
|
||||
import { ComponentPropsWithoutRef, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { CenterAlignBlock, JustifyAlignBlock, LeftAlignBlock, RightAlignBlock } from '../Blocks/Alignment'
|
||||
import { BulletedListBlock, ChecklistBlock, NumberedListBlock } from '../Blocks/List'
|
||||
import { CodeBlock } from '../Blocks/Code'
|
||||
import { CollapsibleBlock } from '../Blocks/Collapsible'
|
||||
import { DividerBlock } from '../Blocks/Divider'
|
||||
import { H1Block, H2Block, H3Block } from '../Blocks/Headings'
|
||||
import { IndentBlock, OutdentBlock } from '../Blocks/IndentOutdent'
|
||||
import { ParagraphBlock } from '../Blocks/Paragraph'
|
||||
import { QuoteBlock } from '../Blocks/Quote'
|
||||
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import { SUPER_TOGGLE_SEARCH, SUPER_TOGGLE_TOOLBAR } from '@standardnotes/ui-services'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
import { GetRemoteImageBlock } from '../Blocks/RemoteImage'
|
||||
import { InsertRemoteImageDialog } from '../RemoteImagePlugin/RemoteImagePlugin'
|
||||
import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip'
|
||||
import { Toolbar, ToolbarItem, useToolbarStore } from '@ariakit/react'
|
||||
import { PasswordBlock } from '../Blocks/Password'
|
||||
import LinkEditor from './ToolbarLinkEditor'
|
||||
import { FOCUSABLE_BUT_NOT_TABBABLE, URL_REGEX } from '@/Constants/Constants'
|
||||
import { useSelectedTextFormatInfo } from './useSelectedTextFormatInfo'
|
||||
import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip'
|
||||
import LinkTextEditor, { $isLinkTextNode } from './ToolbarLinkTextEditor'
|
||||
import { Toolbar, ToolbarItem, useToolbarStore } from '@ariakit/react'
|
||||
|
||||
const TOGGLE_LINK_AND_EDIT_COMMAND = createCommand<string | null>('TOGGLE_LINK_AND_EDIT_COMMAND')
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
interface ToolbarButtonProps extends ComponentPropsWithoutRef<'button'> {
|
||||
name: string
|
||||
active?: boolean
|
||||
@@ -65,7 +75,7 @@ const ToolbarButton = ({ name, active, iconName, onSelect, disabled, ...props }:
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
return (
|
||||
<StyledTooltip showOnMobile showOnHover label={name}>
|
||||
<StyledTooltip showOnMobile showOnHover label={name} side="top">
|
||||
<ToolbarItem
|
||||
className="flex select-none items-center justify-center rounded p-0.5 focus:shadow-none focus:outline-none enabled:hover:bg-default enabled:focus-visible:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||
onMouseDown={(event) => {
|
||||
@@ -94,54 +104,124 @@ const ToolbarButton = ({ name, active, iconName, onSelect, disabled, ...props }:
|
||||
|
||||
const ToolbarPlugin = () => {
|
||||
const application = useApplication()
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const [modal, showModal] = useModal()
|
||||
|
||||
const [isInEditor, setIsInEditor] = useState(false)
|
||||
const [isInToolbar, setIsInToolbar] = useState(false)
|
||||
const isMobile = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const toolbarRef = useRef<HTMLDivElement>(null)
|
||||
const linkEditorRef = useRef<HTMLDivElement>(null)
|
||||
const backspaceButtonRef = useRef<HTMLButtonElement>(null)
|
||||
const [modal, showModal] = useModal()
|
||||
|
||||
const insertLink = useCallback(() => {
|
||||
const selection = $getSelection()
|
||||
if (!$isRangeSelection(selection)) {
|
||||
return
|
||||
}
|
||||
const node = getSelectedNode(selection)
|
||||
const parent = node.getParent()
|
||||
const isLink = $isLinkNode(parent) || $isLinkNode(node)
|
||||
if (!isLink) {
|
||||
editor.update(() => {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, 'https://')
|
||||
})
|
||||
} else {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
|
||||
}
|
||||
}, [editor])
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const [activeEditor, setActiveEditor] = useState(editor)
|
||||
|
||||
const {
|
||||
isBold,
|
||||
isItalic,
|
||||
isUnderline,
|
||||
isSubscript,
|
||||
isSuperscript,
|
||||
isStrikethrough,
|
||||
blockType,
|
||||
isHighlighted,
|
||||
isLink,
|
||||
isLinkText,
|
||||
isAutoLink,
|
||||
} = useSelectedTextFormatInfo()
|
||||
const [blockType, setBlockType] = useState<keyof typeof blockTypeToBlockName>('paragraph')
|
||||
const [elementFormat, setElementFormat] = useState<ElementFormatType>('left')
|
||||
|
||||
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 [isHighlight, setIsHighlight] = useState(false)
|
||||
|
||||
const [isLink, setIsLink] = useState(false)
|
||||
const [isAutoLink, setIsAutoLink] = useState(false)
|
||||
const [isLinkText, setIsLinkText] = useState(false)
|
||||
const [isLinkEditMode, setIsLinkEditMode] = useState(false)
|
||||
const [isLinkTextEditMode, setIsLinkTextEditMode] = useState(false)
|
||||
const [linkText, setLinkText] = useState<string>('')
|
||||
const [linkUrl, setLinkUrl] = useState<string>('')
|
||||
|
||||
const [canUndo, setCanUndo] = useState(false)
|
||||
const [canRedo, setCanRedo] = useState(false)
|
||||
|
||||
const $updateToolbar = useCallback(() => {
|
||||
const selection = $getSelection()
|
||||
if ($isRangeSelection(selection)) {
|
||||
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)
|
||||
|
||||
// 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'))
|
||||
setIsHighlight(selection.hasFormat('highlight'))
|
||||
|
||||
// Update links
|
||||
const node = getSelectedNode(selection)
|
||||
const parent = node.getParent()
|
||||
if ($isLinkNode(parent) || $isLinkNode(node)) {
|
||||
setIsLink(true)
|
||||
} else {
|
||||
setIsLink(false)
|
||||
}
|
||||
setLinkUrl($isLinkNode(parent) ? parent.getURL() : $isLinkNode(node) ? node.getURL() : '')
|
||||
if ($isAutoLinkNode(parent) || $isAutoLinkNode(node)) {
|
||||
setIsAutoLink(true)
|
||||
} else {
|
||||
setIsAutoLink(false)
|
||||
}
|
||||
if ($isLinkTextNode(node, selection)) {
|
||||
setIsLinkText(true)
|
||||
setLinkText(node.getTextContent())
|
||||
} else {
|
||||
setIsLinkText(false)
|
||||
setLinkText('')
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setElementFormat(($isElementNode(node) ? node.getFormatType() : parent?.getFormatType()) || 'left')
|
||||
}
|
||||
}, [activeEditor])
|
||||
|
||||
useEffect(() => {
|
||||
return editor.registerCommand(
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
(_payload, newEditor) => {
|
||||
$updateToolbar()
|
||||
setActiveEditor(newEditor)
|
||||
return false
|
||||
},
|
||||
COMMAND_PRIORITY_CRITICAL,
|
||||
)
|
||||
}, [editor, $updateToolbar])
|
||||
|
||||
useEffect(() => {
|
||||
return mergeRegister(
|
||||
editor.registerCommand<boolean>(
|
||||
activeEditor.registerUpdateListener(({ editorState }) => {
|
||||
editorState.read(() => {
|
||||
$updateToolbar()
|
||||
})
|
||||
}),
|
||||
activeEditor.registerCommand<boolean>(
|
||||
CAN_UNDO_COMMAND,
|
||||
(payload) => {
|
||||
setCanUndo(payload)
|
||||
@@ -149,7 +229,7 @@ const ToolbarPlugin = () => {
|
||||
},
|
||||
COMMAND_PRIORITY_CRITICAL,
|
||||
),
|
||||
editor.registerCommand<boolean>(
|
||||
activeEditor.registerCommand<boolean>(
|
||||
CAN_REDO_COMMAND,
|
||||
(payload) => {
|
||||
setCanRedo(payload)
|
||||
@@ -157,7 +237,7 @@ const ToolbarPlugin = () => {
|
||||
},
|
||||
COMMAND_PRIORITY_CRITICAL,
|
||||
),
|
||||
editor.registerCommand(
|
||||
activeEditor.registerCommand(
|
||||
TOGGLE_LINK_AND_EDIT_COMMAND,
|
||||
(payload) => {
|
||||
if (payload === null) {
|
||||
@@ -172,180 +252,54 @@ const ToolbarPlugin = () => {
|
||||
},
|
||||
COMMAND_PRIORITY_LOW,
|
||||
),
|
||||
editor.registerCommand(
|
||||
KEY_MODIFIER_COMMAND,
|
||||
(payload) => {
|
||||
const event: KeyboardEvent = payload
|
||||
const { code, ctrlKey, metaKey } = event
|
||||
|
||||
if (code === 'KeyK' && (ctrlKey || metaKey)) {
|
||||
event.preventDefault()
|
||||
if ('readText' in navigator.clipboard) {
|
||||
navigator.clipboard
|
||||
.readText()
|
||||
.then((text) => {
|
||||
if (URL_REGEX.test(text)) {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, text)
|
||||
} else {
|
||||
throw new Error('Not a valid URL')
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
editor.dispatchCommand(TOGGLE_LINK_AND_EDIT_COMMAND, '')
|
||||
setIsLinkEditMode(true)
|
||||
})
|
||||
} else {
|
||||
editor.dispatchCommand(TOGGLE_LINK_AND_EDIT_COMMAND, '')
|
||||
setIsLinkEditMode(true)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
COMMAND_PRIORITY_NORMAL,
|
||||
),
|
||||
)
|
||||
}, [editor])
|
||||
}, [$updateToolbar, activeEditor, editor])
|
||||
|
||||
const items = useMemo(
|
||||
(): {
|
||||
name: string
|
||||
iconName: string
|
||||
keywords?: string[]
|
||||
active?: boolean
|
||||
disabled?: boolean
|
||||
onSelect: () => void
|
||||
}[] => [
|
||||
{
|
||||
name: 'Undo',
|
||||
iconName: 'undo',
|
||||
disabled: !canUndo,
|
||||
onSelect: () => {
|
||||
editor.dispatchCommand(UNDO_COMMAND, undefined)
|
||||
},
|
||||
useEffect(() => {
|
||||
return activeEditor.registerCommand(
|
||||
KEY_MODIFIER_COMMAND,
|
||||
(payload) => {
|
||||
const event: KeyboardEvent = payload
|
||||
const { code, ctrlKey, metaKey, shiftKey } = event
|
||||
|
||||
if (code === 'KeyK' && (ctrlKey || metaKey) && !shiftKey) {
|
||||
event.preventDefault()
|
||||
if ('readText' in navigator.clipboard) {
|
||||
navigator.clipboard
|
||||
.readText()
|
||||
.then((text) => {
|
||||
if (URL_REGEX.test(text)) {
|
||||
activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, text)
|
||||
} else {
|
||||
throw new Error('Not a valid URL')
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
activeEditor.dispatchCommand(TOGGLE_LINK_AND_EDIT_COMMAND, '')
|
||||
setIsLinkEditMode(true)
|
||||
})
|
||||
} else {
|
||||
activeEditor.dispatchCommand(TOGGLE_LINK_AND_EDIT_COMMAND, '')
|
||||
setIsLinkEditMode(true)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
{
|
||||
name: 'Redo',
|
||||
iconName: 'redo',
|
||||
disabled: !canRedo,
|
||||
onSelect: () => {
|
||||
editor.dispatchCommand(REDO_COMMAND, undefined)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Bold',
|
||||
iconName: 'bold',
|
||||
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: 'Highlight',
|
||||
iconName: 'draw',
|
||||
onSelect: () => {
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'highlight')
|
||||
},
|
||||
active: isHighlighted,
|
||||
},
|
||||
{
|
||||
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',
|
||||
iconName: 'link',
|
||||
onSelect: () => {
|
||||
editor.update(() => {
|
||||
insertLink()
|
||||
})
|
||||
},
|
||||
active: isLink,
|
||||
},
|
||||
{
|
||||
name: 'Search',
|
||||
iconName: 'search',
|
||||
onSelect: () => {
|
||||
application.keyboardService.triggerCommand(SUPER_TOGGLE_SEARCH)
|
||||
},
|
||||
},
|
||||
GetParagraphBlock(editor),
|
||||
...GetHeadingsBlocks(editor),
|
||||
...GetIndentOutdentBlocks(editor),
|
||||
...GetAlignmentBlocks(editor),
|
||||
GetTableBlock(() =>
|
||||
showModal('Insert Table', (onClose) => <InsertTableDialog activeEditor={editor} onClose={onClose} />),
|
||||
),
|
||||
GetRemoteImageBlock(() => {
|
||||
showModal('Insert image from URL', (onClose) => <InsertRemoteImageDialog onClose={onClose} />)
|
||||
}),
|
||||
GetNumberedListBlock(editor, blockType === 'number'),
|
||||
GetBulletedListBlock(editor, blockType === 'bullet'),
|
||||
GetChecklistBlock(editor),
|
||||
GetQuoteBlock(editor),
|
||||
GetCodeBlock(editor),
|
||||
GetDividerBlock(editor),
|
||||
...GetDatetimeBlocks(editor),
|
||||
...[GetPasswordBlock(editor)],
|
||||
GetCollapsibleBlock(editor),
|
||||
...GetEmbedsBlocks(editor),
|
||||
],
|
||||
[
|
||||
application.keyboardService,
|
||||
blockType,
|
||||
canRedo,
|
||||
canUndo,
|
||||
editor,
|
||||
insertLink,
|
||||
isBold,
|
||||
isHighlighted,
|
||||
isItalic,
|
||||
isLink,
|
||||
isStrikethrough,
|
||||
isSubscript,
|
||||
isSuperscript,
|
||||
isUnderline,
|
||||
showModal,
|
||||
],
|
||||
)
|
||||
COMMAND_PRIORITY_NORMAL,
|
||||
)
|
||||
}, [activeEditor, isLink])
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const dismissButtonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const [isFocusInEditor, setIsFocusInEditor] = useState(false)
|
||||
const [isFocusInToolbar, setIsFocusInToolbar] = useState(false)
|
||||
const isFocusInEditorOrToolbar = isFocusInEditor || isFocusInToolbar
|
||||
const [isToolbarVisible, setIsToolbarVisible] = useState(true)
|
||||
const canShowToolbar = isMobile ? isFocusInEditorOrToolbar : isToolbarVisible
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current
|
||||
@@ -355,28 +309,18 @@ const ToolbarPlugin = () => {
|
||||
return
|
||||
}
|
||||
|
||||
const handleToolbarFocus = () => setIsInToolbar(true)
|
||||
const handleToolbarBlur = (event: FocusEvent) => {
|
||||
const elementToBeFocused = event.relatedTarget as Node
|
||||
if (elementToBeFocused === backspaceButtonRef.current) {
|
||||
return
|
||||
}
|
||||
setIsInToolbar(false)
|
||||
}
|
||||
const handleToolbarFocus = () => setIsFocusInToolbar(true)
|
||||
const handleToolbarBlur = () => setIsFocusInToolbar(false)
|
||||
|
||||
const handleRootFocus = () => setIsInEditor(true)
|
||||
const handleRootFocus = () => setIsFocusInEditor(true)
|
||||
const handleRootBlur = (event: FocusEvent) => {
|
||||
const elementToBeFocused = event.relatedTarget as Node
|
||||
|
||||
const containerContainsElementToFocus = container?.contains(elementToBeFocused)
|
||||
|
||||
const willFocusBackspaceButton = backspaceButtonRef.current && elementToBeFocused === backspaceButtonRef.current
|
||||
|
||||
if (containerContainsElementToFocus || willFocusBackspaceButton) {
|
||||
const willFocusDismissButton = dismissButtonRef.current === elementToBeFocused
|
||||
if (containerContainsElementToFocus && !willFocusDismissButton) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsInEditor(false)
|
||||
setIsFocusInEditor(false)
|
||||
}
|
||||
|
||||
rootElement.addEventListener('focus', handleRootFocus)
|
||||
@@ -395,70 +339,33 @@ const ToolbarPlugin = () => {
|
||||
}
|
||||
}, [editor])
|
||||
|
||||
const [linkUrl, setLinkUrl] = useState('')
|
||||
const [linkText, setLinkText] = useState('')
|
||||
const [isLinkEditMode, setIsLinkEditMode] = useState(false)
|
||||
const [isLinkTextEditMode, setIsLinkTextEditMode] = 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)) {
|
||||
setLinkUrl(parent.getURL())
|
||||
} else if ($isLinkNode(node)) {
|
||||
setLinkUrl(node.getURL())
|
||||
} else {
|
||||
setLinkUrl('')
|
||||
}
|
||||
if ($isLinkTextNode(node, selection)) {
|
||||
setLinkText(node.getTextContent())
|
||||
} else {
|
||||
setLinkText('')
|
||||
}
|
||||
|
||||
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])
|
||||
|
||||
const toolbarRef = useRef<HTMLDivElement>(null)
|
||||
const toolbarStore = useToolbarStore()
|
||||
useEffect(() => {
|
||||
return mergeRegister(
|
||||
editor.registerCommand(
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
() => {
|
||||
updateEditorSelection()
|
||||
return false
|
||||
},
|
||||
COMMAND_PRIORITY_CRITICAL,
|
||||
),
|
||||
editor.registerUpdateListener(() => {
|
||||
updateEditorSelection()
|
||||
}),
|
||||
)
|
||||
}, [editor, updateEditorSelection])
|
||||
return application.keyboardService.addCommandHandler({
|
||||
command: SUPER_TOGGLE_TOOLBAR,
|
||||
onKeyDown(event) {
|
||||
if (isMobile) {
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
|
||||
if (!isToolbarVisible) {
|
||||
setIsToolbarVisible(true)
|
||||
toolbarStore.move(toolbarStore.first())
|
||||
return
|
||||
}
|
||||
|
||||
const isFocusInContainer = containerRef.current?.contains(document.activeElement)
|
||||
if (isFocusInContainer) {
|
||||
setIsToolbarVisible(false)
|
||||
editor.focus()
|
||||
} else {
|
||||
toolbarStore.move(toolbarStore.first())
|
||||
}
|
||||
},
|
||||
})
|
||||
}, [application.keyboardService, editor, isMobile, isToolbarVisible, toolbarStore])
|
||||
useEffect(() => {
|
||||
const container = containerRef.current
|
||||
const rootElement = editor.getRootElement()
|
||||
@@ -474,7 +381,7 @@ const ToolbarPlugin = () => {
|
||||
|
||||
const containerHeight = container.offsetHeight
|
||||
|
||||
rootElement.style.paddingBottom = containerHeight ? `${containerHeight + 16 * 2}px` : ''
|
||||
rootElement.style.paddingBottom = containerHeight ? `${containerHeight + 8}px` : ''
|
||||
})
|
||||
|
||||
resizeObserver.observe(container)
|
||||
@@ -484,47 +391,13 @@ const ToolbarPlugin = () => {
|
||||
}
|
||||
}, [editor, isMobile])
|
||||
|
||||
const isFocusInEditorOrToolbar = isInEditor || isInToolbar
|
||||
const [isToolbarVisible, setIsToolbarVisible] = useState(true)
|
||||
const canShowToolbar = isMobile ? isFocusInEditorOrToolbar : isToolbarVisible
|
||||
|
||||
const toolbarStore = useToolbarStore()
|
||||
|
||||
useEffect(() => {
|
||||
return application.keyboardService.addCommandHandler({
|
||||
command: SUPER_TOGGLE_TOOLBAR,
|
||||
onKeyDown: (event) => {
|
||||
if (isMobile) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
const isFocusInContainer = containerRef.current?.contains(document.activeElement)
|
||||
|
||||
if (!isToolbarVisible) {
|
||||
setIsToolbarVisible(true)
|
||||
toolbarStore.move(toolbarStore.first())
|
||||
return
|
||||
}
|
||||
|
||||
if (isFocusInContainer) {
|
||||
setIsToolbarVisible(false)
|
||||
editor.focus()
|
||||
} else {
|
||||
toolbarStore.move(toolbarStore.first())
|
||||
}
|
||||
},
|
||||
})
|
||||
}, [application.keyboardService, editor, isMobile, isToolbarVisible, toolbarStore])
|
||||
|
||||
return (
|
||||
<>
|
||||
{modal}
|
||||
<div
|
||||
className={classNames(
|
||||
'bg-contrast',
|
||||
'md:absolute md:bottom-4 md:left-1/2 md:max-w-[60%] md:-translate-x-1/2 md:rounded-lg md:border md:border-border md:px-2 md:py-1 md:translucent-ui:border-[--popover-border-color] md:translucent-ui:bg-[--popover-background-color] md:translucent-ui:[backdrop-filter:var(--popover-backdrop-filter)]',
|
||||
'md:absolute md:bottom-0 md:left-1/2 md:max-w-[60%] md:-translate-x-1/2 md:rounded-t-lg md:border md:border-b-0 md:border-border md:px-2 md:py-1 md:translucent-ui:border-[--popover-border-color] md:translucent-ui:bg-[--popover-background-color] md:translucent-ui:[backdrop-filter:var(--popover-backdrop-filter)]',
|
||||
!canShowToolbar ? 'hidden' : '',
|
||||
)}
|
||||
id="super-mobile-toolbar"
|
||||
@@ -536,7 +409,6 @@ const ToolbarPlugin = () => {
|
||||
<LinkTextEditor
|
||||
linkText={linkText}
|
||||
editor={editor}
|
||||
lastSelection={lastSelection}
|
||||
isEditMode={isLinkTextEditMode}
|
||||
setEditMode={setIsLinkTextEditMode}
|
||||
/>
|
||||
@@ -551,7 +423,6 @@ const ToolbarPlugin = () => {
|
||||
<>
|
||||
<div
|
||||
className="border-t border-border px-1 py-1 focus:shadow-none focus:outline-none md:border-0 md:px-0 md:py-0"
|
||||
ref={linkEditorRef}
|
||||
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
>
|
||||
<LinkEditor
|
||||
@@ -560,7 +431,6 @@ const ToolbarPlugin = () => {
|
||||
setEditMode={setIsLinkEditMode}
|
||||
isAutoLink={isAutoLink}
|
||||
editor={editor}
|
||||
lastSelection={lastSelection}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@@ -571,27 +441,199 @@ const ToolbarPlugin = () => {
|
||||
)}
|
||||
<div className="flex w-full flex-shrink-0 border-t border-border md:border-0">
|
||||
<Toolbar
|
||||
className="flex items-center gap-1 overflow-x-auto pl-1 [&::-webkit-scrollbar]:h-0"
|
||||
className="flex items-center gap-1 overflow-x-auto px-1 [&::-webkit-scrollbar]:h-0"
|
||||
ref={toolbarRef}
|
||||
store={toolbarStore}
|
||||
>
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<ToolbarButton
|
||||
name={item.name}
|
||||
iconName={item.iconName}
|
||||
active={item.active}
|
||||
disabled={item.disabled}
|
||||
onSelect={item.onSelect}
|
||||
key={item.name}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<ToolbarButton
|
||||
name="Undo"
|
||||
iconName="undo"
|
||||
disabled={!canUndo}
|
||||
onSelect={() => editor.dispatchCommand(UNDO_COMMAND, undefined)}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name="Redo"
|
||||
iconName="redo"
|
||||
disabled={!canRedo}
|
||||
onSelect={() => editor.dispatchCommand(REDO_COMMAND, undefined)}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name="Bold"
|
||||
iconName="bold"
|
||||
active={isBold}
|
||||
onSelect={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold')}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name="Italic"
|
||||
iconName="italic"
|
||||
active={isItalic}
|
||||
onSelect={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic')}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name="Underline"
|
||||
iconName="underline"
|
||||
active={isUnderline}
|
||||
onSelect={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline')}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name="Highlight"
|
||||
iconName="draw"
|
||||
active={isHighlight}
|
||||
onSelect={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'highlight')}
|
||||
/>
|
||||
<ToolbarButton name="Link" iconName="link" active={isLink} onSelect={() => {}} />
|
||||
<ToolbarButton
|
||||
name="Strikethrough"
|
||||
iconName="strikethrough"
|
||||
active={isStrikethrough}
|
||||
onSelect={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough')}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name="Subscript"
|
||||
iconName="subscript"
|
||||
active={isSubscript}
|
||||
onSelect={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript')}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name="Superscript"
|
||||
iconName="superscript"
|
||||
active={isSuperscript}
|
||||
onSelect={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript')}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name="Inline Code"
|
||||
iconName="code-tags"
|
||||
active={isCode}
|
||||
onSelect={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code')}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name="Search"
|
||||
iconName="search"
|
||||
onSelect={() => application.keyboardService.triggerCommand(SUPER_TOGGLE_SEARCH)}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name={ParagraphBlock.name}
|
||||
iconName={ParagraphBlock.iconName}
|
||||
active={blockType === 'paragraph'}
|
||||
onSelect={() => ParagraphBlock.onSelect(editor)}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name={H1Block.name}
|
||||
iconName={H1Block.iconName}
|
||||
active={blockType === 'h1'}
|
||||
onSelect={() => H1Block.onSelect(editor)}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name={H2Block.name}
|
||||
iconName={H2Block.iconName}
|
||||
active={blockType === 'h2'}
|
||||
onSelect={() => H2Block.onSelect(editor)}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name={H3Block.name}
|
||||
iconName={H3Block.iconName}
|
||||
active={blockType === 'h3'}
|
||||
onSelect={() => H3Block.onSelect(editor)}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name={IndentBlock.name}
|
||||
iconName={IndentBlock.iconName}
|
||||
onSelect={() => IndentBlock.onSelect(editor)}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name={OutdentBlock.name}
|
||||
iconName={OutdentBlock.iconName}
|
||||
onSelect={() => OutdentBlock.onSelect(editor)}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name={LeftAlignBlock.name}
|
||||
iconName={LeftAlignBlock.iconName}
|
||||
active={elementFormat === 'left'}
|
||||
onSelect={() => LeftAlignBlock.onSelect(editor)}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name={CenterAlignBlock.name}
|
||||
iconName={CenterAlignBlock.iconName}
|
||||
active={elementFormat === 'center'}
|
||||
onSelect={() => CenterAlignBlock.onSelect(editor)}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name={RightAlignBlock.name}
|
||||
iconName={RightAlignBlock.iconName}
|
||||
active={elementFormat === 'right'}
|
||||
onSelect={() => RightAlignBlock.onSelect(editor)}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name={JustifyAlignBlock.name}
|
||||
iconName={JustifyAlignBlock.iconName}
|
||||
active={elementFormat === 'justify'}
|
||||
onSelect={() => JustifyAlignBlock.onSelect(editor)}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name={BulletedListBlock.name}
|
||||
iconName={BulletedListBlock.iconName}
|
||||
active={blockType === 'bullet'}
|
||||
onSelect={() => BulletedListBlock.onSelect(editor)}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name={NumberedListBlock.name}
|
||||
iconName={NumberedListBlock.iconName}
|
||||
active={blockType === 'number'}
|
||||
onSelect={() => NumberedListBlock.onSelect(editor)}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name={ChecklistBlock.name}
|
||||
iconName={ChecklistBlock.iconName}
|
||||
active={blockType === 'check'}
|
||||
onSelect={() => ChecklistBlock.onSelect(editor)}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name="Table"
|
||||
iconName="table"
|
||||
onSelect={() =>
|
||||
showModal('Insert Table', (onClose) => <InsertTableDialog activeEditor={editor} onClose={onClose} />)
|
||||
}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name="Image from URL"
|
||||
iconName="image"
|
||||
onSelect={() =>
|
||||
showModal('Insert image from URL', (onClose) => <InsertRemoteImageDialog onClose={onClose} />)
|
||||
}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name={CodeBlock.name}
|
||||
iconName={CodeBlock.iconName}
|
||||
active={blockType === 'code'}
|
||||
onSelect={() => CodeBlock.onSelect(editor)}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name={QuoteBlock.name}
|
||||
iconName={QuoteBlock.iconName}
|
||||
active={blockType === 'quote'}
|
||||
onSelect={() => QuoteBlock.onSelect(editor)}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name={DividerBlock.name}
|
||||
iconName={DividerBlock.iconName}
|
||||
onSelect={() => DividerBlock.onSelect(editor)}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name={CollapsibleBlock.name}
|
||||
iconName={CollapsibleBlock.iconName}
|
||||
onSelect={() => CollapsibleBlock.onSelect(editor)}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name={PasswordBlock.name}
|
||||
iconName={PasswordBlock.iconName}
|
||||
onSelect={() => PasswordBlock.onSelect(editor)}
|
||||
/>
|
||||
</Toolbar>
|
||||
{isMobile && (
|
||||
<button
|
||||
className="flex flex-shrink-0 items-center justify-center rounded border-l border-border px-3 py-3"
|
||||
aria-label="Dismiss keyboard"
|
||||
ref={dismissButtonRef}
|
||||
>
|
||||
<Icon type="keyboard-close" size="medium" />
|
||||
</button>
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
import { $isCodeHighlightNode } from '@lexical/code'
|
||||
import { $isLinkNode, $isAutoLinkNode } from '@lexical/link'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { mergeRegister, $findMatchingParent, $getNearestNodeOfType } from '@lexical/utils'
|
||||
import {
|
||||
$getSelection,
|
||||
$isRangeSelection,
|
||||
$isTextNode,
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
$isRootOrShadowRoot,
|
||||
COMMAND_PRIORITY_CRITICAL,
|
||||
} from 'lexical'
|
||||
import { $isHeadingNode } from '@lexical/rich-text'
|
||||
import { $isListNode, ListNode } from '@lexical/list'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { getSelectedNode } from '../../Lexical/Utils/getSelectedNode'
|
||||
import { $isLinkTextNode } from './ToolbarLinkTextEditor'
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
export function useSelectedTextFormatInfo() {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
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 [isHighlighted, setIsHighlighted] = 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 updateTextFormatInfo = 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()
|
||||
|
||||
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'))
|
||||
setIsHighlighted(selection.hasFormat('highlight'))
|
||||
|
||||
// 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)
|
||||
updateTextFormatInfo()
|
||||
return false
|
||||
},
|
||||
COMMAND_PRIORITY_CRITICAL,
|
||||
)
|
||||
}, [editor, updateTextFormatInfo])
|
||||
|
||||
useEffect(() => {
|
||||
return mergeRegister(
|
||||
editor.registerUpdateListener(() => {
|
||||
updateTextFormatInfo()
|
||||
}),
|
||||
editor.registerRootListener(() => {
|
||||
if (editor.getRootElement() === null) {
|
||||
setIsText(false)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}, [editor, updateTextFormatInfo])
|
||||
|
||||
return {
|
||||
isText,
|
||||
isLink,
|
||||
isAutoLink,
|
||||
isLinkText,
|
||||
isBold,
|
||||
isItalic,
|
||||
isUnderline,
|
||||
isStrikethrough,
|
||||
isSubscript,
|
||||
isSuperscript,
|
||||
isHighlighted,
|
||||
isCode,
|
||||
blockType,
|
||||
}
|
||||
}
|
||||
@@ -243,3 +243,15 @@ export const getBlobFromBase64 = (b64Data: string, contentType = '', sliceSize =
|
||||
const blob = new Blob(byteArrays, { type: contentType })
|
||||
return blob
|
||||
}
|
||||
|
||||
export function getScrollParent(node: HTMLElement | null): HTMLElement | null {
|
||||
if (!node) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (node.scrollHeight > node.clientHeight || node.scrollWidth > node.clientWidth) {
|
||||
return node
|
||||
} else {
|
||||
return getScrollParent(node.parentElement)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user