diff --git a/packages/icons/src/Icons/ic-indent.svg b/packages/icons/src/Icons/ic-indent.svg new file mode 100644 index 00000000000..5b7a870a660 --- /dev/null +++ b/packages/icons/src/Icons/ic-indent.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/icons/src/Icons/ic-outdent.svg b/packages/icons/src/Icons/ic-outdent.svg new file mode 100644 index 00000000000..b486024aa13 --- /dev/null +++ b/packages/icons/src/Icons/ic-outdent.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/icons/src/Icons/index.ts b/packages/icons/src/Icons/index.ts index 8a340f8eb57..6f0f9a4e396 100644 --- a/packages/icons/src/Icons/index.ts +++ b/packages/icons/src/Icons/index.ts @@ -104,6 +104,7 @@ import HistoryLockedIllustration from './il-history-locked.svg' import IconsSpriteStylekit from './icons-sprite-stylekit.svg' import IlNotesIcon from './il-notes.svg' import ImageIcon from './ic-image.svg' +import IndentIcon from './ic-indent.svg' import InfoIcon from './ic-info.svg' import ItalicIcon from './ic-italic.svg' import KeyboardCloseIcon from './ic-keyboard-close.svg' @@ -137,6 +138,7 @@ import NoPreviewIllustration from './il-no-preview.svg' import NotesFilledIcon from './ic-notes-filled.svg' import NotesIcon from './ic-notes.svg' import OpenInIcon from './ic-open-in.svg' +import OutdentIcon from './ic-outdent.svg' import PasswordIcon from './ic-textbox-password.svg' import PencilFilledIcon from './ic-pencil-filled.svg' import PencilIcon from './ic-pencil.svg' @@ -188,8 +190,8 @@ import SubtractIcon from './ic-subtract.svg' import SuperscriptIcon from './ic-superscript.svg' import SyncIcon from './ic-sync.svg' import TasksIcon from './ic-tasks.svg' -import TextIcon from './ic-text.svg' import TextCircleIcon from './ic-text-circle.svg' +import TextIcon from './ic-text.svg' import TextParagraphLongIcon from './ic-text-paragraph-long.svg' import ThemesFilledIcon from './ic-themes-filled.svg' import ThemesIcon from './ic-themes.svg' @@ -221,6 +223,7 @@ export { AddBoldIcon, AddIcon, AddTextIcon, + AegisIcon, ArchiveIcon, ArrowDownCheckmarkIcon, ArrowDownIcon, @@ -274,6 +277,7 @@ export { EmailFilledIcon, EmailIcon, EnterIcon, + EvernoteIcon, EyeFilledIcon, EyeIcon, EyeOffFilledIcon, @@ -304,6 +308,7 @@ export { FullscreenExitIcon, FullscreenIcon, GiftOutlineIcon, + GoogleKeepIcon, GroupIcon, HashtagFilledIcon, HashtagIcon, @@ -316,6 +321,7 @@ export { IconsSpriteStylekit, IlNotesIcon, ImageIcon, + IndentIcon, InfoIcon, ItalicIcon, KeyboardCloseIcon, @@ -349,6 +355,7 @@ export { NotesFilledIcon, NotesIcon, OpenInIcon, + OutdentIcon, PasswordIcon, PencilFilledIcon, PencilIcon, @@ -364,6 +371,8 @@ export { ProtectedIllustration, RedoIcon, ReorderIcon, + ReplaceAllIcon, + ReplaceIcon, RestoreIcon, RichTextIcon, SafeIcon, @@ -382,6 +391,7 @@ export { ShortcutButtonIcon, SignInIcon, SignOutIcon, + SimplenoteIcon, SNLogoAltIcon, SNLogoFull, SNLogoIcon, @@ -397,8 +407,8 @@ export { SuperscriptIcon, SyncIcon, TasksIcon, - TextIcon, TextCircleIcon, + TextIcon, TextParagraphLongIcon, ThemesFilledIcon, ThemesIcon, @@ -420,10 +430,4 @@ export { ViewIcon, WarningIcon, WindowIcon, - EvernoteIcon, - GoogleKeepIcon, - SimplenoteIcon, - AegisIcon, - ReplaceIcon, - ReplaceAllIcon, } diff --git a/packages/icons/src/Lexical/indent.svg b/packages/icons/src/Lexical/indent.svg deleted file mode 100644 index 215abe52c4b..00000000000 --- a/packages/icons/src/Lexical/indent.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/icons/src/Lexical/outdent.svg b/packages/icons/src/Lexical/outdent.svg deleted file mode 100644 index 812abfce1cd..00000000000 --- a/packages/icons/src/Lexical/outdent.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/ui-services/src/Keyboard/KeyboardCommands.ts b/packages/ui-services/src/Keyboard/KeyboardCommands.ts index 5369e22b061..b796f81086c 100644 --- a/packages/ui-services/src/Keyboard/KeyboardCommands.ts +++ b/packages/ui-services/src/Keyboard/KeyboardCommands.ts @@ -27,6 +27,7 @@ export const STAR_NOTE_COMMAND = createKeyboardCommand('STAR_NOTE_COMMAND') export const PIN_NOTE_COMMAND = createKeyboardCommand('PIN_NOTE_COMMAND') export const SUPER_TOGGLE_SEARCH = createKeyboardCommand('SUPER_TOGGLE_SEARCH') +export const SUPER_TOGGLE_TOOLBAR = createKeyboardCommand('SUPER_TOGGLE_TOOLBAR') export const SUPER_SEARCH_TOGGLE_CASE_SENSITIVE = createKeyboardCommand('SUPER_SEARCH_TOGGLE_CASE_SENSITIVE') export const SUPER_SEARCH_TOGGLE_REPLACE_MODE = createKeyboardCommand('SUPER_SEARCH_TOGGLE_REPLACE_MODE') export const SUPER_SEARCH_NEXT_RESULT = createKeyboardCommand('SUPER_SEARCH_NEXT_RESULT') diff --git a/packages/ui-services/src/Keyboard/getKeyboardShortcuts.ts b/packages/ui-services/src/Keyboard/getKeyboardShortcuts.ts index d1fc2699adb..d6ba3bb7365 100644 --- a/packages/ui-services/src/Keyboard/getKeyboardShortcuts.ts +++ b/packages/ui-services/src/Keyboard/getKeyboardShortcuts.ts @@ -30,6 +30,7 @@ import { SUPER_SEARCH_PREVIOUS_RESULT, SUPER_SEARCH_TOGGLE_REPLACE_MODE, CHANGE_EDITOR_WIDTH_COMMAND, + SUPER_TOGGLE_TOOLBAR, } from './KeyboardCommands' import { KeyboardKey } from './KeyboardKey' import { KeyboardModifier } from './KeyboardModifier' @@ -147,6 +148,11 @@ export function getKeyboardShortcuts(platform: Platform, _environment: Environme modifiers: [primaryModifier, KeyboardModifier.Shift], preventDefault: true, }, + { + command: SUPER_TOGGLE_TOOLBAR, + key: 'k', + modifiers: [primaryModifier, KeyboardModifier.Shift], + }, { command: SUPER_TOGGLE_SEARCH, key: 'f', diff --git a/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx b/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx index 8eed74b8a59..67c63a550c9 100644 --- a/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx +++ b/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx @@ -42,8 +42,8 @@ export const IconNameToSvgMapping = { 'fullscreen-exit': icons.FullscreenExitIcon, 'hashtag-off': icons.HashtagOffIcon, 'keyboard-close': icons.KeyboardCloseIcon, - 'link-off': icons.LinkOffIcon, 'line-width': icons.LineWidthIcon, + 'link-off': icons.LinkOffIcon, 'list-bulleted': icons.ListBulleted, 'list-numbered': icons.ListNumbered, 'lock-filled': icons.LockFilledIcon, @@ -99,17 +99,19 @@ export const IconNameToSvgMapping = { hashtag: icons.HashtagIcon, help: icons.HelpIcon, history: icons.HistoryIcon, + image: icons.ImageIcon, + indent: icons.IndentIcon, info: icons.InfoIcon, italic: icons.ItalicIcon, keyboard: icons.KeyboardIcon, link: icons.LinkIcon, listed: icons.ListedIcon, lock: icons.LockIcon, - image: icons.ImageIcon, markdown: icons.MarkdownIcon, merge: icons.MergeIcon, more: icons.MoreIcon, notes: icons.NotesIcon, + outdent: icons.OutdentIcon, paragraph: icons.TextParagraphLongIcon, password: icons.PasswordIcon, pencil: icons.PencilIcon, diff --git a/packages/web/src/javascripts/Components/SuperEditor/BlocksEditor.tsx b/packages/web/src/javascripts/Components/SuperEditor/BlocksEditor.tsx index 3a9e6a3ec43..a9edfe7ddd6 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/BlocksEditor.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/BlocksEditor.tsx @@ -19,7 +19,6 @@ import AutoEmbedPlugin from './Plugins/AutoEmbedPlugin' import CollapsiblePlugin from './Plugins/CollapsiblePlugin' import DraggableBlockPlugin from './Plugins/DraggableBlockPlugin' import CodeHighlightPlugin from './Plugins/CodeHighlightPlugin' -import FloatingTextFormatToolbarPlugin from './Plugins/ToolbarPlugins/FloatingTextFormatToolbarPlugin' import { TabIndentationPlugin } from './Plugins/TabIndentationPlugin' import { handleEditorChange } from './Utils' import { SuperEditorContentId } from './Constants' @@ -110,7 +109,6 @@ export const BlocksEditor: FunctionComponent = ({ {!readonly && floatingAnchorElem && ( <> - 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 1035401f005..40e5e710861 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/IndentOutdent.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/IndentOutdent.tsx @@ -4,13 +4,13 @@ export function GetIndentOutdentBlocks(editor: LexicalEditor) { return [ { name: 'Indent', - iconName: 'arrow-right', + iconName: 'indent', keywords: ['indent'], onSelect: () => editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined), }, { name: 'Outdent', - iconName: 'arrow-left', + iconName: 'outdent', keywords: ['outdent'], onSelect: () => editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined), }, diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/LinkEditor/LinkEditor.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarLinkEditor.tsx similarity index 53% rename from packages/web/src/javascripts/Components/SuperEditor/Plugins/LinkEditor/LinkEditor.tsx rename to packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarLinkEditor.tsx index ef4cba5f1c8..e6a69d775c0 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/LinkEditor/LinkEditor.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarLinkEditor.tsx @@ -1,12 +1,11 @@ import Icon from '@/Components/Icon/Icon' -import { CloseIcon, CheckIcon, PencilFilledIcon, TrashFilledIcon } from '@standardnotes/icons' import { KeyboardKey } from '@standardnotes/ui-services' -import { IconComponent } from '../../Lexical/Theme/IconComponent' 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 { classNames } from '@standardnotes/snjs' +import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip' type Props = { linkUrl: string @@ -65,29 +64,27 @@ const LinkEditor = ({ linkUrl, isEditMode, setEditMode, editor, lastSelection, i }} className="flex-grow rounded-sm bg-contrast p-1 text-text sm:min-w-[40ch]" /> - - + + + + + + ) : (
@@ -105,31 +102,29 @@ const LinkEditor = ({ linkUrl, isEditMode, setEditMode, editor, lastSelection, i {!isAutoLink && ( <> - - + + + + + + )}
diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/LinkEditor/LinkTextEditor.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarLinkTextEditor.tsx similarity index 56% rename from packages/web/src/javascripts/Components/SuperEditor/Plugins/LinkEditor/LinkTextEditor.tsx rename to packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarLinkTextEditor.tsx index c2f89589b13..dc56f097ffe 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/LinkEditor/LinkTextEditor.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarLinkTextEditor.tsx @@ -1,17 +1,18 @@ import Icon from '@/Components/Icon/Icon' -import { CloseIcon, CheckIcon, PencilFilledIcon } from '@standardnotes/icons' import { KeyboardKey } from '@standardnotes/ui-services' -import { IconComponent } from '../../Lexical/Theme/IconComponent' import { $isRangeSelection, $isTextNode, GridSelection, LexicalEditor, NodeSelection, RangeSelection } from 'lexical' import { useCallback, useEffect, useRef, useState } from 'react' import { VisuallyHidden } from '@ariakit/react' import { getSelectedNode } from '../../Lexical/Utils/getSelectedNode' import { $isLinkNode } from '@lexical/link' +import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip' type Props = { linkText: string editor: LexicalEditor lastSelection: RangeSelection | GridSelection | NodeSelection | null + isEditMode: boolean + setEditMode: (isEditMode: boolean) => void } export const $isLinkTextNode = (node: ReturnType, selection: RangeSelection) => { @@ -19,9 +20,8 @@ export const $isLinkTextNode = (node: ReturnType, select return $isLinkNode(parent) && $isTextNode(node) && selection.anchor.getNode() === selection.focus.getNode() } -const LinkTextEditor = ({ linkText, editor, lastSelection }: Props) => { +const LinkTextEditor = ({ linkText, editor, isEditMode, setEditMode, lastSelection }: Props) => { const [editedLinkText, setEditedLinkText] = useState(() => linkText) - const [isEditMode, setEditMode] = useState(false) const editModeContainer = useRef(null) useEffect(() => { @@ -72,50 +72,47 @@ const LinkTextEditor = ({ linkText, editor, lastSelection }: Props) => { }} className="flex-grow rounded-sm bg-contrast p-1 text-text sm:min-w-[20ch]" /> - - + + + + + + ) : (
-
+
Link text: {linkText}
- + + +
) } diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugins/MobileToolbarPlugin.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarPlugin.tsx similarity index 57% rename from packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugins/MobileToolbarPlugin.tsx rename to packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarPlugin.tsx index 11396e8b906..9279de287bc 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugins/MobileToolbarPlugin.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarPlugin.tsx @@ -18,8 +18,8 @@ import { UNDO_COMMAND, } from 'lexical' import { mergeRegister } from '@lexical/utils' -import { $isLinkNode, $isAutoLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link' -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +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' @@ -37,25 +37,65 @@ import { GetQuoteBlock } from '../Blocks/Quote' import { GetTableBlock } from '../Blocks/Table' import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery' import { classNames } from '@standardnotes/snjs' -import { SUPER_TOGGLE_SEARCH } from '@standardnotes/ui-services' +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 LinkEditor from '../LinkEditor/LinkEditor' +import LinkEditor from './ToolbarLinkEditor' import { FOCUSABLE_BUT_NOT_TABBABLE } 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 MobileToolbarPlugin = () => { +interface ToolbarButtonProps extends ComponentPropsWithoutRef<'button'> { + name: string + active?: boolean + iconName: string + onSelect: () => void +} + +const ToolbarButton = ({ name, active, iconName, onSelect, disabled, ...props }: ToolbarButtonProps) => { + const [editor] = useLexicalComposerContext() + + return ( + + { + event.preventDefault() + onSelect() + }} + onContextMenu={(event) => { + editor.focus() + event.preventDefault() + }} + disabled={disabled} + {...props} + > +
+ +
+
+
+ ) +} + +const ToolbarPlugin = () => { const application = useApplication() const [editor] = useLexicalComposerContext() const [modal, showModal] = useModal() const [isInEditor, setIsInEditor] = useState(false) - const [isInLinkEditor, setIsInLinkEditor] = useState(false) const [isInToolbar, setIsInToolbar] = useState(false) const isMobile = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm) + const containerRef = useRef(null) const toolbarRef = useRef(null) const linkEditorRef = useRef(null) const backspaceButtonRef = useRef(null) @@ -77,9 +117,19 @@ const MobileToolbarPlugin = () => { } }, [editor]) - const { isBold, isItalic, isUnderline, isSubscript, isSuperscript, isStrikethrough, blockType, isHighlighted } = - useSelectedTextFormatInfo() - const [isSelectionLink, setIsSelectionLink] = useState(false) + const { + isBold, + isItalic, + isUnderline, + isSubscript, + isSuperscript, + isStrikethrough, + blockType, + isHighlighted, + isLink, + isLinkText, + isAutoLink, + } = useSelectedTextFormatInfo() const [canUndo, setCanUndo] = useState(false) const [canRedo, setCanRedo] = useState(false) @@ -193,7 +243,7 @@ const MobileToolbarPlugin = () => { insertLink() }) }, - active: isSelectionLink, + active: isLink, }, { name: 'Search', @@ -205,6 +255,7 @@ const MobileToolbarPlugin = () => { GetParagraphBlock(editor), ...GetHeadingsBlocks(editor), ...GetIndentOutdentBlocks(editor), + ...GetAlignmentBlocks(editor), GetTableBlock(() => showModal('Insert Table', (onClose) => ), ), @@ -218,7 +269,6 @@ const MobileToolbarPlugin = () => { GetCodeBlock(editor), GetDividerBlock(editor), ...GetDatetimeBlocks(editor), - ...GetAlignmentBlocks(editor), ...[GetPasswordBlock(editor)], GetCollapsibleBlock(editor), ...GetEmbedsBlocks(editor), @@ -233,7 +283,7 @@ const MobileToolbarPlugin = () => { isBold, isHighlighted, isItalic, - isSelectionLink, + isLink, isStrikethrough, isSubscript, isSuperscript, @@ -243,41 +293,14 @@ const MobileToolbarPlugin = () => { ) useEffect(() => { + const container = containerRef.current const rootElement = editor.getRootElement() if (!rootElement) { return } - const handleFocus = () => setIsInEditor(true) - const handleBlur = (event: FocusEvent) => { - const elementToBeFocused = event.relatedTarget as Node - const toolbarContainsElementToFocus = toolbarRef.current && toolbarRef.current.contains(elementToBeFocused) - const linkEditorContainsElementToFocus = - linkEditorRef.current && - (linkEditorRef.current.contains(elementToBeFocused) || elementToBeFocused === linkEditorRef.current) - const willFocusBackspaceButton = backspaceButtonRef.current && elementToBeFocused === backspaceButtonRef.current - if (toolbarContainsElementToFocus || linkEditorContainsElementToFocus || willFocusBackspaceButton) { - return - } - setIsInEditor(false) - } - - rootElement.addEventListener('focus', handleFocus) - rootElement.addEventListener('blur', handleBlur) - - return () => { - rootElement.removeEventListener('focus', handleFocus) - rootElement.removeEventListener('blur', handleBlur) - } - }, [editor]) - - useEffect(() => { - const toolbar = toolbarRef.current - const linkEditor = linkEditorRef.current - const handleToolbarFocus = () => setIsInToolbar(true) - const handleLinkEditorFocus = () => setIsInLinkEditor(true) const handleToolbarBlur = (event: FocusEvent) => { const elementToBeFocused = event.relatedTarget as Node if (elementToBeFocused === backspaceButtonRef.current) { @@ -285,34 +308,42 @@ const MobileToolbarPlugin = () => { } setIsInToolbar(false) } - const handleLinkEditorBlur = (event: FocusEvent) => { + + const handleRootFocus = () => setIsInEditor(true) + const handleRootBlur = (event: FocusEvent) => { const elementToBeFocused = event.relatedTarget as Node - if (elementToBeFocused === backspaceButtonRef.current) { + + const containerContainsElementToFocus = container?.contains(elementToBeFocused) + + const willFocusBackspaceButton = backspaceButtonRef.current && elementToBeFocused === backspaceButtonRef.current + + if (containerContainsElementToFocus || willFocusBackspaceButton) { return } - setIsInLinkEditor(false) - } - if (toolbar) { - toolbar.addEventListener('focus', handleToolbarFocus) - toolbar.addEventListener('blur', handleToolbarBlur) + setIsInEditor(false) } - if (linkEditor) { - linkEditor.addEventListener('focus', handleLinkEditorFocus) - linkEditor.addEventListener('blur', handleLinkEditorBlur) + rootElement.addEventListener('focus', handleRootFocus) + rootElement.addEventListener('blur', handleRootBlur) + + if (container) { + container.addEventListener('focus', handleToolbarFocus) + container.addEventListener('blur', handleToolbarBlur) } return () => { - toolbar?.removeEventListener('focus', handleToolbarFocus) - toolbar?.removeEventListener('blur', handleToolbarBlur) - linkEditor?.removeEventListener('focus', handleLinkEditorFocus) - linkEditor?.removeEventListener('blur', handleLinkEditorBlur) + rootElement.removeEventListener('focus', handleRootFocus) + rootElement.removeEventListener('blur', handleRootBlur) + container?.removeEventListener('focus', handleToolbarFocus) + container?.removeEventListener('blur', handleToolbarBlur) } - }, []) - const [isSelectionAutoLink, setIsSelectionAutoLink] = useState(false) + }, [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(() => { @@ -329,18 +360,6 @@ const MobileToolbarPlugin = () => { const node = getSelectedNode(selection) const parent = node.getParent() - if ($isLinkNode(parent) || $isLinkNode(node)) { - setIsSelectionLink(true) - } else { - setIsSelectionLink(false) - } - - if ($isAutoLinkNode(parent) || $isAutoLinkNode(node)) { - setIsSelectionAutoLink(true) - } else { - setIsSelectionAutoLink(false) - } - if ($isLinkNode(parent)) { setLinkUrl(parent.getURL()) } else if ($isLinkNode(node)) { @@ -348,6 +367,11 @@ const MobileToolbarPlugin = () => { } else { setLinkUrl('') } + if ($isLinkTextNode(node, selection)) { + setLinkText(node.getTextContent()) + } else { + setLinkText('') + } if ( selection !== null && @@ -380,76 +404,147 @@ const MobileToolbarPlugin = () => { ) }, [editor, updateEditorSelection]) - const isFocusInEditorOrToolbar = isInEditor || isInToolbar || isInLinkEditor + useEffect(() => { + const container = containerRef.current + const rootElement = editor.getRootElement() + + if (!container || !rootElement) { + return + } + + const resizeObserver = new ResizeObserver(() => { + if (isMobile) { + return + } + + const containerHeight = container.offsetHeight + + rootElement.style.paddingBottom = containerHeight ? `${containerHeight + 16 * 2}px` : '' + }) + + resizeObserver.observe(container) + + return () => { + resizeObserver.disconnect() + } + }, [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}
- {isSelectionLink && ( -
- +
+ +
+
+ + )} + {isLink && ( + <> +
+ +
+
-
+ )} -
-
+ {items.map((item) => { return ( - - - + ) })} -
- + + {isMobile && ( + + )}
) } -export default MobileToolbarPlugin +export default ToolbarPlugin diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugins/useSelectedTextFormatInfo.ts b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/useSelectedTextFormatInfo.ts similarity index 98% rename from packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugins/useSelectedTextFormatInfo.ts rename to packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/useSelectedTextFormatInfo.ts index e209a4da9e8..a541b215bc6 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugins/useSelectedTextFormatInfo.ts +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/useSelectedTextFormatInfo.ts @@ -14,7 +14,7 @@ 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 '../LinkEditor/LinkTextEditor' +import { $isLinkTextNode } from './ToolbarLinkTextEditor' const blockTypeToBlockName = { bullet: 'Bulleted List', diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugins/FloatingTextFormatToolbarPlugin.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugins/FloatingTextFormatToolbarPlugin.tsx deleted file mode 100644 index 723a394a362..00000000000 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugins/FloatingTextFormatToolbarPlugin.tsx +++ /dev/null @@ -1,535 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link' -import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' -import { mergeRegister } from '@lexical/utils' -import { - $getSelection, - $isRangeSelection, - FORMAT_TEXT_COMMAND, - LexicalEditor, - SELECTION_CHANGE_COMMAND, - COMMAND_PRIORITY_LOW, - RangeSelection, - GridSelection, - NodeSelection, - KEY_MODIFIER_COMMAND, - COMMAND_PRIORITY_NORMAL, - createCommand, -} from 'lexical' -import { INSERT_UNORDERED_LIST_COMMAND, REMOVE_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND } from '@lexical/list' -import { ComponentPropsWithoutRef, useCallback, useEffect, useRef, useState } from 'react' -import { createPortal } from 'react-dom' -import { getSelectedNode } from '../../Lexical/Utils/getSelectedNode' -import { - BoldIcon, - ItalicIcon, - UnderlineIcon, - StrikethroughIcon, - CodeIcon, - LinkIcon, - SuperscriptIcon, - SubscriptIcon, - ListBulleted, - ListNumbered, - DrawIcon, -} from '@standardnotes/icons' -import { IconComponent } from '../../Lexical/Theme/IconComponent' -import { classNames } from '@standardnotes/snjs' -import { getDOMRangeRect } from '../../Lexical/Utils/getDOMRangeRect' -import { getPositionedPopoverStyles } from '@/Components/Popover/GetPositionedPopoverStyles' -import { getAdjustedStylesForNonPortalPopover } from '@/Components/Popover/Utils/getAdjustedStylesForNonPortal' -import LinkEditor from '../LinkEditor/LinkEditor' -import LinkTextEditor, { $isLinkTextNode } from '../LinkEditor/LinkTextEditor' -import { URL_REGEX } from '@/Constants/Constants' -import { useSelectedTextFormatInfo } from './useSelectedTextFormatInfo' -import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery' -import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip' - -const IconSize = 15 - -const TOGGLE_LINK_AND_EDIT_COMMAND = createCommand('TOGGLE_LINK_AND_EDIT_COMMAND') - -const ToolbarButton = ({ active, ...props }: { active?: boolean } & ComponentPropsWithoutRef<'button'>) => { - return ( - - ) -} - -function TextFormatFloatingToolbar({ - editor, - anchorElem, - isText, - isLink, - isLinkText, - isAutoLink, - isBold, - isItalic, - isUnderline, - isCode, - isStrikethrough, - isSubscript, - isSuperscript, - isBulletedList, - isNumberedList, - isHighlighted, -}: { - editor: LexicalEditor - anchorElem: HTMLElement - isText: boolean - isBold: boolean - isCode: boolean - isItalic: boolean - isLink: boolean - isLinkText: boolean - isAutoLink: boolean - isStrikethrough: boolean - isSubscript: boolean - isSuperscript: boolean - isUnderline: boolean - isBulletedList: boolean - isNumberedList: boolean - isHighlighted: boolean -}) { - const toolbarRef = useRef(null) - - const [linkUrl, setLinkUrl] = useState('') - const [linkText, setLinkText] = useState('') - const [isLinkEditMode, setIsLinkEditMode] = useState(false) - const [lastSelection, setLastSelection] = useState(null) - - useEffect(() => { - return editor.registerCommand( - TOGGLE_LINK_AND_EDIT_COMMAND, - (payload) => { - if (payload === null) { - return editor.dispatchCommand(TOGGLE_LINK_COMMAND, null) - } else if (typeof payload === 'string') { - const dispatched = editor.dispatchCommand(TOGGLE_LINK_COMMAND, payload) - setLinkUrl(payload) - setIsLinkEditMode(true) - return dispatched - } - return false - }, - COMMAND_PRIORITY_LOW, - ) - }, [editor]) - - const insertLink = useCallback(() => { - if (!isLink) { - editor.update(() => { - editor.dispatchCommand(TOGGLE_LINK_AND_EDIT_COMMAND, '') - }) - } else { - editor.dispatchCommand(TOGGLE_LINK_AND_EDIT_COMMAND, null) - } - }, [editor, isLink]) - - const formatBulletList = useCallback(() => { - if (!isBulletedList) { - editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined) - } else { - editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined) - } - }, [editor, isBulletedList]) - - const formatNumberedList = useCallback(() => { - if (!isNumberedList) { - editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined) - } else { - editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined) - } - }, [editor, isNumberedList]) - - const updateToolbar = useCallback(() => { - const selection = $getSelection() - if ($isRangeSelection(selection)) { - 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('') - } - } - - const toolbarElement = toolbarRef.current - - if (!toolbarElement) { - return - } - - const nativeSelection = window.getSelection() - const activeElement = document.activeElement - const rootElement = editor.getRootElement() - - if ( - selection !== null && - nativeSelection !== null && - rootElement !== null && - rootElement.contains(nativeSelection.anchorNode) - ) { - setLastSelection(selection) - - const rangeRect = getDOMRangeRect(nativeSelection, rootElement) - const toolbarRect = toolbarElement.getBoundingClientRect() - const rootElementRect = rootElement.getBoundingClientRect() - - const calculatedStyles = getPositionedPopoverStyles({ - align: 'start', - side: 'top', - anchorRect: rangeRect, - popoverRect: toolbarRect, - documentRect: rootElementRect, - offset: 12, - maxHeightFunction: () => 'none', - }) - if (calculatedStyles) { - toolbarElement.style.setProperty('--offset', calculatedStyles['--offset']) - } - - if (calculatedStyles) { - Object.assign(toolbarElement.style, calculatedStyles) - const adjustedStyles = getAdjustedStylesForNonPortalPopover(toolbarElement, calculatedStyles, rootElement) - toolbarElement.style.setProperty('--translate-x', adjustedStyles['--translate-x']) - toolbarElement.style.setProperty('--translate-y', adjustedStyles['--translate-y']) - } - } else if (!activeElement || activeElement.id !== 'link-input') { - setLastSelection(null) - setIsLinkEditMode(false) - setLinkUrl('') - } - - return true - }, [editor]) - - useEffect(() => { - const scrollerElem = editor.getRootElement() - - const update = () => { - editor.getEditorState().read(() => { - updateToolbar() - }) - } - - window.addEventListener('resize', update) - if (scrollerElem) { - scrollerElem.addEventListener('scroll', update) - } - - return () => { - window.removeEventListener('resize', update) - if (scrollerElem) { - scrollerElem.removeEventListener('scroll', update) - } - } - }, [editor, anchorElem, updateToolbar]) - - useEffect(() => { - editor.getEditorState().read(() => { - updateToolbar() - }) - return mergeRegister( - editor.registerUpdateListener(({ editorState }) => { - editorState.read(() => { - updateToolbar() - }) - }), - - editor.registerCommand( - SELECTION_CHANGE_COMMAND, - () => { - updateToolbar() - return false - }, - COMMAND_PRIORITY_LOW, - ), - ) - }, [editor, updateToolbar]) - - useEffect(() => { - return 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]) - - useEffect(() => { - editor.getEditorState().read(() => updateToolbar()) - }, [editor, isLink, isText, updateToolbar]) - - if (!editor.isEditable()) { - return null - } - - return ( -
- {isLinkText && !isAutoLink && ( - <> - -
- - )} - {isLink && ( - - )} - {isText && isLink && ( -
- )} - {isText && ( -
- - { - editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold') - }} - aria-label="Format text as bold" - > - - - - - - - { - editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic') - }} - active={isItalic} - aria-label="Format text as italics" - > - - - - - - - { - editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline') - }} - active={isUnderline} - aria-label="Format text to underlined" - > - - - - - - - { - editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough') - }} - active={isStrikethrough} - aria-label="Format text with a strikethrough" - > - - - - - - - { - editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript') - }} - active={isSubscript} - title="Subscript" - aria-label="Format Subscript" - > - - - - - - - { - editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript') - }} - active={isSuperscript} - title="Superscript" - aria-label="Format Superscript" - > - - - - - - - { - editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code') - }} - active={isCode} - aria-label="Insert code block" - > - - - - - - - { - editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'highlight') - }} - active={isHighlighted} - aria-label="Highlight text" - > - - - - - - - - - - - - - - - - - - - - - - - - - - -
- )} -
- ) -} - -function useFloatingTextFormatToolbar(editor: LexicalEditor, anchorElem: HTMLElement): JSX.Element | null { - const { - isText, - isLink, - isLinkText, - isAutoLink, - isBold, - isItalic, - isStrikethrough, - isSubscript, - isSuperscript, - isUnderline, - isCode, - isHighlighted, - blockType, - } = useSelectedTextFormatInfo() - - const isMobile = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm) - - if (isMobile) { - return null - } - - if (!isText && !isLink) { - return null - } - - return createPortal( - , - anchorElem, - ) -} - -export default function FloatingTextFormatToolbarPlugin({ - anchorElem = document.body, -}: { - anchorElem?: HTMLElement -}): JSX.Element | null { - const [editor] = useLexicalComposerContext() - return useFloatingTextFormatToolbar(editor, anchorElem) -} diff --git a/packages/web/src/javascripts/Components/SuperEditor/SuperEditor.tsx b/packages/web/src/javascripts/Components/SuperEditor/SuperEditor.tsx index c21d4d82776..a90f4f1f41e 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/SuperEditor.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/SuperEditor.tsx @@ -44,7 +44,7 @@ import ReadonlyPlugin from './Plugins/ReadonlyPlugin/ReadonlyPlugin' import { SuperSearchContextProvider } from './Plugins/SearchPlugin/Context' import { SearchPlugin } from './Plugins/SearchPlugin/SearchPlugin' import ModalOverlay from '@/Components/Modal/ModalOverlay' -import MobileToolbarPlugin from './Plugins/ToolbarPlugins/MobileToolbarPlugin' +import ToolbarPlugin from './Plugins/ToolbarPlugin/ToolbarPlugin' import CodeOptionsPlugin from './Plugins/CodeOptionsPlugin/CodeOptions' import RemoteImagePlugin from './Plugins/RemoteImagePlugin/RemoteImagePlugin' import NotEntitledBanner from '../ComponentView/NotEntitledBanner' @@ -250,7 +250,7 @@ export const SuperEditor: FunctionComponent = ({ - +