From 9e35e2ecebfd43c135d1e480e0c0e4c9d0ef39d9 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Tue, 17 Oct 2023 22:49:19 +0530 Subject: [PATCH] fix: Fixed performance regression in Super notes and improved toolbar (#2590) --- packages/icons/src/Icons/ic-details-block.svg | 3 + packages/icons/src/Icons/index.ts | 2 + .../src/Domain/Utilities/Icon/IconType.ts | 1 + .../Components/Icon/IconNameToSvgMapping.tsx | 2 + .../Components/Panes/usePaneGesture.ts | 13 +- .../StyledTooltip/StyledTooltip.tsx | 27 +- .../BlockPickerPlugin/BlockPickerPlugin.tsx | 48 +- .../BlockPickerPlugin/Options/Alignment.tsx | 14 - .../Options/BulletedList.tsx | 13 - .../BlockPickerPlugin/Options/Checklist.tsx | 13 - .../BlockPickerPlugin/Options/Code.tsx | 12 - .../BlockPickerPlugin/Options/Collapsible.tsx | 12 - .../BlockPickerPlugin/Options/DateTime.tsx | 15 - .../BlockPickerPlugin/Options/Divider.tsx | 13 - .../BlockPickerPlugin/Options/Embeds.tsx | 15 - .../BlockPickerPlugin/Options/Headings.tsx | 15 - .../Options/IndentOutdent.tsx | 15 - .../Options/NumberedList.tsx | 13 - .../BlockPickerPlugin/Options/Paragraph.tsx | 13 - .../BlockPickerPlugin/Options/Password.tsx | 42 - .../BlockPickerPlugin/Options/Quote.tsx | 13 - .../BlockPickerPlugin/Options/RemoteImage.tsx | 12 - .../BlockPickerPlugin/Options/Table.tsx | 56 -- .../SuperEditor/Plugins/Blocks/Alignment.tsx | 68 +- .../Plugins/Blocks/BulletedList.tsx | 12 - .../SuperEditor/Plugins/Blocks/Checklist.tsx | 11 - .../SuperEditor/Plugins/Blocks/Code.tsx | 47 +- .../Plugins/Blocks/Collapsible.tsx | 21 +- .../SuperEditor/Plugins/Blocks/DateTime.tsx | 13 + .../SuperEditor/Plugins/Blocks/Divider.tsx | 22 +- .../SuperEditor/Plugins/Blocks/Embeds.tsx | 12 + .../SuperEditor/Plugins/Blocks/Headings.tsx | 79 +- .../Plugins/Blocks/IndentOutdent.tsx | 45 +- .../SuperEditor/Plugins/Blocks/List.tsx | 49 ++ .../Plugins/Blocks/NumberedList.tsx | 12 - .../SuperEditor/Plugins/Blocks/Paragraph.tsx | 36 +- .../SuperEditor/Plugins/Blocks/Password.tsx | 49 +- .../SuperEditor/Plugins/Blocks/Quote.tsx | 36 +- .../Plugins/Blocks/RemoteImage.tsx | 14 +- .../SuperEditor/Plugins/Blocks/Table.tsx | 55 +- .../ToolbarPlugin/ToolbarLinkEditor.tsx | 13 +- .../ToolbarPlugin/ToolbarLinkTextEditor.tsx | 19 +- .../Plugins/ToolbarPlugin/ToolbarPlugin.tsx | 780 +++++++++--------- .../useSelectedTextFormatInfo.ts | 180 ---- packages/web/src/javascripts/Utils/Utils.ts | 12 + 45 files changed, 933 insertions(+), 1034 deletions(-) create mode 100644 packages/icons/src/Icons/ic-details-block.svg delete mode 100644 packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Alignment.tsx delete mode 100644 packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/BulletedList.tsx delete mode 100644 packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Checklist.tsx delete mode 100644 packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Code.tsx delete mode 100644 packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Collapsible.tsx delete mode 100644 packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/DateTime.tsx delete mode 100644 packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Divider.tsx delete mode 100644 packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Embeds.tsx delete mode 100644 packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Headings.tsx delete mode 100644 packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/IndentOutdent.tsx delete mode 100644 packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/NumberedList.tsx delete mode 100644 packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Paragraph.tsx delete mode 100644 packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Password.tsx delete mode 100644 packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Quote.tsx delete mode 100644 packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/RemoteImage.tsx delete mode 100644 packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Table.tsx delete mode 100644 packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/BulletedList.tsx delete mode 100644 packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Checklist.tsx create mode 100644 packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/List.tsx delete mode 100644 packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/NumberedList.tsx delete mode 100644 packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/useSelectedTextFormatInfo.ts diff --git a/packages/icons/src/Icons/ic-details-block.svg b/packages/icons/src/Icons/ic-details-block.svg new file mode 100644 index 000000000..eed4a93db --- /dev/null +++ b/packages/icons/src/Icons/ic-details-block.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/icons/src/Icons/index.ts b/packages/icons/src/Icons/index.ts index 6f0f9a4e3..c05c632d4 100644 --- a/packages/icons/src/Icons/index.ts +++ b/packages/icons/src/Icons/index.ts @@ -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, diff --git a/packages/models/src/Domain/Utilities/Icon/IconType.ts b/packages/models/src/Domain/Utilities/Icon/IconType.ts index aeae82224..615ca7840 100644 --- a/packages/models/src/Domain/Utilities/Icon/IconType.ts +++ b/packages/models/src/Domain/Utilities/Icon/IconType.ts @@ -189,3 +189,4 @@ export type IconType = | 'view' | 'warning' | 'window' + | 'details-block' diff --git a/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx b/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx index 40532e73f..755abcf7b 100644 --- a/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx +++ b/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx @@ -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, diff --git a/packages/web/src/javascripts/Components/Panes/usePaneGesture.ts b/packages/web/src/javascripts/Components/Panes/usePaneGesture.ts index f53b1dc1a..cee8b31e7 100644 --- a/packages/web/src/javascripts/Components/Panes/usePaneGesture.ts +++ b/packages/web/src/javascripts/Components/Panes/usePaneGesture.ts @@ -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 diff --git a/packages/web/src/javascripts/Components/StyledTooltip/StyledTooltip.tsx b/packages/web/src/javascripts/Components/StyledTooltip/StyledTooltip.tsx index 17f45540b..a3bfe5d4d 100644 --- a/packages/web/src/javascripts/Components/StyledTooltip/StyledTooltip.tsx +++ b/packages/web/src/javascripts/Components/StyledTooltip/StyledTooltip.tsx @@ -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) => { const [forceOpen, setForceOpen] = useState() @@ -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, diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/BlockPickerPlugin.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/BlockPickerPlugin.tsx index 2e921506f..5dd4d7fe6 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/BlockPickerPlugin.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/BlockPickerPlugin.tsx @@ -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) => ), @@ -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), diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Alignment.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Alignment.tsx deleted file mode 100644 index 3e249b630..000000000 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Alignment.tsx +++ /dev/null @@ -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, - }), - ) -} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/BulletedList.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/BulletedList.tsx deleted file mode 100644 index ad83f4b46..000000000 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/BulletedList.tsx +++ /dev/null @@ -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, - }) -} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Checklist.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Checklist.tsx deleted file mode 100644 index c72decafd..000000000 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Checklist.tsx +++ /dev/null @@ -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, - }) -} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Code.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Code.tsx deleted file mode 100644 index 06b7ce191..000000000 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Code.tsx +++ /dev/null @@ -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, - }) -} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Collapsible.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Collapsible.tsx deleted file mode 100644 index 65bd126a7..000000000 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Collapsible.tsx +++ /dev/null @@ -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, - }) -} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/DateTime.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/DateTime.tsx deleted file mode 100644 index 2ac4aeac0..000000000 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/DateTime.tsx +++ /dev/null @@ -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, - }), - ) -} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Divider.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Divider.tsx deleted file mode 100644 index bbbe1e828..000000000 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Divider.tsx +++ /dev/null @@ -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, - }) -} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Embeds.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Embeds.tsx deleted file mode 100644 index b1329f081..000000000 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Embeds.tsx +++ /dev/null @@ -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, - }), - ) -} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Headings.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Headings.tsx deleted file mode 100644 index 06baa66ff..000000000 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Headings.tsx +++ /dev/null @@ -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, - }), - ) -} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/IndentOutdent.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/IndentOutdent.tsx deleted file mode 100644 index 9d5109f50..000000000 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/IndentOutdent.tsx +++ /dev/null @@ -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, - }), - ) -} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/NumberedList.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/NumberedList.tsx deleted file mode 100644 index 63a456352..000000000 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/NumberedList.tsx +++ /dev/null @@ -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, - }) -} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Paragraph.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Paragraph.tsx deleted file mode 100644 index d88cc275d..000000000 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Paragraph.tsx +++ /dev/null @@ -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, - }) -} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Password.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Password.tsx deleted file mode 100644 index e23c7db40..000000000 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Password.tsx +++ /dev/null @@ -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()), - }), - ] -} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Quote.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Quote.tsx deleted file mode 100644 index 8a1671c4c..000000000 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Quote.tsx +++ /dev/null @@ -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, - }) -} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/RemoteImage.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/RemoteImage.tsx deleted file mode 100644 index d0a7fc4f3..000000000 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/RemoteImage.tsx +++ /dev/null @@ -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, - }) -} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Table.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Table.tsx deleted file mode 100644 index cfa090336..000000000 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/Options/Table.tsx +++ /dev/null @@ -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 = [] - - 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 -} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Alignment.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Alignment.tsx index 878b52c7d..c9e4e960d 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Alignment.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Alignment.tsx @@ -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), + }) } diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/BulletedList.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/BulletedList.tsx deleted file mode 100644 index cecb8b984..000000000 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/BulletedList.tsx +++ /dev/null @@ -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, - } -} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Checklist.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Checklist.tsx deleted file mode 100644 index 11652a73c..000000000 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Checklist.tsx +++ /dev/null @@ -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), - } -} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Code.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Code.tsx index cc5265a7e..b8e633338 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Code.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Code.tsx @@ -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), + }) } diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Collapsible.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Collapsible.tsx index 5eae15b42..a652164c4 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Collapsible.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Collapsible.tsx @@ -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), + }) } diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/DateTime.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/DateTime.tsx index 66b328523..04dde6e38 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/DateTime.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/DateTime.tsx @@ -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, + }), + ) +} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Divider.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Divider.tsx index a1e0c2020..d2f70d288 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Divider.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Divider.tsx @@ -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), + }) } diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Embeds.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Embeds.tsx index 308042ba0..4964537f3 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Embeds.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Embeds.tsx @@ -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, + }), + ) +} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Headings.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Headings.tsx index f8c9fd76f..8b132f599 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Headings.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Headings.tsx @@ -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), + }) } diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/IndentOutdent.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/IndentOutdent.tsx index 40e5e7108..a77b5f0ac 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/IndentOutdent.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/IndentOutdent.tsx @@ -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), + }) } diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/List.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/List.tsx new file mode 100644 index 000000000..86f6c347e --- /dev/null +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/List.tsx @@ -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), + }) +} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/NumberedList.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/NumberedList.tsx deleted file mode 100644 index f61a9da2e..000000000 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/NumberedList.tsx +++ /dev/null @@ -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, - } -} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Paragraph.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Paragraph.tsx index ab79d22c6..864794d71 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Paragraph.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Paragraph.tsx @@ -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), + }) } diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Password.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Password.tsx index 316b6e06b..e4146aa6b 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Password.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Password.tsx @@ -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()), + }), + ] } diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Quote.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Quote.tsx index 5115f5695..e3dc1a3ce 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Quote.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Quote.tsx @@ -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), + }) } diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/RemoteImage.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/RemoteImage.tsx index 4e6eba56b..e9965a96a 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/RemoteImage.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/RemoteImage.tsx @@ -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, + }) } diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Table.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Table.tsx index 7f803cdb5..24e219d10 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Table.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/Table.tsx @@ -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 = [] + + 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 } diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarLinkEditor.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarLinkEditor.tsx index e6a69d775..babac09e0 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarLinkEditor.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarLinkEditor.tsx @@ -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(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) => { diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarLinkTextEditor.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarLinkTextEditor.tsx index dc56f097f..594c07615 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarLinkTextEditor.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarLinkTextEditor.tsx @@ -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, 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(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) } diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarPlugin.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarPlugin.tsx index 1ac7f91f1..b28c0c309 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarPlugin.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarPlugin.tsx @@ -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('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 ( - + { @@ -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(null) - const toolbarRef = useRef(null) - const linkEditorRef = useRef(null) - const backspaceButtonRef = useRef(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('paragraph') + const [elementFormat, setElementFormat] = useState('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('') + const [linkUrl, setLinkUrl] = useState('') 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(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( + activeEditor.registerUpdateListener(({ editorState }) => { + editorState.read(() => { + $updateToolbar() + }) + }), + activeEditor.registerCommand( CAN_UNDO_COMMAND, (payload) => { setCanUndo(payload) @@ -149,7 +229,7 @@ const ToolbarPlugin = () => { }, COMMAND_PRIORITY_CRITICAL, ), - editor.registerCommand( + activeEditor.registerCommand( 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) => ), - ), - GetRemoteImageBlock(() => { - showModal('Insert image from URL', (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(null) + const dismissButtonRef = useRef(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(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(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}
{ @@ -551,7 +423,6 @@ const ToolbarPlugin = () => { <>
{ setEditMode={setIsLinkEditMode} isAutoLink={isAutoLink} editor={editor} - lastSelection={lastSelection} />
{ )}
- {items.map((item) => { - return ( - - ) - })} + editor.dispatchCommand(UNDO_COMMAND, undefined)} + /> + editor.dispatchCommand(REDO_COMMAND, undefined)} + /> + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold')} + /> + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic')} + /> + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline')} + /> + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'highlight')} + /> + {}} /> + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough')} + /> + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript')} + /> + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript')} + /> + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code')} + /> + application.keyboardService.triggerCommand(SUPER_TOGGLE_SEARCH)} + /> + ParagraphBlock.onSelect(editor)} + /> + H1Block.onSelect(editor)} + /> + H2Block.onSelect(editor)} + /> + H3Block.onSelect(editor)} + /> + IndentBlock.onSelect(editor)} + /> + OutdentBlock.onSelect(editor)} + /> + LeftAlignBlock.onSelect(editor)} + /> + CenterAlignBlock.onSelect(editor)} + /> + RightAlignBlock.onSelect(editor)} + /> + JustifyAlignBlock.onSelect(editor)} + /> + BulletedListBlock.onSelect(editor)} + /> + NumberedListBlock.onSelect(editor)} + /> + ChecklistBlock.onSelect(editor)} + /> + + showModal('Insert Table', (onClose) => ) + } + /> + + showModal('Insert image from URL', (onClose) => ) + } + /> + CodeBlock.onSelect(editor)} + /> + QuoteBlock.onSelect(editor)} + /> + DividerBlock.onSelect(editor)} + /> + CollapsibleBlock.onSelect(editor)} + /> + PasswordBlock.onSelect(editor)} + /> {isMobile && ( diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/useSelectedTextFormatInfo.ts b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/useSelectedTextFormatInfo.ts deleted file mode 100644 index a541b215b..000000000 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/useSelectedTextFormatInfo.ts +++ /dev/null @@ -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('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(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, - } -} diff --git a/packages/web/src/javascripts/Utils/Utils.ts b/packages/web/src/javascripts/Utils/Utils.ts index c9428cf57..ec7d63a2c 100644 --- a/packages/web/src/javascripts/Utils/Utils.ts +++ b/packages/web/src/javascripts/Utils/Utils.ts @@ -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) + } +}