feat: Added above-keyboard toolbar to Super notes on mobile for formatting & selecting blocks (#2189)
This commit is contained in:
@@ -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 = [
|
||||
|
||||
@@ -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),
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -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),
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}),
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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) {
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}))
|
||||
}
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
}),
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}),
|
||||
}))
|
||||
}
|
||||
@@ -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),
|
||||
}),
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function GetTableBlock(onSelect: () => void) {
|
||||
return { name: 'Table', iconName: 'table', keywords: ['table', 'grid', 'spreadsheet', 'rows', 'columns'], onSelect }
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user