feat: Added above-keyboard toolbar to Super notes on mobile for formatting & selecting blocks (#2189)

This commit is contained in:
Aman Harwara
2023-02-01 13:59:51 +05:30
committed by GitHub
parent d33c737f86
commit 4a3f9f12e7
48 changed files with 629 additions and 183 deletions

View File

@@ -1,5 +1,5 @@
import { ApplicationGroup } from '@/Application/ApplicationGroup'
import { getPlatformString } from '@/Utils'
import { getPlatformString, isIOS } from '@/Utils'
import { ApplicationEvent, Challenge, removeFromArray, WebAppEvent } from '@standardnotes/snjs'
import { alertDialog, RouteType } from '@standardnotes/ui-services'
import { WebApplication } from '@/Application/Application'
@@ -29,6 +29,7 @@ import PanesSystemComponent from '../Panes/PanesSystemComponent'
import DotOrgNotice from './DotOrgNotice'
import LinkingControllerProvider from '@/Controllers/LinkingControllerProvider'
import ImportModal from '../ImportModal/ImportModal'
import IosKeyboardClose from '../IosKeyboardClose/IosKeyboardClose'
type Props = {
application: WebApplication
@@ -235,6 +236,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
<ImportModal viewControllerManager={viewControllerManager} />
</>
{application.routeService.isDotOrg && <DotOrgNotice />}
{isIOS() && <IosKeyboardClose />}
</div>
</LinkingControllerProvider>
</PremiumModalProvider>

View File

@@ -2,6 +2,10 @@ import * as icons from '@standardnotes/icons'
export const IconNameToSvgMapping = {
'account-circle': icons.AccountCircleIcon,
'align-center': icons.FormatAlignCenterIcon,
'align-justify': icons.FormatAlignJustifyIcon,
'align-left': icons.FormatAlignLeftIcon,
'align-right': icons.FormatAlignRightIcon,
'arrow-down': icons.ArrowDownIcon,
'arrow-left': icons.ArrowLeftIcon,
'arrow-right': icons.ArrowRightIcon,
@@ -11,8 +15,8 @@ export const IconNameToSvgMapping = {
'arrows-vertical': icons.ArrowsVerticalIcon,
'attachment-file': icons.AttachmentFileIcon,
'check-bold': icons.CheckBoldIcon,
'check-circle': icons.CheckCircleIcon,
'check-circle-filled': icons.CheckCircleFilledIcon,
'check-circle': icons.CheckCircleIcon,
'chevron-down': icons.ChevronDownIcon,
'chevron-left': icons.ChevronLeftIcon,
'chevron-right': icons.ChevronRightIcon,
@@ -36,8 +40,10 @@ export const IconNameToSvgMapping = {
'format-align-right': icons.FormatAlignRightIcon,
'fullscreen-exit': icons.FullscreenExitIcon,
'hashtag-off': icons.HashtagOffIcon,
'keyboard-close': icons.KeyboardCloseIcon,
'link-off': icons.LinkOffIcon,
'list-bulleted': icons.ListBulleted,
'list-numbered': icons.ListNumbered,
'lock-filled': icons.LockFilledIcon,
'menu-arrow-down-alt': icons.MenuArrowDownAlt,
'menu-arrow-down': icons.MenuArrowDownIcon,
@@ -63,9 +69,11 @@ export const IconNameToSvgMapping = {
'user-switch': icons.UserSwitch,
accessibility: icons.AccessibilityIcon,
add: icons.AddIcon,
aegis: icons.AegisIcon,
archive: icons.ArchiveIcon,
asterisk: icons.AsteriskIcon,
authenticator: icons.AuthenticatorIcon,
bold: icons.BoldIcon,
camera: icons.CameraIcon,
check: icons.CheckIcon,
close: icons.CloseIcon,
@@ -77,13 +85,16 @@ export const IconNameToSvgMapping = {
drag: icons.DragIcon,
editor: icons.EditorIcon,
email: icons.EmailIcon,
evernote: icons.EvernoteIcon,
eye: icons.EyeIcon,
file: icons.FileIcon,
folder: icons.FolderIcon,
gkeep: icons.GoogleKeepIcon,
hashtag: icons.HashtagIcon,
help: icons.HelpIcon,
history: icons.HistoryIcon,
info: icons.InfoIcon,
italic: icons.ItalicIcon,
keyboard: icons.KeyboardIcon,
link: icons.LinkIcon,
listed: icons.ListedIcon,
@@ -91,6 +102,7 @@ export const IconNameToSvgMapping = {
markdown: icons.MarkdownIcon,
more: icons.MoreIcon,
notes: icons.NotesIcon,
paragraph: icons.TextParagraphLongIcon,
password: icons.PasswordIcon,
pencil: icons.PencilIcon,
pin: icons.PinIcon,
@@ -102,23 +114,24 @@ export const IconNameToSvgMapping = {
share: icons.ShareIcon,
signIn: icons.SignInIcon,
signOut: icons.SignOutIcon,
simplenote: icons.SimplenoteIcon,
spreadsheets: icons.SpreadsheetsIcon,
star: icons.StarIcon,
strikethrough: icons.StrikethroughIcon,
subscript: icons.SubscriptIcon,
subtract: icons.SubtractIcon,
superscript: icons.SuperscriptIcon,
sync: icons.SyncIcon,
tasks: icons.TasksIcon,
themes: icons.ThemesIcon,
trash: icons.TrashIcon,
tune: icons.TuneIcon,
unarchive: icons.UnarchiveIcon,
underline: icons.UnderlineIcon,
unpin: icons.UnpinIcon,
upload: icons.UploadIcon,
user: icons.UserIcon,
view: icons.ViewIcon,
warning: icons.WarningIcon,
window: icons.WindowIcon,
evernote: icons.EvernoteIcon,
gkeep: icons.GoogleKeepIcon,
simplenote: icons.SimplenoteIcon,
aegis: icons.AegisIcon,
}

View File

@@ -0,0 +1,53 @@
import { classNames, ReactNativeToWebEvent } from '@standardnotes/snjs'
import { useEffect, useState } from 'react'
import { useApplication } from '../ApplicationProvider'
import Icon from '../Icon/Icon'
const IosKeyboardClose = () => {
const application = useApplication()
const [isVisible, setIsVisible] = useState(false)
const [isFocusInSuperEditor, setIsFocusInSuperEditor] = useState(
() => !!document.activeElement?.closest('#blocks-editor'),
)
useEffect(() => {
return application.addNativeMobileEventListener((event) => {
if (event === ReactNativeToWebEvent.KeyboardWillShow) {
setIsVisible(true)
} else if (event === ReactNativeToWebEvent.KeyboardWillHide) {
setIsVisible(false)
}
})
}, [application])
useEffect(() => {
const handleFocusChange = () => {
setIsFocusInSuperEditor(!!document.activeElement?.closest('#blocks-editor'))
}
document.addEventListener('focusin', handleFocusChange)
document.addEventListener('focusout', handleFocusChange)
return () => {
document.removeEventListener('focusin', handleFocusChange)
document.removeEventListener('focusout', handleFocusChange)
}
}, [])
if (!isVisible) {
return null
}
return (
<button
className={classNames(
'absolute bottom-3 right-3 rounded-full border border-border bg-contrast p-3',
isFocusInSuperEditor && 'hidden',
)}
>
<Icon type="keyboard-close" />
</button>
)
}
export default IosKeyboardClose

View File

@@ -6,25 +6,25 @@ import useModal from '@standardnotes/blocks-editor/src/Lexical/Hooks/useModal'
import { InsertTableDialog } from '@standardnotes/blocks-editor/src/Lexical/Plugins/TablePlugin'
import { BlockPickerOption } from './BlockPickerOption'
import { BlockPickerMenuItem } from './BlockPickerMenuItem'
import { GetNumberedListBlock } from './Blocks/NumberedList'
import { GetBulletedListBlock } from './Blocks/BulletedList'
import { GetChecklistBlock } from './Blocks/Checklist'
import { GetDividerBlock } from './Blocks/Divider'
import { GetCollapsibleBlock } from './Blocks/Collapsible'
import { GetDynamicPasswordBlocks, GetPasswordBlocks } from './Blocks/Password'
import { GetParagraphBlock } from './Blocks/Paragraph'
import { GetHeadingsBlocks } from './Blocks/Headings'
import { GetQuoteBlock } from './Blocks/Quote'
import { GetAlignmentBlocks } from './Blocks/Alignment'
import { GetCodeBlock } from './Blocks/Code'
import { GetEmbedsBlocks } from './Blocks/Embeds'
import { GetDynamicTableBlocks, GetTableBlock } from './Blocks/Table'
import { GetNumberedListBlockOption } from './Options/NumberedList'
import { GetBulletedListBlockOption } from './Options/BulletedList'
import { GetChecklistBlockOption } from './Options/Checklist'
import { GetDividerBlockOption } from './Options/Divider'
import { GetCollapsibleBlockOption } from './Options/Collapsible'
import { GetDynamicPasswordBlocks, GetPasswordBlockOption } from './Options/Password'
import { GetParagraphBlockOption } from './Options/Paragraph'
import { GetHeadingsBlockOptions } from './Options/Headings'
import { GetQuoteBlockOption } from './Options/Quote'
import { GetAlignmentBlockOptions } from './Options/Alignment'
import { GetCodeBlockOption } from './Options/Code'
import { GetEmbedsBlockOptions } from './Options/Embeds'
import { GetDynamicTableBlocks, GetTableBlockOption } from './Options/Table'
import Popover from '@/Components/Popover/Popover'
import { PopoverClassNames } from '../ClassNames'
import { GetDatetimeBlocks } from './Blocks/DateTime'
import { GetDatetimeBlockOptions } from './Options/DateTime'
import { isMobileScreen } from '@/Utils'
import { useApplication } from '@/Components/ApplicationProvider'
import { GetIndentOutdentBlocks } from './Blocks/IndentOutdent'
import { GetIndentOutdentBlockOptions } from './Options/IndentOutdent'
export default function BlockPickerMenuPlugin(): JSX.Element {
const [editor] = useLexicalComposerContext()
@@ -37,26 +37,26 @@ export default function BlockPickerMenuPlugin(): JSX.Element {
})
const options = useMemo(() => {
const indentOutdentOptions = application.isNativeMobileWeb() ? GetIndentOutdentBlocks(editor) : []
const indentOutdentOptions = application.isNativeMobileWeb() ? GetIndentOutdentBlockOptions(editor) : []
const baseOptions = [
GetParagraphBlock(editor),
...GetHeadingsBlocks(editor),
GetParagraphBlockOption(editor),
...GetHeadingsBlockOptions(editor),
...indentOutdentOptions,
GetTableBlock(() =>
GetTableBlockOption(() =>
showModal('Insert Table', (onClose) => <InsertTableDialog activeEditor={editor} onClose={onClose} />),
),
GetNumberedListBlock(editor),
GetBulletedListBlock(editor),
GetChecklistBlock(editor),
GetQuoteBlock(editor),
GetCodeBlock(editor),
GetDividerBlock(editor),
...GetDatetimeBlocks(editor),
...GetAlignmentBlocks(editor),
...GetPasswordBlocks(editor),
GetCollapsibleBlock(editor),
...GetEmbedsBlocks(editor),
GetNumberedListBlockOption(editor),
GetBulletedListBlockOption(editor),
GetChecklistBlockOption(editor),
GetQuoteBlockOption(editor),
GetCodeBlockOption(editor),
GetDividerBlockOption(editor),
...GetDatetimeBlockOptions(editor),
...GetAlignmentBlockOptions(editor),
GetPasswordBlockOption(editor),
GetCollapsibleBlockOption(editor),
...GetEmbedsBlockOptions(editor),
]
const dynamicOptions = [

View File

@@ -1,14 +0,0 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { FORMAT_ELEMENT_COMMAND, LexicalEditor, ElementFormatType } from 'lexical'
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
export function GetAlignmentBlocks(editor: LexicalEditor) {
return ['left', 'center', 'right', 'justify'].map(
(alignment) =>
new BlockPickerOption(`Align ${alignment}`, {
iconName: `align-${alignment}` as LexicalIconName,
keywords: ['align', 'justify', alignment],
onSelect: () => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, alignment as ElementFormatType),
}),
)
}

View File

@@ -1,16 +0,0 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { INSERT_EMBED_COMMAND } from '@lexical/react/LexicalAutoEmbedPlugin'
import { EmbedConfigs } from '@standardnotes/blocks-editor/src/Lexical/Plugins/AutoEmbedPlugin'
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
export function GetEmbedsBlocks(editor: LexicalEditor) {
return EmbedConfigs.map(
(embedConfig) =>
new BlockPickerOption(`Embed ${embedConfig.contentName}`, {
iconName: embedConfig.iconName as LexicalIconName,
keywords: [...embedConfig.keywords, 'embed'],
onSelect: () => editor.dispatchCommand(INSERT_EMBED_COMMAND, embedConfig.type),
}),
)
}

View File

@@ -1,22 +0,0 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { $wrapNodes } from '@lexical/selection'
import { $getSelection, $isRangeSelection, LexicalEditor } from 'lexical'
import { $createHeadingNode, HeadingTagType } from '@lexical/rich-text'
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
export function GetHeadingsBlocks(editor: LexicalEditor) {
return Array.from({ length: 3 }, (_, i) => i + 1).map(
(n) =>
new BlockPickerOption(`Heading ${n}`, {
iconName: `h${n}` as LexicalIconName,
keywords: ['heading', 'header', `h${n}`],
onSelect: () =>
editor.update(() => {
const selection = $getSelection()
if ($isRangeSelection(selection)) {
$wrapNodes(selection, () => $createHeadingNode(`h${n}` as HeadingTagType))
}
}),
}),
)
}

View File

@@ -0,0 +1,14 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { GetAlignmentBlocks } from '../../Blocks/Alignment'
export function GetAlignmentBlockOptions(editor: LexicalEditor) {
return GetAlignmentBlocks(editor).map(
(block) =>
new BlockPickerOption(block.name, {
iconName: block.iconName,
keywords: block.keywords,
onSelect: block.onSelect,
}),
)
}

View File

@@ -0,0 +1,13 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { GetBulletedListBlock } from '../../Blocks/BulletedList'
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
export function GetBulletedListBlockOption(editor: LexicalEditor) {
const block = GetBulletedListBlock(editor)
return new BlockPickerOption(block.name, {
iconName: block.iconName as LexicalIconName,
keywords: block.keywords,
onSelect: block.onSelect,
})
}

View File

@@ -0,0 +1,13 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { GetChecklistBlock } from '../../Blocks/Checklist'
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
export function GetChecklistBlockOption(editor: LexicalEditor) {
const block = GetChecklistBlock(editor)
return new BlockPickerOption(block.name, {
iconName: block.iconName as LexicalIconName,
keywords: block.keywords,
onSelect: block.onSelect,
})
}

View File

@@ -0,0 +1,12 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { GetCodeBlock } from '../../Blocks/Code'
export function GetCodeBlockOption(editor: LexicalEditor) {
const block = GetCodeBlock(editor)
return new BlockPickerOption(block.name, {
iconName: block.iconName,
keywords: block.keywords,
onSelect: block.onSelect,
})
}

View File

@@ -0,0 +1,12 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { GetCollapsibleBlock } from '../../Blocks/Collapsible'
export function GetCollapsibleBlockOption(editor: LexicalEditor) {
const block = GetCollapsibleBlock(editor)
return new BlockPickerOption(block.name, {
iconName: block.iconName,
keywords: block.keywords,
onSelect: block.onSelect,
})
}

View File

@@ -0,0 +1,15 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { GetDatetimeBlocks } from '../../Blocks/DateTime'
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
export function GetDatetimeBlockOptions(editor: LexicalEditor) {
return GetDatetimeBlocks(editor).map(
(block) =>
new BlockPickerOption(block.name, {
iconName: block.iconName as LexicalIconName,
keywords: block.keywords,
onSelect: block.onSelect,
}),
)
}

View File

@@ -0,0 +1,13 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { GetDividerBlock } from '../../Blocks/Divider'
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
export function GetDividerBlockOption(editor: LexicalEditor) {
const block = GetDividerBlock(editor)
return new BlockPickerOption(block.name, {
iconName: block.iconName as LexicalIconName,
keywords: block.keywords,
onSelect: block.onSelect,
})
}

View File

@@ -0,0 +1,15 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
import { GetEmbedsBlocks } from '../../Blocks/Embeds'
export function GetEmbedsBlockOptions(editor: LexicalEditor) {
return GetEmbedsBlocks(editor).map(
(block) =>
new BlockPickerOption(block.name, {
iconName: block.iconName as LexicalIconName,
keywords: block.keywords,
onSelect: block.onSelect,
}),
)
}

View File

@@ -0,0 +1,15 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
import { GetHeadingsBlocks } from '../../Blocks/Headings'
export function GetHeadingsBlockOptions(editor: LexicalEditor) {
return GetHeadingsBlocks(editor).map(
(block) =>
new BlockPickerOption(block.name, {
iconName: block.iconName as LexicalIconName,
keywords: block.keywords,
onSelect: block.onSelect,
}),
)
}

View File

@@ -0,0 +1,15 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { GetIndentOutdentBlocks } from '../../Blocks/IndentOutdent'
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
export function GetIndentOutdentBlockOptions(editor: LexicalEditor) {
return GetIndentOutdentBlocks(editor).map(
(block) =>
new BlockPickerOption(block.name, {
iconName: block.iconName as LexicalIconName,
keywords: block.keywords,
onSelect: block.onSelect,
}),
)
}

View File

@@ -0,0 +1,13 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { GetNumberedListBlock } from '../../Blocks/NumberedList'
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
export function GetNumberedListBlockOption(editor: LexicalEditor) {
const block = GetNumberedListBlock(editor)
return new BlockPickerOption(block.name, {
iconName: block.iconName as LexicalIconName,
keywords: block.keywords,
onSelect: block.onSelect,
})
}

View File

@@ -0,0 +1,13 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { GetParagraphBlock } from '../../Blocks/Paragraph'
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
export function GetParagraphBlockOption(editor: LexicalEditor) {
const block = GetParagraphBlock(editor)
return new BlockPickerOption(block.name, {
iconName: block.iconName as LexicalIconName,
keywords: block.keywords,
onSelect: block.onSelect,
})
}

View File

@@ -1,18 +1,18 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { INSERT_PASSWORD_COMMAND } from '../../Commands'
import { GetPasswordBlock } from '../../Blocks/Password'
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
const DEFAULT_PASSWORD_LENGTH = 16
const MIN_PASSWORD_LENGTH = 8
export function GetPasswordBlocks(editor: LexicalEditor) {
return [
new BlockPickerOption('Generate cryptographically secure password', {
iconName: 'password',
keywords: ['password', 'secure'],
onSelect: () => editor.dispatchCommand(INSERT_PASSWORD_COMMAND, String(DEFAULT_PASSWORD_LENGTH)),
}),
]
export function GetPasswordBlockOption(editor: LexicalEditor) {
const block = GetPasswordBlock(editor)
return new BlockPickerOption(block.name, {
iconName: block.iconName as LexicalIconName,
keywords: block.keywords,
onSelect: block.onSelect,
})
}
export function GetDynamicPasswordBlocks(editor: LexicalEditor, queryString: string) {

View File

@@ -0,0 +1,13 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { GetQuoteBlock } from '../../Blocks/Quote'
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
export function GetQuoteBlockOption(editor: LexicalEditor) {
const block = GetQuoteBlock(editor)
return new BlockPickerOption(block.name, {
iconName: block.iconName as LexicalIconName,
keywords: block.keywords,
onSelect: block.onSelect,
})
}

View File

@@ -1,12 +1,15 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { INSERT_TABLE_COMMAND } from '@lexical/table'
import { GetTableBlock } from '../../Blocks/Table'
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
export function GetTableBlock(onSelect: () => void) {
return new BlockPickerOption('Table', {
iconName: 'table',
keywords: ['table', 'grid', 'spreadsheet', 'rows', 'columns'],
onSelect,
export function GetTableBlockOption(onSelect: () => void) {
const block = GetTableBlock(onSelect)
return new BlockPickerOption(block.name, {
iconName: block.iconName as LexicalIconName,
keywords: block.keywords,
onSelect: block.onSelect,
})
}

View File

@@ -0,0 +1,11 @@
import { FORMAT_ELEMENT_COMMAND, LexicalEditor, ElementFormatType } from 'lexical'
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
export function GetAlignmentBlocks(editor: LexicalEditor) {
return ['left', 'center', 'right', 'justify'].map((alignment) => ({
name: `Align ${alignment}`,
iconName: `align-${alignment}` as LexicalIconName,
keywords: ['align', 'justify', alignment],
onSelect: () => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, alignment as ElementFormatType),
}))
}

View File

@@ -1,11 +1,11 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { INSERT_UNORDERED_LIST_COMMAND } from '@lexical/list'
export function GetBulletedListBlock(editor: LexicalEditor) {
return new BlockPickerOption('Bulleted List', {
iconName: 'list-ul',
return {
name: 'Bulleted List',
iconName: 'list-bulleted',
keywords: ['bulleted list', 'unordered list', 'ul'],
onSelect: () => editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined),
})
}
}

View File

@@ -1,11 +1,11 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { INSERT_CHECK_LIST_COMMAND } from '@lexical/list'
export function GetChecklistBlock(editor: LexicalEditor) {
return new BlockPickerOption('Check List', {
return {
name: 'Check List',
iconName: 'check',
keywords: ['check list', 'todo list'],
onSelect: () => editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined),
})
}
}

View File

@@ -1,11 +1,12 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { $wrapNodes } from '@lexical/selection'
import { $getSelection, $isRangeSelection, LexicalEditor } from 'lexical'
import { $createCodeNode } from '@lexical/code'
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
export function GetCodeBlock(editor: LexicalEditor) {
return new BlockPickerOption('Code', {
iconName: 'lexical-code',
return {
name: 'Code',
iconName: 'code' as LexicalIconName,
keywords: ['javascript', 'python', 'js', 'codeblock'],
onSelect: () =>
editor.update(() => {
@@ -21,5 +22,5 @@ export function GetCodeBlock(editor: LexicalEditor) {
}
}
}),
})
}
}

View File

@@ -1,11 +1,12 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { INSERT_COLLAPSIBLE_COMMAND } from '@standardnotes/blocks-editor/src/Lexical/Plugins/CollapsiblePlugin'
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
export function GetCollapsibleBlock(editor: LexicalEditor) {
return new BlockPickerOption('Collapsible', {
iconName: 'caret-right-fill',
return {
name: 'Collapsible',
iconName: 'caret-right-fill' as LexicalIconName,
keywords: ['collapse', 'collapsible', 'toggle'],
onSelect: () => editor.dispatchCommand(INSERT_COLLAPSIBLE_COMMAND, undefined),
})
}
}

View File

@@ -1,23 +1,25 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { INSERT_DATETIME_COMMAND, INSERT_DATE_COMMAND, INSERT_TIME_COMMAND } from '../../Commands'
import { INSERT_DATETIME_COMMAND, INSERT_DATE_COMMAND, INSERT_TIME_COMMAND } from '../Commands'
export function GetDatetimeBlocks(editor: LexicalEditor) {
return [
new BlockPickerOption('Current date and time', {
{
name: 'Current date and time',
iconName: 'authenticator',
keywords: ['date', 'current'],
onSelect: () => editor.dispatchCommand(INSERT_DATETIME_COMMAND, 'datetime'),
}),
new BlockPickerOption('Current time', {
},
{
name: 'Current time',
iconName: 'authenticator',
keywords: ['time', 'current'],
onSelect: () => editor.dispatchCommand(INSERT_TIME_COMMAND, 'datetime'),
}),
new BlockPickerOption('Current date', {
},
{
name: 'Current date',
iconName: 'authenticator',
keywords: ['date', 'current'],
onSelect: () => editor.dispatchCommand(INSERT_DATE_COMMAND, 'datetime'),
}),
},
]
}

View File

@@ -1,11 +1,11 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/react/LexicalHorizontalRuleNode'
export function GetDividerBlock(editor: LexicalEditor) {
return new BlockPickerOption('Divider', {
return {
name: 'Divider',
iconName: 'horizontal-rule',
keywords: ['horizontal rule', 'divider', 'hr'],
onSelect: () => editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined),
})
}
}

View File

@@ -0,0 +1,13 @@
import { LexicalEditor } from 'lexical'
import { INSERT_EMBED_COMMAND } from '@lexical/react/LexicalAutoEmbedPlugin'
import { EmbedConfigs } from '@standardnotes/blocks-editor/src/Lexical/Plugins/AutoEmbedPlugin'
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
export function GetEmbedsBlocks(editor: LexicalEditor) {
return EmbedConfigs.map((embedConfig) => ({
name: `Embed ${embedConfig.contentName}`,
iconName: embedConfig.iconName as LexicalIconName,
keywords: [...embedConfig.keywords, 'embed'],
onSelect: () => editor.dispatchCommand(INSERT_EMBED_COMMAND, embedConfig.type),
}))
}

View File

@@ -0,0 +1,19 @@
import { $wrapNodes } from '@lexical/selection'
import { $getSelection, $isRangeSelection, LexicalEditor } from 'lexical'
import { $createHeadingNode, HeadingTagType } from '@lexical/rich-text'
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
export function GetHeadingsBlocks(editor: LexicalEditor) {
return Array.from({ length: 3 }, (_, i) => i + 1).map((n) => ({
name: `Heading ${n}`,
iconName: `h${n}` as LexicalIconName,
keywords: ['heading', 'header', `h${n}`],
onSelect: () =>
editor.update(() => {
const selection = $getSelection()
if ($isRangeSelection(selection)) {
$wrapNodes(selection, () => $createHeadingNode(`h${n}` as HeadingTagType))
}
}),
}))
}

View File

@@ -1,17 +1,18 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { INDENT_CONTENT_COMMAND, OUTDENT_CONTENT_COMMAND, LexicalEditor } from 'lexical'
export function GetIndentOutdentBlocks(editor: LexicalEditor) {
return [
new BlockPickerOption('Indent', {
{
name: 'Indent',
iconName: 'arrow-right',
keywords: ['indent'],
onSelect: () => editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined),
}),
new BlockPickerOption('Outdent', {
},
{
name: 'Outdent',
iconName: 'arrow-left',
keywords: ['outdent'],
onSelect: () => editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined),
}),
},
]
}

View File

@@ -1,11 +1,11 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { INSERT_ORDERED_LIST_COMMAND } from '@lexical/list'
export function GetNumberedListBlock(editor: LexicalEditor) {
return new BlockPickerOption('Numbered List', {
iconName: 'list-ol',
return {
name: 'Numbered List',
iconName: 'list-numbered',
keywords: ['numbered list', 'ordered list', 'ol'],
onSelect: () => editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined),
})
}
}

View File

@@ -1,9 +1,9 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { $wrapNodes } from '@lexical/selection'
import { $createParagraphNode, $getSelection, $isRangeSelection, LexicalEditor } from 'lexical'
export function GetParagraphBlock(editor: LexicalEditor) {
return new BlockPickerOption('Paragraph', {
return {
name: 'Paragraph',
iconName: 'paragraph',
keywords: ['normal', 'paragraph', 'p', 'text'],
onSelect: () =>
@@ -13,5 +13,5 @@ export function GetParagraphBlock(editor: LexicalEditor) {
$wrapNodes(selection, () => $createParagraphNode())
}
}),
})
}
}

View File

@@ -0,0 +1,13 @@
import { LexicalEditor } from 'lexical'
import { INSERT_PASSWORD_COMMAND } from '../Commands'
const DEFAULT_PASSWORD_LENGTH = 16
export function GetPasswordBlock(editor: LexicalEditor) {
return {
name: 'Generate cryptographically secure password',
iconName: 'password',
keywords: ['password', 'secure'],
onSelect: () => editor.dispatchCommand(INSERT_PASSWORD_COMMAND, String(DEFAULT_PASSWORD_LENGTH)),
}
}

View File

@@ -1,10 +1,10 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { $wrapNodes } from '@lexical/selection'
import { $getSelection, $isRangeSelection, LexicalEditor } from 'lexical'
import { $createQuoteNode } from '@lexical/rich-text'
export function GetQuoteBlock(editor: LexicalEditor) {
return new BlockPickerOption('Quote', {
return {
name: 'Quote',
iconName: 'quote',
keywords: ['block quote'],
onSelect: () =>
@@ -14,5 +14,5 @@ export function GetQuoteBlock(editor: LexicalEditor) {
$wrapNodes(selection, () => $createQuoteNode())
}
}),
})
}
}

View File

@@ -0,0 +1,3 @@
export function GetTableBlock(onSelect: () => void) {
return { name: 'Table', iconName: 'table', keywords: ['table', 'grid', 'spreadsheet', 'rows', 'columns'], onSelect }
}

View File

@@ -0,0 +1,180 @@
import Icon from '@/Components/Icon/Icon'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import useModal from '@standardnotes/blocks-editor/src/Lexical/Hooks/useModal'
import { InsertTableDialog } from '@standardnotes/blocks-editor/src/Lexical/Plugins/TablePlugin'
import { getSelectedNode } from '@standardnotes/blocks-editor/src/Lexical/Utils/getSelectedNode'
import { sanitizeUrl } from '@standardnotes/blocks-editor/src/Lexical/Utils/sanitizeUrl'
import { $getSelection, $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { GetAlignmentBlocks } from '../Blocks/Alignment'
import { GetBulletedListBlock } from '../Blocks/BulletedList'
import { GetChecklistBlock } from '../Blocks/Checklist'
import { GetCodeBlock } from '../Blocks/Code'
import { GetCollapsibleBlock } from '../Blocks/Collapsible'
import { GetDatetimeBlocks } from '../Blocks/DateTime'
import { GetDividerBlock } from '../Blocks/Divider'
import { GetEmbedsBlocks } from '../Blocks/Embeds'
import { GetHeadingsBlocks } from '../Blocks/Headings'
import { GetIndentOutdentBlocks } from '../Blocks/IndentOutdent'
import { GetNumberedListBlock } from '../Blocks/NumberedList'
import { GetParagraphBlock } from '../Blocks/Paragraph'
import { GetPasswordBlock } from '../Blocks/Password'
import { GetQuoteBlock } from '../Blocks/Quote'
import { GetTableBlock } from '../Blocks/Table'
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
import { classNames } from '@standardnotes/snjs'
const MobileToolbarPlugin = () => {
const [editor] = useLexicalComposerContext()
const [modal, showModal] = useModal()
const [isInEditor, setIsInEditor] = useState(false)
const isMobile = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
const insertLink = useCallback(() => {
const selection = $getSelection()
if (!$isRangeSelection(selection)) {
return
}
const node = getSelectedNode(selection)
const parent = node.getParent()
const isLink = $isLinkNode(parent) || $isLinkNode(node)
if (!isLink) {
editor.update(() => {
const selection = $getSelection()
const textContent = selection?.getTextContent()
if (!textContent) {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, 'https://')
return
}
editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl(textContent))
})
} else {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
}
}, [editor])
const items = useMemo(
() => [
{
name: 'Bold',
iconName: 'bold',
onSelect: () => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold')
},
},
{
name: 'Italic',
iconName: 'italic',
onSelect: () => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic')
},
},
{
name: 'Underline',
iconName: 'underline',
onSelect: () => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline')
},
},
{
name: 'Strikethrough',
iconName: 'strikethrough',
onSelect: () => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough')
},
},
{
name: 'Subscript',
iconName: 'subscript',
onSelect: () => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript')
},
},
{
name: 'Superscript',
iconName: 'superscript',
onSelect: () => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript')
},
},
{
name: 'Link',
iconName: 'link',
onSelect: insertLink,
},
GetParagraphBlock(editor),
...GetHeadingsBlocks(editor),
...GetIndentOutdentBlocks(editor),
GetTableBlock(() =>
showModal('Insert Table', (onClose) => <InsertTableDialog activeEditor={editor} onClose={onClose} />),
),
GetNumberedListBlock(editor),
GetBulletedListBlock(editor),
GetChecklistBlock(editor),
GetQuoteBlock(editor),
GetCodeBlock(editor),
GetDividerBlock(editor),
...GetDatetimeBlocks(editor),
...GetAlignmentBlocks(editor),
...[GetPasswordBlock(editor)],
GetCollapsibleBlock(editor),
...GetEmbedsBlocks(editor),
],
[editor, insertLink, showModal],
)
useEffect(() => {
const rootElement = editor.getRootElement()
if (!rootElement) {
return
}
const handleFocus = () => setIsInEditor(true)
const handleBlur = () => setIsInEditor(false)
rootElement.addEventListener('focus', handleFocus)
rootElement.addEventListener('blur', handleBlur)
return () => {
rootElement.removeEventListener('focus', handleFocus)
rootElement.removeEventListener('blur', handleBlur)
}
}, [editor])
if (!isMobile || !isInEditor) {
return null
}
return (
<>
{modal}
<div className="flex w-full flex-shrink-0 border-t border-border bg-contrast">
<div className={classNames('flex items-center gap-1 overflow-x-auto', '[&::-webkit-scrollbar]:h-0')}>
{items.map((item) => {
return (
<button
className="flex items-center justify-center rounded py-3 px-3"
aria-label={item.name}
onClick={item.onSelect}
key={item.name}
>
<Icon type={item.iconName} size="medium" />
</button>
)
})}
</div>
<button
className="flex flex-shrink-0 items-center justify-center rounded border-l border-border py-3 px-3"
aria-label="Dismiss keyboard"
>
<Icon type="keyboard-close" size="medium" />
</button>
</div>
</>
)
}
export default MobileToolbarPlugin

View File

@@ -41,6 +41,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/MobileToolbarPlugin/MobileToolbarPlugin'
import { SuperEditorNodes } from './SuperEditorNodes'
const NotePreviewCharLimit = 160
@@ -164,7 +165,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
}, [reloadPreferences, application])
return (
<div className="font-editor relative h-full w-full">
<div className="font-editor relative flex h-full w-full flex-col md:block">
<ErrorBoundary>
<LinkingControllerProvider controller={linkingController}>
<FilesControllerProvider controller={filesController}>
@@ -203,6 +204,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
<SuperSearchContextProvider>
<SearchPlugin />
</SuperSearchContextProvider>
<MobileToolbarPlugin />
</BlocksEditor>
</BlocksEditorComposer>
</FilesControllerProvider>