feat: Full toolbar is now shown in Super notes on non-mobile screens instead of floating formatting toolbar. It can be focused or toggled using Ctrl/Cmd+Shift+K (#2576)
This commit is contained in:
3
packages/icons/src/Icons/ic-indent.svg
Normal file
3
packages/icons/src/Icons/ic-indent.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||||
|
<path fill="currentColor" d="M2 3.5a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm.646 2.146a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1 0 .708l-2 2a.5.5 0 0 1-.708-.708L4.293 8L2.646 6.354a.5.5 0 0 1 0-.708zM7 6.5a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5zm0 3a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5zm-5 3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 464 B |
3
packages/icons/src/Icons/ic-outdent.svg
Normal file
3
packages/icons/src/Icons/ic-outdent.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||||
|
<path fill="currentColor" d="M2 3.5a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm10.646 2.146a.5.5 0 0 1 .708.708L11.707 8l1.647 1.646a.5.5 0 0 1-.708.708l-2-2a.5.5 0 0 1 0-.708l2-2zM2 6.5a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5zm0 3a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5zm0 3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 449 B |
@@ -104,6 +104,7 @@ import HistoryLockedIllustration from './il-history-locked.svg'
|
|||||||
import IconsSpriteStylekit from './icons-sprite-stylekit.svg'
|
import IconsSpriteStylekit from './icons-sprite-stylekit.svg'
|
||||||
import IlNotesIcon from './il-notes.svg'
|
import IlNotesIcon from './il-notes.svg'
|
||||||
import ImageIcon from './ic-image.svg'
|
import ImageIcon from './ic-image.svg'
|
||||||
|
import IndentIcon from './ic-indent.svg'
|
||||||
import InfoIcon from './ic-info.svg'
|
import InfoIcon from './ic-info.svg'
|
||||||
import ItalicIcon from './ic-italic.svg'
|
import ItalicIcon from './ic-italic.svg'
|
||||||
import KeyboardCloseIcon from './ic-keyboard-close.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 NotesFilledIcon from './ic-notes-filled.svg'
|
||||||
import NotesIcon from './ic-notes.svg'
|
import NotesIcon from './ic-notes.svg'
|
||||||
import OpenInIcon from './ic-open-in.svg'
|
import OpenInIcon from './ic-open-in.svg'
|
||||||
|
import OutdentIcon from './ic-outdent.svg'
|
||||||
import PasswordIcon from './ic-textbox-password.svg'
|
import PasswordIcon from './ic-textbox-password.svg'
|
||||||
import PencilFilledIcon from './ic-pencil-filled.svg'
|
import PencilFilledIcon from './ic-pencil-filled.svg'
|
||||||
import PencilIcon from './ic-pencil.svg'
|
import PencilIcon from './ic-pencil.svg'
|
||||||
@@ -188,8 +190,8 @@ import SubtractIcon from './ic-subtract.svg'
|
|||||||
import SuperscriptIcon from './ic-superscript.svg'
|
import SuperscriptIcon from './ic-superscript.svg'
|
||||||
import SyncIcon from './ic-sync.svg'
|
import SyncIcon from './ic-sync.svg'
|
||||||
import TasksIcon from './ic-tasks.svg'
|
import TasksIcon from './ic-tasks.svg'
|
||||||
import TextIcon from './ic-text.svg'
|
|
||||||
import TextCircleIcon from './ic-text-circle.svg'
|
import TextCircleIcon from './ic-text-circle.svg'
|
||||||
|
import TextIcon from './ic-text.svg'
|
||||||
import TextParagraphLongIcon from './ic-text-paragraph-long.svg'
|
import TextParagraphLongIcon from './ic-text-paragraph-long.svg'
|
||||||
import ThemesFilledIcon from './ic-themes-filled.svg'
|
import ThemesFilledIcon from './ic-themes-filled.svg'
|
||||||
import ThemesIcon from './ic-themes.svg'
|
import ThemesIcon from './ic-themes.svg'
|
||||||
@@ -221,6 +223,7 @@ export {
|
|||||||
AddBoldIcon,
|
AddBoldIcon,
|
||||||
AddIcon,
|
AddIcon,
|
||||||
AddTextIcon,
|
AddTextIcon,
|
||||||
|
AegisIcon,
|
||||||
ArchiveIcon,
|
ArchiveIcon,
|
||||||
ArrowDownCheckmarkIcon,
|
ArrowDownCheckmarkIcon,
|
||||||
ArrowDownIcon,
|
ArrowDownIcon,
|
||||||
@@ -274,6 +277,7 @@ export {
|
|||||||
EmailFilledIcon,
|
EmailFilledIcon,
|
||||||
EmailIcon,
|
EmailIcon,
|
||||||
EnterIcon,
|
EnterIcon,
|
||||||
|
EvernoteIcon,
|
||||||
EyeFilledIcon,
|
EyeFilledIcon,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
EyeOffFilledIcon,
|
EyeOffFilledIcon,
|
||||||
@@ -304,6 +308,7 @@ export {
|
|||||||
FullscreenExitIcon,
|
FullscreenExitIcon,
|
||||||
FullscreenIcon,
|
FullscreenIcon,
|
||||||
GiftOutlineIcon,
|
GiftOutlineIcon,
|
||||||
|
GoogleKeepIcon,
|
||||||
GroupIcon,
|
GroupIcon,
|
||||||
HashtagFilledIcon,
|
HashtagFilledIcon,
|
||||||
HashtagIcon,
|
HashtagIcon,
|
||||||
@@ -316,6 +321,7 @@ export {
|
|||||||
IconsSpriteStylekit,
|
IconsSpriteStylekit,
|
||||||
IlNotesIcon,
|
IlNotesIcon,
|
||||||
ImageIcon,
|
ImageIcon,
|
||||||
|
IndentIcon,
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
ItalicIcon,
|
ItalicIcon,
|
||||||
KeyboardCloseIcon,
|
KeyboardCloseIcon,
|
||||||
@@ -349,6 +355,7 @@ export {
|
|||||||
NotesFilledIcon,
|
NotesFilledIcon,
|
||||||
NotesIcon,
|
NotesIcon,
|
||||||
OpenInIcon,
|
OpenInIcon,
|
||||||
|
OutdentIcon,
|
||||||
PasswordIcon,
|
PasswordIcon,
|
||||||
PencilFilledIcon,
|
PencilFilledIcon,
|
||||||
PencilIcon,
|
PencilIcon,
|
||||||
@@ -364,6 +371,8 @@ export {
|
|||||||
ProtectedIllustration,
|
ProtectedIllustration,
|
||||||
RedoIcon,
|
RedoIcon,
|
||||||
ReorderIcon,
|
ReorderIcon,
|
||||||
|
ReplaceAllIcon,
|
||||||
|
ReplaceIcon,
|
||||||
RestoreIcon,
|
RestoreIcon,
|
||||||
RichTextIcon,
|
RichTextIcon,
|
||||||
SafeIcon,
|
SafeIcon,
|
||||||
@@ -382,6 +391,7 @@ export {
|
|||||||
ShortcutButtonIcon,
|
ShortcutButtonIcon,
|
||||||
SignInIcon,
|
SignInIcon,
|
||||||
SignOutIcon,
|
SignOutIcon,
|
||||||
|
SimplenoteIcon,
|
||||||
SNLogoAltIcon,
|
SNLogoAltIcon,
|
||||||
SNLogoFull,
|
SNLogoFull,
|
||||||
SNLogoIcon,
|
SNLogoIcon,
|
||||||
@@ -397,8 +407,8 @@ export {
|
|||||||
SuperscriptIcon,
|
SuperscriptIcon,
|
||||||
SyncIcon,
|
SyncIcon,
|
||||||
TasksIcon,
|
TasksIcon,
|
||||||
TextIcon,
|
|
||||||
TextCircleIcon,
|
TextCircleIcon,
|
||||||
|
TextIcon,
|
||||||
TextParagraphLongIcon,
|
TextParagraphLongIcon,
|
||||||
ThemesFilledIcon,
|
ThemesFilledIcon,
|
||||||
ThemesIcon,
|
ThemesIcon,
|
||||||
@@ -420,10 +430,4 @@ export {
|
|||||||
ViewIcon,
|
ViewIcon,
|
||||||
WarningIcon,
|
WarningIcon,
|
||||||
WindowIcon,
|
WindowIcon,
|
||||||
EvernoteIcon,
|
|
||||||
GoogleKeepIcon,
|
|
||||||
SimplenoteIcon,
|
|
||||||
AegisIcon,
|
|
||||||
ReplaceIcon,
|
|
||||||
ReplaceAllIcon,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="bi bi-text-indent-left"><path d="M2 3.5a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm.646 2.146a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1 0 .708l-2 2a.5.5 0 0 1-.708-.708L4.293 8 2.646 6.354a.5.5 0 0 1 0-.708zM7 6.5a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5zm0 3a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5zm-5 3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 490 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="bi bi-text-indent-right"><path d="M2 3.5a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm10.646 2.146a.5.5 0 0 1 .708.708L11.707 8l1.647 1.646a.5.5 0 0 1-.708.708l-2-2a.5.5 0 0 1 0-.708l2-2zM2 6.5a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5zm0 3a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5zm0 3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 476 B |
@@ -27,6 +27,7 @@ export const STAR_NOTE_COMMAND = createKeyboardCommand('STAR_NOTE_COMMAND')
|
|||||||
export const PIN_NOTE_COMMAND = createKeyboardCommand('PIN_NOTE_COMMAND')
|
export const PIN_NOTE_COMMAND = createKeyboardCommand('PIN_NOTE_COMMAND')
|
||||||
|
|
||||||
export const SUPER_TOGGLE_SEARCH = createKeyboardCommand('SUPER_TOGGLE_SEARCH')
|
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_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_TOGGLE_REPLACE_MODE = createKeyboardCommand('SUPER_SEARCH_TOGGLE_REPLACE_MODE')
|
||||||
export const SUPER_SEARCH_NEXT_RESULT = createKeyboardCommand('SUPER_SEARCH_NEXT_RESULT')
|
export const SUPER_SEARCH_NEXT_RESULT = createKeyboardCommand('SUPER_SEARCH_NEXT_RESULT')
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
SUPER_SEARCH_PREVIOUS_RESULT,
|
SUPER_SEARCH_PREVIOUS_RESULT,
|
||||||
SUPER_SEARCH_TOGGLE_REPLACE_MODE,
|
SUPER_SEARCH_TOGGLE_REPLACE_MODE,
|
||||||
CHANGE_EDITOR_WIDTH_COMMAND,
|
CHANGE_EDITOR_WIDTH_COMMAND,
|
||||||
|
SUPER_TOGGLE_TOOLBAR,
|
||||||
} from './KeyboardCommands'
|
} from './KeyboardCommands'
|
||||||
import { KeyboardKey } from './KeyboardKey'
|
import { KeyboardKey } from './KeyboardKey'
|
||||||
import { KeyboardModifier } from './KeyboardModifier'
|
import { KeyboardModifier } from './KeyboardModifier'
|
||||||
@@ -147,6 +148,11 @@ export function getKeyboardShortcuts(platform: Platform, _environment: Environme
|
|||||||
modifiers: [primaryModifier, KeyboardModifier.Shift],
|
modifiers: [primaryModifier, KeyboardModifier.Shift],
|
||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
command: SUPER_TOGGLE_TOOLBAR,
|
||||||
|
key: 'k',
|
||||||
|
modifiers: [primaryModifier, KeyboardModifier.Shift],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
command: SUPER_TOGGLE_SEARCH,
|
command: SUPER_TOGGLE_SEARCH,
|
||||||
key: 'f',
|
key: 'f',
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ export const IconNameToSvgMapping = {
|
|||||||
'fullscreen-exit': icons.FullscreenExitIcon,
|
'fullscreen-exit': icons.FullscreenExitIcon,
|
||||||
'hashtag-off': icons.HashtagOffIcon,
|
'hashtag-off': icons.HashtagOffIcon,
|
||||||
'keyboard-close': icons.KeyboardCloseIcon,
|
'keyboard-close': icons.KeyboardCloseIcon,
|
||||||
'link-off': icons.LinkOffIcon,
|
|
||||||
'line-width': icons.LineWidthIcon,
|
'line-width': icons.LineWidthIcon,
|
||||||
|
'link-off': icons.LinkOffIcon,
|
||||||
'list-bulleted': icons.ListBulleted,
|
'list-bulleted': icons.ListBulleted,
|
||||||
'list-numbered': icons.ListNumbered,
|
'list-numbered': icons.ListNumbered,
|
||||||
'lock-filled': icons.LockFilledIcon,
|
'lock-filled': icons.LockFilledIcon,
|
||||||
@@ -99,17 +99,19 @@ export const IconNameToSvgMapping = {
|
|||||||
hashtag: icons.HashtagIcon,
|
hashtag: icons.HashtagIcon,
|
||||||
help: icons.HelpIcon,
|
help: icons.HelpIcon,
|
||||||
history: icons.HistoryIcon,
|
history: icons.HistoryIcon,
|
||||||
|
image: icons.ImageIcon,
|
||||||
|
indent: icons.IndentIcon,
|
||||||
info: icons.InfoIcon,
|
info: icons.InfoIcon,
|
||||||
italic: icons.ItalicIcon,
|
italic: icons.ItalicIcon,
|
||||||
keyboard: icons.KeyboardIcon,
|
keyboard: icons.KeyboardIcon,
|
||||||
link: icons.LinkIcon,
|
link: icons.LinkIcon,
|
||||||
listed: icons.ListedIcon,
|
listed: icons.ListedIcon,
|
||||||
lock: icons.LockIcon,
|
lock: icons.LockIcon,
|
||||||
image: icons.ImageIcon,
|
|
||||||
markdown: icons.MarkdownIcon,
|
markdown: icons.MarkdownIcon,
|
||||||
merge: icons.MergeIcon,
|
merge: icons.MergeIcon,
|
||||||
more: icons.MoreIcon,
|
more: icons.MoreIcon,
|
||||||
notes: icons.NotesIcon,
|
notes: icons.NotesIcon,
|
||||||
|
outdent: icons.OutdentIcon,
|
||||||
paragraph: icons.TextParagraphLongIcon,
|
paragraph: icons.TextParagraphLongIcon,
|
||||||
password: icons.PasswordIcon,
|
password: icons.PasswordIcon,
|
||||||
pencil: icons.PencilIcon,
|
pencil: icons.PencilIcon,
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import AutoEmbedPlugin from './Plugins/AutoEmbedPlugin'
|
|||||||
import CollapsiblePlugin from './Plugins/CollapsiblePlugin'
|
import CollapsiblePlugin from './Plugins/CollapsiblePlugin'
|
||||||
import DraggableBlockPlugin from './Plugins/DraggableBlockPlugin'
|
import DraggableBlockPlugin from './Plugins/DraggableBlockPlugin'
|
||||||
import CodeHighlightPlugin from './Plugins/CodeHighlightPlugin'
|
import CodeHighlightPlugin from './Plugins/CodeHighlightPlugin'
|
||||||
import FloatingTextFormatToolbarPlugin from './Plugins/ToolbarPlugins/FloatingTextFormatToolbarPlugin'
|
|
||||||
import { TabIndentationPlugin } from './Plugins/TabIndentationPlugin'
|
import { TabIndentationPlugin } from './Plugins/TabIndentationPlugin'
|
||||||
import { handleEditorChange } from './Utils'
|
import { handleEditorChange } from './Utils'
|
||||||
import { SuperEditorContentId } from './Constants'
|
import { SuperEditorContentId } from './Constants'
|
||||||
@@ -110,7 +109,6 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
|
|||||||
<RemoveBrokenTablesPlugin />
|
<RemoveBrokenTablesPlugin />
|
||||||
{!readonly && floatingAnchorElem && (
|
{!readonly && floatingAnchorElem && (
|
||||||
<>
|
<>
|
||||||
<FloatingTextFormatToolbarPlugin anchorElem={floatingAnchorElem} />
|
|
||||||
<DraggableBlockPlugin anchorElem={floatingAnchorElem} />
|
<DraggableBlockPlugin anchorElem={floatingAnchorElem} />
|
||||||
<TableActionMenuPlugin anchorElem={floatingAnchorElem} cellMerge />
|
<TableActionMenuPlugin anchorElem={floatingAnchorElem} cellMerge />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ export function GetIndentOutdentBlocks(editor: LexicalEditor) {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'Indent',
|
name: 'Indent',
|
||||||
iconName: 'arrow-right',
|
iconName: 'indent',
|
||||||
keywords: ['indent'],
|
keywords: ['indent'],
|
||||||
onSelect: () => editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined),
|
onSelect: () => editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Outdent',
|
name: 'Outdent',
|
||||||
iconName: 'arrow-left',
|
iconName: 'outdent',
|
||||||
keywords: ['outdent'],
|
keywords: ['outdent'],
|
||||||
onSelect: () => editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined),
|
onSelect: () => editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import Icon from '@/Components/Icon/Icon'
|
import Icon from '@/Components/Icon/Icon'
|
||||||
import { CloseIcon, CheckIcon, PencilFilledIcon, TrashFilledIcon } from '@standardnotes/icons'
|
|
||||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||||
import { IconComponent } from '../../Lexical/Theme/IconComponent'
|
|
||||||
import { sanitizeUrl } from '../../Lexical/Utils/sanitizeUrl'
|
import { sanitizeUrl } from '../../Lexical/Utils/sanitizeUrl'
|
||||||
import { TOGGLE_LINK_COMMAND } from '@lexical/link'
|
import { TOGGLE_LINK_COMMAND } from '@lexical/link'
|
||||||
import { useCallback, useState, useRef, useEffect } from 'react'
|
import { useCallback, useState, useRef, useEffect } from 'react'
|
||||||
import { GridSelection, LexicalEditor, NodeSelection, RangeSelection } from 'lexical'
|
import { GridSelection, LexicalEditor, NodeSelection, RangeSelection } from 'lexical'
|
||||||
import { classNames } from '@standardnotes/snjs'
|
import { classNames } from '@standardnotes/snjs'
|
||||||
|
import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
linkUrl: string
|
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]"
|
className="flex-grow rounded-sm bg-contrast p-1 text-text sm:min-w-[40ch]"
|
||||||
/>
|
/>
|
||||||
<button
|
<StyledTooltip showOnMobile showOnHover label="Cancel editing">
|
||||||
className="flex rounded-lg p-3 hover:bg-contrast hover:text-text disabled:cursor-not-allowed"
|
<button
|
||||||
onClick={() => {
|
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||||
setEditMode(false)
|
onClick={() => {
|
||||||
editor.focus()
|
setEditMode(false)
|
||||||
}}
|
editor.focus()
|
||||||
aria-label="Cancel editing link"
|
}}
|
||||||
onMouseDown={(event) => event.preventDefault()}
|
onMouseDown={(event) => event.preventDefault()}
|
||||||
>
|
>
|
||||||
<IconComponent size={15}>
|
<Icon type="close" size="medium" />
|
||||||
<CloseIcon />
|
</button>
|
||||||
</IconComponent>
|
</StyledTooltip>
|
||||||
</button>
|
<StyledTooltip showOnMobile showOnHover label="Save link">
|
||||||
<button
|
<button
|
||||||
className="flex rounded-lg p-3 hover:bg-contrast hover:text-text disabled:cursor-not-allowed"
|
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||||
onClick={handleLinkSubmission}
|
onClick={handleLinkSubmission}
|
||||||
aria-label="Save link"
|
onMouseDown={(event) => event.preventDefault()}
|
||||||
onMouseDown={(event) => event.preventDefault()}
|
>
|
||||||
>
|
<Icon type="check" size="medium" />
|
||||||
<IconComponent size={15}>
|
</button>
|
||||||
<CheckIcon />
|
</StyledTooltip>
|
||||||
</IconComponent>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
@@ -105,31 +102,29 @@ const LinkEditor = ({ linkUrl, isEditMode, setEditMode, editor, lastSelection, i
|
|||||||
</a>
|
</a>
|
||||||
{!isAutoLink && (
|
{!isAutoLink && (
|
||||||
<>
|
<>
|
||||||
<button
|
<StyledTooltip showOnMobile showOnHover label="Edit link">
|
||||||
className="flex rounded-lg p-3 hover:bg-contrast hover:text-text disabled:cursor-not-allowed"
|
<button
|
||||||
onClick={() => {
|
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||||
setEditedLinkUrl(linkUrl)
|
onClick={() => {
|
||||||
setEditMode(true)
|
setEditedLinkUrl(linkUrl)
|
||||||
}}
|
setEditMode(true)
|
||||||
aria-label="Edit link"
|
}}
|
||||||
onMouseDown={(event) => event.preventDefault()}
|
onMouseDown={(event) => event.preventDefault()}
|
||||||
>
|
>
|
||||||
<IconComponent size={15}>
|
<Icon type="pencil-filled" size="medium" />
|
||||||
<PencilFilledIcon />
|
</button>
|
||||||
</IconComponent>
|
</StyledTooltip>
|
||||||
</button>
|
<StyledTooltip showOnMobile showOnHover label="Remove link">
|
||||||
<button
|
<button
|
||||||
className="flex rounded-lg p-3 hover:bg-contrast hover:text-text disabled:cursor-not-allowed"
|
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
|
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
|
||||||
}}
|
}}
|
||||||
aria-label="Remove link"
|
onMouseDown={(event) => event.preventDefault()}
|
||||||
onMouseDown={(event) => event.preventDefault()}
|
>
|
||||||
>
|
<Icon type="trash-filled" size="medium" />
|
||||||
<IconComponent size={15}>
|
</button>
|
||||||
<TrashFilledIcon />
|
</StyledTooltip>
|
||||||
</IconComponent>
|
|
||||||
</button>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
import Icon from '@/Components/Icon/Icon'
|
import Icon from '@/Components/Icon/Icon'
|
||||||
import { CloseIcon, CheckIcon, PencilFilledIcon } from '@standardnotes/icons'
|
|
||||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||||
import { IconComponent } from '../../Lexical/Theme/IconComponent'
|
|
||||||
import { $isRangeSelection, $isTextNode, GridSelection, LexicalEditor, NodeSelection, RangeSelection } from 'lexical'
|
import { $isRangeSelection, $isTextNode, GridSelection, LexicalEditor, NodeSelection, RangeSelection } from 'lexical'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { VisuallyHidden } from '@ariakit/react'
|
import { VisuallyHidden } from '@ariakit/react'
|
||||||
import { getSelectedNode } from '../../Lexical/Utils/getSelectedNode'
|
import { getSelectedNode } from '../../Lexical/Utils/getSelectedNode'
|
||||||
import { $isLinkNode } from '@lexical/link'
|
import { $isLinkNode } from '@lexical/link'
|
||||||
|
import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
linkText: string
|
linkText: string
|
||||||
editor: LexicalEditor
|
editor: LexicalEditor
|
||||||
lastSelection: RangeSelection | GridSelection | NodeSelection | null
|
lastSelection: RangeSelection | GridSelection | NodeSelection | null
|
||||||
|
isEditMode: boolean
|
||||||
|
setEditMode: (isEditMode: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const $isLinkTextNode = (node: ReturnType<typeof getSelectedNode>, selection: RangeSelection) => {
|
export const $isLinkTextNode = (node: ReturnType<typeof getSelectedNode>, selection: RangeSelection) => {
|
||||||
@@ -19,9 +20,8 @@ export const $isLinkTextNode = (node: ReturnType<typeof getSelectedNode>, select
|
|||||||
return $isLinkNode(parent) && $isTextNode(node) && selection.anchor.getNode() === selection.focus.getNode()
|
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 [editedLinkText, setEditedLinkText] = useState(() => linkText)
|
||||||
const [isEditMode, setEditMode] = useState(false)
|
|
||||||
const editModeContainer = useRef<HTMLDivElement>(null)
|
const editModeContainer = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
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]"
|
className="flex-grow rounded-sm bg-contrast p-1 text-text sm:min-w-[20ch]"
|
||||||
/>
|
/>
|
||||||
<button
|
<StyledTooltip showOnMobile showOnHover label="Cancel editing">
|
||||||
className="flex rounded-lg p-3 hover:bg-contrast hover:text-text disabled:cursor-not-allowed"
|
<button
|
||||||
onClick={() => {
|
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||||
setEditMode(false)
|
onClick={() => {
|
||||||
editor.focus()
|
setEditMode(false)
|
||||||
}}
|
editor.focus()
|
||||||
aria-label="Cancel editing link"
|
}}
|
||||||
onMouseDown={(event) => event.preventDefault()}
|
onMouseDown={(event) => event.preventDefault()}
|
||||||
>
|
>
|
||||||
<IconComponent size={15}>
|
<Icon type="close" size="medium" />
|
||||||
<CloseIcon />
|
</button>
|
||||||
</IconComponent>
|
</StyledTooltip>
|
||||||
</button>
|
<StyledTooltip showOnMobile showOnHover label="Save link text">
|
||||||
<button
|
<button
|
||||||
className="flex rounded-lg p-3 hover:bg-contrast hover:text-text disabled:cursor-not-allowed"
|
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||||
onClick={handleLinkTextSubmission}
|
onClick={handleLinkTextSubmission}
|
||||||
aria-label="Save link"
|
onMouseDown={(event) => event.preventDefault()}
|
||||||
onMouseDown={(event) => event.preventDefault()}
|
>
|
||||||
>
|
<Icon type="check" size="medium" />
|
||||||
<IconComponent size={15}>
|
</button>
|
||||||
<CheckIcon />
|
</StyledTooltip>
|
||||||
</IconComponent>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Icon type="plain-text" className="ml-1 mr-1 flex-shrink-0" />
|
<Icon type="plain-text" className="ml-1 mr-1 flex-shrink-0" />
|
||||||
<div className="flex-grow max-w-[35ch] overflow-hidden text-ellipsis whitespace-nowrap">
|
<div className="flex-grow overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
<VisuallyHidden>Link text:</VisuallyHidden>
|
<VisuallyHidden>Link text:</VisuallyHidden>
|
||||||
{linkText}
|
{linkText}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<StyledTooltip showOnMobile showOnHover label="Edit link text">
|
||||||
className="flex rounded-lg p-3 hover:bg-contrast hover:text-text disabled:cursor-not-allowed ml-auto"
|
<button
|
||||||
onClick={() => {
|
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||||
setEditedLinkText(linkText)
|
onClick={() => {
|
||||||
setEditMode(true)
|
setEditedLinkText(linkText)
|
||||||
}}
|
setEditMode(true)
|
||||||
aria-label="Edit link"
|
}}
|
||||||
onMouseDown={(event) => event.preventDefault()}
|
onMouseDown={(event) => event.preventDefault()}
|
||||||
>
|
>
|
||||||
<IconComponent size={15}>
|
<Icon type="pencil-filled" size="medium" />
|
||||||
<PencilFilledIcon />
|
</button>
|
||||||
</IconComponent>
|
</StyledTooltip>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -18,8 +18,8 @@ import {
|
|||||||
UNDO_COMMAND,
|
UNDO_COMMAND,
|
||||||
} from 'lexical'
|
} from 'lexical'
|
||||||
import { mergeRegister } from '@lexical/utils'
|
import { mergeRegister } from '@lexical/utils'
|
||||||
import { $isLinkNode, $isAutoLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'
|
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { ComponentPropsWithoutRef, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { GetAlignmentBlocks } from '../Blocks/Alignment'
|
import { GetAlignmentBlocks } from '../Blocks/Alignment'
|
||||||
import { GetBulletedListBlock } from '../Blocks/BulletedList'
|
import { GetBulletedListBlock } from '../Blocks/BulletedList'
|
||||||
import { GetChecklistBlock } from '../Blocks/Checklist'
|
import { GetChecklistBlock } from '../Blocks/Checklist'
|
||||||
@@ -37,25 +37,65 @@ import { GetQuoteBlock } from '../Blocks/Quote'
|
|||||||
import { GetTableBlock } from '../Blocks/Table'
|
import { GetTableBlock } from '../Blocks/Table'
|
||||||
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||||
import { classNames } from '@standardnotes/snjs'
|
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 { useApplication } from '@/Components/ApplicationProvider'
|
||||||
import { GetRemoteImageBlock } from '../Blocks/RemoteImage'
|
import { GetRemoteImageBlock } from '../Blocks/RemoteImage'
|
||||||
import { InsertRemoteImageDialog } from '../RemoteImagePlugin/RemoteImagePlugin'
|
import { InsertRemoteImageDialog } from '../RemoteImagePlugin/RemoteImagePlugin'
|
||||||
import LinkEditor from '../LinkEditor/LinkEditor'
|
import LinkEditor from './ToolbarLinkEditor'
|
||||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
|
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
|
||||||
import { useSelectedTextFormatInfo } from './useSelectedTextFormatInfo'
|
import { useSelectedTextFormatInfo } from './useSelectedTextFormatInfo'
|
||||||
import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip'
|
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 (
|
||||||
|
<StyledTooltip showOnMobile showOnHover label={name}>
|
||||||
|
<ToolbarItem
|
||||||
|
className="flex select-none items-center justify-center rounded p-0.5 focus:shadow-none focus:outline-none enabled:hover:bg-default enabled:focus-visible:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
onSelect()
|
||||||
|
}}
|
||||||
|
onContextMenu={(event) => {
|
||||||
|
editor.focus()
|
||||||
|
event.preventDefault()
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'flex items-center justify-center rounded p-2 transition-colors duration-75',
|
||||||
|
active && 'bg-info text-info-contrast',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon type={iconName} size="medium" className="!text-current [&>path]:!text-current" />
|
||||||
|
</div>
|
||||||
|
</ToolbarItem>
|
||||||
|
</StyledTooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToolbarPlugin = () => {
|
||||||
const application = useApplication()
|
const application = useApplication()
|
||||||
const [editor] = useLexicalComposerContext()
|
const [editor] = useLexicalComposerContext()
|
||||||
const [modal, showModal] = useModal()
|
const [modal, showModal] = useModal()
|
||||||
|
|
||||||
const [isInEditor, setIsInEditor] = useState(false)
|
const [isInEditor, setIsInEditor] = useState(false)
|
||||||
const [isInLinkEditor, setIsInLinkEditor] = useState(false)
|
|
||||||
const [isInToolbar, setIsInToolbar] = useState(false)
|
const [isInToolbar, setIsInToolbar] = useState(false)
|
||||||
const isMobile = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
|
const isMobile = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
|
||||||
|
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const toolbarRef = useRef<HTMLDivElement>(null)
|
const toolbarRef = useRef<HTMLDivElement>(null)
|
||||||
const linkEditorRef = useRef<HTMLDivElement>(null)
|
const linkEditorRef = useRef<HTMLDivElement>(null)
|
||||||
const backspaceButtonRef = useRef<HTMLButtonElement>(null)
|
const backspaceButtonRef = useRef<HTMLButtonElement>(null)
|
||||||
@@ -77,9 +117,19 @@ const MobileToolbarPlugin = () => {
|
|||||||
}
|
}
|
||||||
}, [editor])
|
}, [editor])
|
||||||
|
|
||||||
const { isBold, isItalic, isUnderline, isSubscript, isSuperscript, isStrikethrough, blockType, isHighlighted } =
|
const {
|
||||||
useSelectedTextFormatInfo()
|
isBold,
|
||||||
const [isSelectionLink, setIsSelectionLink] = useState(false)
|
isItalic,
|
||||||
|
isUnderline,
|
||||||
|
isSubscript,
|
||||||
|
isSuperscript,
|
||||||
|
isStrikethrough,
|
||||||
|
blockType,
|
||||||
|
isHighlighted,
|
||||||
|
isLink,
|
||||||
|
isLinkText,
|
||||||
|
isAutoLink,
|
||||||
|
} = useSelectedTextFormatInfo()
|
||||||
|
|
||||||
const [canUndo, setCanUndo] = useState(false)
|
const [canUndo, setCanUndo] = useState(false)
|
||||||
const [canRedo, setCanRedo] = useState(false)
|
const [canRedo, setCanRedo] = useState(false)
|
||||||
@@ -193,7 +243,7 @@ const MobileToolbarPlugin = () => {
|
|||||||
insertLink()
|
insertLink()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
active: isSelectionLink,
|
active: isLink,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Search',
|
name: 'Search',
|
||||||
@@ -205,6 +255,7 @@ const MobileToolbarPlugin = () => {
|
|||||||
GetParagraphBlock(editor),
|
GetParagraphBlock(editor),
|
||||||
...GetHeadingsBlocks(editor),
|
...GetHeadingsBlocks(editor),
|
||||||
...GetIndentOutdentBlocks(editor),
|
...GetIndentOutdentBlocks(editor),
|
||||||
|
...GetAlignmentBlocks(editor),
|
||||||
GetTableBlock(() =>
|
GetTableBlock(() =>
|
||||||
showModal('Insert Table', (onClose) => <InsertTableDialog activeEditor={editor} onClose={onClose} />),
|
showModal('Insert Table', (onClose) => <InsertTableDialog activeEditor={editor} onClose={onClose} />),
|
||||||
),
|
),
|
||||||
@@ -218,7 +269,6 @@ const MobileToolbarPlugin = () => {
|
|||||||
GetCodeBlock(editor),
|
GetCodeBlock(editor),
|
||||||
GetDividerBlock(editor),
|
GetDividerBlock(editor),
|
||||||
...GetDatetimeBlocks(editor),
|
...GetDatetimeBlocks(editor),
|
||||||
...GetAlignmentBlocks(editor),
|
|
||||||
...[GetPasswordBlock(editor)],
|
...[GetPasswordBlock(editor)],
|
||||||
GetCollapsibleBlock(editor),
|
GetCollapsibleBlock(editor),
|
||||||
...GetEmbedsBlocks(editor),
|
...GetEmbedsBlocks(editor),
|
||||||
@@ -233,7 +283,7 @@ const MobileToolbarPlugin = () => {
|
|||||||
isBold,
|
isBold,
|
||||||
isHighlighted,
|
isHighlighted,
|
||||||
isItalic,
|
isItalic,
|
||||||
isSelectionLink,
|
isLink,
|
||||||
isStrikethrough,
|
isStrikethrough,
|
||||||
isSubscript,
|
isSubscript,
|
||||||
isSuperscript,
|
isSuperscript,
|
||||||
@@ -243,41 +293,14 @@ const MobileToolbarPlugin = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const container = containerRef.current
|
||||||
const rootElement = editor.getRootElement()
|
const rootElement = editor.getRootElement()
|
||||||
|
|
||||||
if (!rootElement) {
|
if (!rootElement) {
|
||||||
return
|
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 handleToolbarFocus = () => setIsInToolbar(true)
|
||||||
const handleLinkEditorFocus = () => setIsInLinkEditor(true)
|
|
||||||
const handleToolbarBlur = (event: FocusEvent) => {
|
const handleToolbarBlur = (event: FocusEvent) => {
|
||||||
const elementToBeFocused = event.relatedTarget as Node
|
const elementToBeFocused = event.relatedTarget as Node
|
||||||
if (elementToBeFocused === backspaceButtonRef.current) {
|
if (elementToBeFocused === backspaceButtonRef.current) {
|
||||||
@@ -285,34 +308,42 @@ const MobileToolbarPlugin = () => {
|
|||||||
}
|
}
|
||||||
setIsInToolbar(false)
|
setIsInToolbar(false)
|
||||||
}
|
}
|
||||||
const handleLinkEditorBlur = (event: FocusEvent) => {
|
|
||||||
|
const handleRootFocus = () => setIsInEditor(true)
|
||||||
|
const handleRootBlur = (event: FocusEvent) => {
|
||||||
const elementToBeFocused = event.relatedTarget as Node
|
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
|
return
|
||||||
}
|
}
|
||||||
setIsInLinkEditor(false)
|
|
||||||
|
setIsInEditor(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolbar) {
|
rootElement.addEventListener('focus', handleRootFocus)
|
||||||
toolbar.addEventListener('focus', handleToolbarFocus)
|
rootElement.addEventListener('blur', handleRootBlur)
|
||||||
toolbar.addEventListener('blur', handleToolbarBlur)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (linkEditor) {
|
if (container) {
|
||||||
linkEditor.addEventListener('focus', handleLinkEditorFocus)
|
container.addEventListener('focus', handleToolbarFocus)
|
||||||
linkEditor.addEventListener('blur', handleLinkEditorBlur)
|
container.addEventListener('blur', handleToolbarBlur)
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
toolbar?.removeEventListener('focus', handleToolbarFocus)
|
rootElement.removeEventListener('focus', handleRootFocus)
|
||||||
toolbar?.removeEventListener('blur', handleToolbarBlur)
|
rootElement.removeEventListener('blur', handleRootBlur)
|
||||||
linkEditor?.removeEventListener('focus', handleLinkEditorFocus)
|
container?.removeEventListener('focus', handleToolbarFocus)
|
||||||
linkEditor?.removeEventListener('blur', handleLinkEditorBlur)
|
container?.removeEventListener('blur', handleToolbarBlur)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [editor])
|
||||||
const [isSelectionAutoLink, setIsSelectionAutoLink] = useState(false)
|
|
||||||
const [linkUrl, setLinkUrl] = useState('')
|
const [linkUrl, setLinkUrl] = useState('')
|
||||||
|
const [linkText, setLinkText] = useState('')
|
||||||
const [isLinkEditMode, setIsLinkEditMode] = useState(false)
|
const [isLinkEditMode, setIsLinkEditMode] = useState(false)
|
||||||
|
const [isLinkTextEditMode, setIsLinkTextEditMode] = useState(false)
|
||||||
const [lastSelection, setLastSelection] = useState<RangeSelection | GridSelection | NodeSelection | null>(null)
|
const [lastSelection, setLastSelection] = useState<RangeSelection | GridSelection | NodeSelection | null>(null)
|
||||||
|
|
||||||
const updateEditorSelection = useCallback(() => {
|
const updateEditorSelection = useCallback(() => {
|
||||||
@@ -329,18 +360,6 @@ const MobileToolbarPlugin = () => {
|
|||||||
const node = getSelectedNode(selection)
|
const node = getSelectedNode(selection)
|
||||||
const parent = node.getParent()
|
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)) {
|
if ($isLinkNode(parent)) {
|
||||||
setLinkUrl(parent.getURL())
|
setLinkUrl(parent.getURL())
|
||||||
} else if ($isLinkNode(node)) {
|
} else if ($isLinkNode(node)) {
|
||||||
@@ -348,6 +367,11 @@ const MobileToolbarPlugin = () => {
|
|||||||
} else {
|
} else {
|
||||||
setLinkUrl('')
|
setLinkUrl('')
|
||||||
}
|
}
|
||||||
|
if ($isLinkTextNode(node, selection)) {
|
||||||
|
setLinkText(node.getTextContent())
|
||||||
|
} else {
|
||||||
|
setLinkText('')
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
selection !== null &&
|
selection !== null &&
|
||||||
@@ -380,76 +404,147 @@ const MobileToolbarPlugin = () => {
|
|||||||
)
|
)
|
||||||
}, [editor, updateEditorSelection])
|
}, [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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{modal}
|
{modal}
|
||||||
<div
|
<div
|
||||||
className={classNames('bg-contrast', !isMobile || !isFocusInEditorOrToolbar ? 'hidden' : '')}
|
className={classNames(
|
||||||
id="super-mobile-toolbar"
|
'bg-contrast',
|
||||||
>
|
'md:absolute md:bottom-4 md:left-1/2 md:max-w-[60%] md:-translate-x-1/2 md:rounded-lg md:border md:border-border md:px-2 md:py-1 md:translucent-ui:border-[--popover-border-color] md:translucent-ui:bg-[--popover-background-color] md:translucent-ui:[backdrop-filter:var(--popover-backdrop-filter)]',
|
||||||
{isSelectionLink && (
|
!canShowToolbar ? 'hidden' : '',
|
||||||
<div
|
|
||||||
className="border-t border-border px-2 focus:shadow-none focus:outline-none"
|
|
||||||
ref={linkEditorRef}
|
|
||||||
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
|
||||||
>
|
|
||||||
<LinkEditor
|
|
||||||
linkUrl={linkUrl}
|
|
||||||
isEditMode={isLinkEditMode}
|
|
||||||
setEditMode={setIsLinkEditMode}
|
|
||||||
isAutoLink={isSelectionAutoLink}
|
|
||||||
editor={editor}
|
|
||||||
lastSelection={lastSelection}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<div className="flex w-full flex-shrink-0 border-t border-border bg-contrast">
|
id="super-mobile-toolbar"
|
||||||
<div
|
ref={containerRef}
|
||||||
tabIndex={-1}
|
>
|
||||||
|
{isLinkText && !isAutoLink && (
|
||||||
|
<>
|
||||||
|
<div className="border-t border-border px-1 py-1 md:border-0 md:px-0 md:py-0">
|
||||||
|
<LinkTextEditor
|
||||||
|
linkText={linkText}
|
||||||
|
editor={editor}
|
||||||
|
lastSelection={lastSelection}
|
||||||
|
isEditMode={isLinkTextEditMode}
|
||||||
|
setEditMode={setIsLinkTextEditMode}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
role="presentation"
|
||||||
|
className="my-1 hidden h-px bg-border translucent-ui:bg-[--popover-border-color] md:block"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isLink && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="border-t border-border px-1 py-1 focus:shadow-none focus:outline-none md:border-0 md:px-0 md:py-0"
|
||||||
|
ref={linkEditorRef}
|
||||||
|
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||||
|
>
|
||||||
|
<LinkEditor
|
||||||
|
linkUrl={linkUrl}
|
||||||
|
isEditMode={isLinkEditMode}
|
||||||
|
setEditMode={setIsLinkEditMode}
|
||||||
|
isAutoLink={isAutoLink}
|
||||||
|
editor={editor}
|
||||||
|
lastSelection={lastSelection}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
role="presentation"
|
||||||
|
className="my-1 hidden h-px bg-border translucent-ui:bg-[--popover-border-color] md:block"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className="flex w-full flex-shrink-0 border-t border-border md:border-0">
|
||||||
|
<Toolbar
|
||||||
className="flex items-center gap-1 overflow-x-auto pl-1 [&::-webkit-scrollbar]:h-0"
|
className="flex items-center gap-1 overflow-x-auto pl-1 [&::-webkit-scrollbar]:h-0"
|
||||||
ref={toolbarRef}
|
ref={toolbarRef}
|
||||||
|
store={toolbarStore}
|
||||||
>
|
>
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
return (
|
return (
|
||||||
<StyledTooltip showOnMobile showOnHover label={item.name} key={item.name}>
|
<ToolbarButton
|
||||||
<button
|
name={item.name}
|
||||||
className="flex select-none items-center justify-center rounded p-0.5 hover:bg-default disabled:opacity-50"
|
iconName={item.iconName}
|
||||||
aria-label={item.name}
|
active={item.active}
|
||||||
onMouseDown={(event) => {
|
disabled={item.disabled}
|
||||||
event.preventDefault()
|
onSelect={item.onSelect}
|
||||||
item.onSelect()
|
key={item.name}
|
||||||
}}
|
/>
|
||||||
onContextMenu={(event) => {
|
|
||||||
editor.focus()
|
|
||||||
event.preventDefault()
|
|
||||||
}}
|
|
||||||
disabled={item.disabled}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
'flex items-center justify-center rounded p-2 transition-colors duration-75',
|
|
||||||
item.active && 'bg-info text-info-contrast',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon type={item.iconName} size="medium" className="!text-current [&>path]:!text-current" />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</StyledTooltip>
|
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</Toolbar>
|
||||||
<button
|
{isMobile && (
|
||||||
className="flex flex-shrink-0 items-center justify-center rounded border-l border-border px-3 py-3"
|
<button
|
||||||
aria-label="Dismiss keyboard"
|
className="flex flex-shrink-0 items-center justify-center rounded border-l border-border px-3 py-3"
|
||||||
>
|
aria-label="Dismiss keyboard"
|
||||||
<Icon type="keyboard-close" size="medium" />
|
>
|
||||||
</button>
|
<Icon type="keyboard-close" size="medium" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MobileToolbarPlugin
|
export default ToolbarPlugin
|
||||||
@@ -14,7 +14,7 @@ import { $isHeadingNode } from '@lexical/rich-text'
|
|||||||
import { $isListNode, ListNode } from '@lexical/list'
|
import { $isListNode, ListNode } from '@lexical/list'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { getSelectedNode } from '../../Lexical/Utils/getSelectedNode'
|
import { getSelectedNode } from '../../Lexical/Utils/getSelectedNode'
|
||||||
import { $isLinkTextNode } from '../LinkEditor/LinkTextEditor'
|
import { $isLinkTextNode } from './ToolbarLinkTextEditor'
|
||||||
|
|
||||||
const blockTypeToBlockName = {
|
const blockTypeToBlockName = {
|
||||||
bullet: 'Bulleted List',
|
bullet: 'Bulleted List',
|
||||||
@@ -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<string | null>('TOGGLE_LINK_AND_EDIT_COMMAND')
|
|
||||||
|
|
||||||
const ToolbarButton = ({ active, ...props }: { active?: boolean } & ComponentPropsWithoutRef<'button'>) => {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className={classNames(
|
|
||||||
'flex rounded-lg p-3 hover:bg-default hover:text-text disabled:cursor-not-allowed',
|
|
||||||
active && 'bg-info text-info-contrast',
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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<HTMLDivElement | null>(null)
|
|
||||||
|
|
||||||
const [linkUrl, setLinkUrl] = useState('')
|
|
||||||
const [linkText, setLinkText] = useState('')
|
|
||||||
const [isLinkEditMode, setIsLinkEditMode] = useState(false)
|
|
||||||
const [lastSelection, setLastSelection] = useState<RangeSelection | GridSelection | NodeSelection | null>(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 (
|
|
||||||
<div
|
|
||||||
ref={toolbarRef}
|
|
||||||
className="absolute left-0 top-0 rounded-lg border border-border bg-contrast px-2 py-1 shadow-sm shadow-contrast translucent-ui:border-[--popover-border-color] translucent-ui:bg-[--popover-background-color] translucent-ui:[backdrop-filter:var(--popover-backdrop-filter)]"
|
|
||||||
>
|
|
||||||
{isLinkText && !isAutoLink && (
|
|
||||||
<>
|
|
||||||
<LinkTextEditor linkText={linkText} editor={editor} lastSelection={lastSelection} />
|
|
||||||
<div
|
|
||||||
role="presentation"
|
|
||||||
className="mb-1.5 mt-0.5 h-px bg-border translucent-ui:bg-[--popover-border-color]"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{isLink && (
|
|
||||||
<LinkEditor
|
|
||||||
linkUrl={linkUrl}
|
|
||||||
isEditMode={isLinkEditMode}
|
|
||||||
setEditMode={setIsLinkEditMode}
|
|
||||||
isAutoLink={isAutoLink}
|
|
||||||
editor={editor}
|
|
||||||
lastSelection={lastSelection}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isText && isLink && (
|
|
||||||
<div role="presentation" className="mb-1.5 mt-0.5 h-px bg-border translucent-ui:bg-[--popover-border-color]" />
|
|
||||||
)}
|
|
||||||
{isText && (
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<StyledTooltip label="Bold">
|
|
||||||
<ToolbarButton
|
|
||||||
active={isBold}
|
|
||||||
onClick={() => {
|
|
||||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold')
|
|
||||||
}}
|
|
||||||
aria-label="Format text as bold"
|
|
||||||
>
|
|
||||||
<IconComponent size={IconSize}>
|
|
||||||
<BoldIcon />
|
|
||||||
</IconComponent>
|
|
||||||
</ToolbarButton>
|
|
||||||
</StyledTooltip>
|
|
||||||
<StyledTooltip label="Italicize">
|
|
||||||
<ToolbarButton
|
|
||||||
onClick={() => {
|
|
||||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic')
|
|
||||||
}}
|
|
||||||
active={isItalic}
|
|
||||||
aria-label="Format text as italics"
|
|
||||||
>
|
|
||||||
<IconComponent size={IconSize}>
|
|
||||||
<ItalicIcon />
|
|
||||||
</IconComponent>
|
|
||||||
</ToolbarButton>
|
|
||||||
</StyledTooltip>
|
|
||||||
<StyledTooltip label="Underline">
|
|
||||||
<ToolbarButton
|
|
||||||
onClick={() => {
|
|
||||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline')
|
|
||||||
}}
|
|
||||||
active={isUnderline}
|
|
||||||
aria-label="Format text to underlined"
|
|
||||||
>
|
|
||||||
<IconComponent size={IconSize + 1}>
|
|
||||||
<UnderlineIcon />
|
|
||||||
</IconComponent>
|
|
||||||
</ToolbarButton>
|
|
||||||
</StyledTooltip>
|
|
||||||
<StyledTooltip label="Strikethrough">
|
|
||||||
<ToolbarButton
|
|
||||||
onClick={() => {
|
|
||||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough')
|
|
||||||
}}
|
|
||||||
active={isStrikethrough}
|
|
||||||
aria-label="Format text with a strikethrough"
|
|
||||||
>
|
|
||||||
<IconComponent size={IconSize}>
|
|
||||||
<StrikethroughIcon />
|
|
||||||
</IconComponent>
|
|
||||||
</ToolbarButton>
|
|
||||||
</StyledTooltip>
|
|
||||||
<StyledTooltip label="Subscript">
|
|
||||||
<ToolbarButton
|
|
||||||
onClick={() => {
|
|
||||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript')
|
|
||||||
}}
|
|
||||||
active={isSubscript}
|
|
||||||
title="Subscript"
|
|
||||||
aria-label="Format Subscript"
|
|
||||||
>
|
|
||||||
<IconComponent paddingTop={4} size={IconSize - 2}>
|
|
||||||
<SubscriptIcon />
|
|
||||||
</IconComponent>
|
|
||||||
</ToolbarButton>
|
|
||||||
</StyledTooltip>
|
|
||||||
<StyledTooltip label="Superscript">
|
|
||||||
<ToolbarButton
|
|
||||||
onClick={() => {
|
|
||||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript')
|
|
||||||
}}
|
|
||||||
active={isSuperscript}
|
|
||||||
title="Superscript"
|
|
||||||
aria-label="Format Superscript"
|
|
||||||
>
|
|
||||||
<IconComponent paddingTop={1} size={IconSize - 2}>
|
|
||||||
<SuperscriptIcon />
|
|
||||||
</IconComponent>
|
|
||||||
</ToolbarButton>
|
|
||||||
</StyledTooltip>
|
|
||||||
<StyledTooltip label="Code">
|
|
||||||
<ToolbarButton
|
|
||||||
onClick={() => {
|
|
||||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code')
|
|
||||||
}}
|
|
||||||
active={isCode}
|
|
||||||
aria-label="Insert code block"
|
|
||||||
>
|
|
||||||
<IconComponent size={IconSize}>
|
|
||||||
<CodeIcon />
|
|
||||||
</IconComponent>
|
|
||||||
</ToolbarButton>
|
|
||||||
</StyledTooltip>
|
|
||||||
<StyledTooltip label="Highlight">
|
|
||||||
<ToolbarButton
|
|
||||||
onClick={() => {
|
|
||||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'highlight')
|
|
||||||
}}
|
|
||||||
active={isHighlighted}
|
|
||||||
aria-label="Highlight text"
|
|
||||||
>
|
|
||||||
<IconComponent size={IconSize}>
|
|
||||||
<DrawIcon />
|
|
||||||
</IconComponent>
|
|
||||||
</ToolbarButton>
|
|
||||||
</StyledTooltip>
|
|
||||||
<StyledTooltip label="Insert link">
|
|
||||||
<ToolbarButton onClick={insertLink} active={isLink} aria-label="Insert link">
|
|
||||||
<IconComponent size={IconSize}>
|
|
||||||
<LinkIcon />
|
|
||||||
</IconComponent>
|
|
||||||
</ToolbarButton>
|
|
||||||
</StyledTooltip>
|
|
||||||
<StyledTooltip label="Insert bulleted list">
|
|
||||||
<ToolbarButton onClick={formatBulletList} active={isBulletedList} aria-label="Insert bulleted list">
|
|
||||||
<IconComponent size={IconSize}>
|
|
||||||
<ListBulleted />
|
|
||||||
</IconComponent>
|
|
||||||
</ToolbarButton>
|
|
||||||
</StyledTooltip>
|
|
||||||
<StyledTooltip label="Insert numbered list">
|
|
||||||
<ToolbarButton onClick={formatNumberedList} active={isNumberedList} aria-label="Insert numbered list">
|
|
||||||
<IconComponent size={IconSize}>
|
|
||||||
<ListNumbered />
|
|
||||||
</IconComponent>
|
|
||||||
</ToolbarButton>
|
|
||||||
</StyledTooltip>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
|
||||||
<TextFormatFloatingToolbar
|
|
||||||
editor={editor}
|
|
||||||
anchorElem={anchorElem}
|
|
||||||
isText={isText}
|
|
||||||
isLink={isLink}
|
|
||||||
isLinkText={isLinkText}
|
|
||||||
isAutoLink={isAutoLink}
|
|
||||||
isBold={isBold}
|
|
||||||
isItalic={isItalic}
|
|
||||||
isStrikethrough={isStrikethrough}
|
|
||||||
isSubscript={isSubscript}
|
|
||||||
isSuperscript={isSuperscript}
|
|
||||||
isUnderline={isUnderline}
|
|
||||||
isCode={isCode}
|
|
||||||
isHighlighted={isHighlighted}
|
|
||||||
isBulletedList={blockType === 'bullet'}
|
|
||||||
isNumberedList={blockType === 'number'}
|
|
||||||
/>,
|
|
||||||
anchorElem,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function FloatingTextFormatToolbarPlugin({
|
|
||||||
anchorElem = document.body,
|
|
||||||
}: {
|
|
||||||
anchorElem?: HTMLElement
|
|
||||||
}): JSX.Element | null {
|
|
||||||
const [editor] = useLexicalComposerContext()
|
|
||||||
return useFloatingTextFormatToolbar(editor, anchorElem)
|
|
||||||
}
|
|
||||||
@@ -44,7 +44,7 @@ import ReadonlyPlugin from './Plugins/ReadonlyPlugin/ReadonlyPlugin'
|
|||||||
import { SuperSearchContextProvider } from './Plugins/SearchPlugin/Context'
|
import { SuperSearchContextProvider } from './Plugins/SearchPlugin/Context'
|
||||||
import { SearchPlugin } from './Plugins/SearchPlugin/SearchPlugin'
|
import { SearchPlugin } from './Plugins/SearchPlugin/SearchPlugin'
|
||||||
import ModalOverlay from '@/Components/Modal/ModalOverlay'
|
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 CodeOptionsPlugin from './Plugins/CodeOptionsPlugin/CodeOptions'
|
||||||
import RemoteImagePlugin from './Plugins/RemoteImagePlugin/RemoteImagePlugin'
|
import RemoteImagePlugin from './Plugins/RemoteImagePlugin/RemoteImagePlugin'
|
||||||
import NotEntitledBanner from '../ComponentView/NotEntitledBanner'
|
import NotEntitledBanner from '../ComponentView/NotEntitledBanner'
|
||||||
@@ -250,7 +250,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
|
|||||||
<SuperSearchContextProvider>
|
<SuperSearchContextProvider>
|
||||||
<SearchPlugin />
|
<SearchPlugin />
|
||||||
</SuperSearchContextProvider>
|
</SuperSearchContextProvider>
|
||||||
<MobileToolbarPlugin />
|
<ToolbarPlugin />
|
||||||
<CodeOptionsPlugin />
|
<CodeOptionsPlugin />
|
||||||
<RemoteImagePlugin />
|
<RemoteImagePlugin />
|
||||||
</BlocksEditor>
|
</BlocksEditor>
|
||||||
|
|||||||
Reference in New Issue
Block a user