-
{title}
-
-
{children}
+
)
@@ -78,7 +88,7 @@ export default function Modal({
onClose,
children,
title,
- closeOnClickOutside = false,
+ closeOnClickOutside = true,
}: {
children: ReactNode
closeOnClickOutside?: boolean
diff --git a/packages/web/src/javascripts/Components/SuperEditor/Lexical/UI/TextInput.tsx b/packages/web/src/javascripts/Components/SuperEditor/Lexical/UI/TextInput.tsx
deleted file mode 100644
index e66fe0f83..000000000
--- a/packages/web/src/javascripts/Components/SuperEditor/Lexical/UI/TextInput.tsx
+++ /dev/null
@@ -1,41 +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 './Input.css'
-
-type Props = Readonly<{
- 'data-test-id'?: string
- label: string
- onChange: (val: string) => void
- placeholder?: string
- value: string
-}>
-
-export default function TextInput({
- label,
- value,
- onChange,
- placeholder = '',
- 'data-test-id': dataTestId,
-}: Props): JSX.Element {
- return (
-
-
- {
- onChange(e.target.value)
- }}
- data-test-id={dataTestId}
- />
-
- )
-}
diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/AutoEmbedPlugin/index.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/AutoEmbedPlugin/index.tsx
index 7a5ff4052..1908fac71 100644
--- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/AutoEmbedPlugin/index.tsx
+++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/AutoEmbedPlugin/index.tsx
@@ -16,15 +16,16 @@ import {
URL_MATCHER,
} from '@lexical/react/LexicalAutoEmbedPlugin'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
-import { useMemo, useState } from 'react'
+import { useCallback, useMemo, useState } from 'react'
import * as ReactDOM from 'react-dom'
import useModal from '../../Lexical/Hooks/useModal'
-import Button from '../../Lexical/UI/Button'
-import { DialogActions } from '../../Lexical/UI/Dialog'
import { INSERT_TWEET_COMMAND } from '../TwitterPlugin'
import { INSERT_YOUTUBE_COMMAND } from '../YouTubePlugin'
import { classNames } from '@standardnotes/snjs'
+import DecoratedInput from '@/Components/Input/DecoratedInput'
+import Button from '@/Components/Button/Button'
+import { isMobileScreen } from '../../../../Utils'
interface PlaygroundEmbedConfig extends EmbedConfig {
// Human readable name of the embeded content e.g. Tweet or Google Map.
@@ -219,28 +220,31 @@ export function AutoEmbedDialog({
}
}
+ const focusOnMount = useCallback((element: HTMLInputElement | null) => {
+ if (element) {
+ setTimeout(() => element.focus())
+ }
+ }, [])
+
return (
-
-
-
+
-
-
+ >
)
}
diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/BlockPickerPlugin.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/BlockPickerPlugin.tsx
index 0254f5f12..77eb2b155 100644
--- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/BlockPickerPlugin.tsx
+++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/BlockPickerPlugin/BlockPickerPlugin.tsx
@@ -29,6 +29,7 @@ import { GetQuoteBlockOption } from '../Blocks/Quote'
import { GetDividerBlockOption } from '../Blocks/Divider'
import { GetCollapsibleBlockOption } from '../Blocks/Collapsible'
import { GetEmbedsBlockOptions } from '../Blocks/Embeds'
+import { GetUploadFileOption } from '../Blocks/File'
export default function BlockPickerMenuPlugin({ popoverZIndex }: { popoverZIndex?: string }): JSX.Element {
const [editor] = useLexicalComposerContext()
@@ -57,6 +58,7 @@ export default function BlockPickerMenuPlugin({ popoverZIndex }: { popoverZIndex
GetRemoteImageBlockOption(() => {
showModal('Insert image from URL', (onClose) =>
)
}),
+ GetUploadFileOption(editor),
GetNumberedListBlockOption(editor),
GetBulletedListBlockOption(editor),
GetChecklistBlockOption(editor),
diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/File.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/File.tsx
new file mode 100644
index 000000000..86cb61389
--- /dev/null
+++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/Blocks/File.tsx
@@ -0,0 +1,12 @@
+import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
+import { BlockPickerOption } from '../BlockPickerPlugin/BlockPickerOption'
+import { LexicalEditor } from 'lexical'
+import { OPEN_FILE_UPLOAD_MODAL_COMMAND } from '../EncryptedFilePlugin/FilePlugin'
+
+export function GetUploadFileOption(editor: LexicalEditor) {
+ return new BlockPickerOption('Upload file', {
+ iconName: 'file' as LexicalIconName,
+ keywords: ['image', 'upload', 'file'],
+ onSelect: () => editor.dispatchCommand(OPEN_FILE_UPLOAD_MODAL_COMMAND, undefined),
+ })
+}
diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/EncryptedFilePlugin/FilePlugin.ts b/packages/web/src/javascripts/Components/SuperEditor/Plugins/EncryptedFilePlugin/FilePlugin.tsx
similarity index 60%
rename from packages/web/src/javascripts/Components/SuperEditor/Plugins/EncryptedFilePlugin/FilePlugin.ts
rename to packages/web/src/javascripts/Components/SuperEditor/Plugins/EncryptedFilePlugin/FilePlugin.tsx
index a299fd3c6..9648eff9a 100644
--- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/EncryptedFilePlugin/FilePlugin.ts
+++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/EncryptedFilePlugin/FilePlugin.tsx
@@ -1,7 +1,7 @@
import { INSERT_FILE_COMMAND } from '../Commands'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
-import { useEffect } from 'react'
+import { useEffect, useState } from 'react'
import { FileNode } from './Nodes/FileNode'
import {
$createParagraphNode,
@@ -10,6 +10,7 @@ import {
COMMAND_PRIORITY_NORMAL,
PASTE_COMMAND,
$isRootOrShadowRoot,
+ createCommand,
} from 'lexical'
import { $createFileNode } from './Nodes/FileUtils'
import { mergeRegister, $wrapNodeInElement } from '@lexical/utils'
@@ -18,6 +19,70 @@ import { FilesControllerEvent } from '@/Controllers/FilesController'
import { useLinkingController } from '@/Controllers/LinkingControllerProvider'
import { useApplication } from '@/Components/ApplicationProvider'
import { SNNote } from '@standardnotes/snjs'
+import Spinner from '../../../Spinner/Spinner'
+import Modal from '../../Lexical/UI/Modal'
+import Button from '@/Components/Button/Button'
+import { isMobileScreen } from '../../../../Utils'
+
+export const OPEN_FILE_UPLOAD_MODAL_COMMAND = createCommand('OPEN_FILE_UPLOAD_MODAL_COMMAND')
+
+function UploadFileDialog({ currentNote, onClose }: { currentNote: SNNote; onClose: () => void }) {
+ const application = useApplication()
+ const [editor] = useLexicalComposerContext()
+ const filesController = useFilesController()
+ const linkingController = useLinkingController()
+
+ const [file, setFile] = useState
()
+ const [isUploadingFile, setIsUploadingFile] = useState(false)
+
+ const onClick = () => {
+ if (!file) {
+ return
+ }
+
+ setIsUploadingFile(true)
+ filesController
+ .uploadNewFile(file)
+ .then((uploadedFile) => {
+ if (!uploadedFile) {
+ return
+ }
+ editor.dispatchCommand(INSERT_FILE_COMMAND, uploadedFile.uuid)
+ void linkingController.linkItemToSelectedItem(uploadedFile)
+ void application.changeAndSaveItem.execute(uploadedFile, (mutator) => {
+ mutator.protected = currentNote.protected
+ })
+ })
+ .catch(console.error)
+ .finally(() => {
+ setIsUploadingFile(false)
+ onClose()
+ })
+ }
+
+ return (
+ <>
+ {
+ const filesList = event.target.files
+ if (filesList && filesList.length === 1) {
+ setFile(filesList[0])
+ }
+ }}
+ />
+
+ {isUploadingFile ? (
+
+ ) : (
+
+ Upload
+
+ )}
+
+ >
+ )
+}
export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JSX.Element | null {
const application = useApplication()
@@ -25,6 +90,8 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS
const filesController = useFilesController()
const linkingController = useLinkingController()
+ const [showFileUploadModal, setShowFileUploadModal] = useState(false)
+
useEffect(() => {
if (!editor.hasNodes([FileNode])) {
throw new Error('FilePlugin: FileNode not registered on editor')
@@ -63,6 +130,14 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS
},
COMMAND_PRIORITY_EDITOR,
),
+ editor.registerCommand(
+ OPEN_FILE_UPLOAD_MODAL_COMMAND,
+ () => {
+ setShowFileUploadModal(true)
+ return true
+ },
+ COMMAND_PRIORITY_NORMAL,
+ ),
editor.registerCommand(
PASTE_COMMAND,
(payload) => {
@@ -103,5 +178,13 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS
return disposer
}, [filesController, editor])
+ if (showFileUploadModal) {
+ return (
+ setShowFileUploadModal(false)} title="Upload File">
+ setShowFileUploadModal(false)} />
+
+ )
+ }
+
return null
}
diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/RemoteImagePlugin/RemoteImagePlugin.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/RemoteImagePlugin/RemoteImagePlugin.tsx
index a399bc2a0..ee9266b1a 100644
--- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/RemoteImagePlugin/RemoteImagePlugin.tsx
+++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/RemoteImagePlugin/RemoteImagePlugin.tsx
@@ -1,12 +1,12 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { $createParagraphNode, $insertNodes, $isRootOrShadowRoot, COMMAND_PRIORITY_NORMAL } from 'lexical'
-import { useEffect, useState } from 'react'
-import Button from '../../Lexical/UI/Button'
-import { DialogActions } from '../../Lexical/UI/Dialog'
-import TextInput from '../../Lexical/UI/TextInput'
+import { useCallback, useEffect, useState } from 'react'
import { INSERT_REMOTE_IMAGE_COMMAND } from '../Commands'
import { $createRemoteImageNode, RemoteImageNode } from './RemoteImageNode'
import { mergeRegister, $wrapNodeInElement } from '@lexical/utils'
+import DecoratedInput from '@/Components/Input/DecoratedInput'
+import Button from '@/Components/Button/Button'
+import { isMobileScreen } from '../../../../Utils'
export function InsertRemoteImageDialog({ onClose }: { onClose: () => void }) {
const [url, setURL] = useState('')
@@ -21,12 +21,23 @@ export function InsertRemoteImageDialog({ onClose }: { onClose: () => void }) {
onClose()
}
+ const focusOnMount = useCallback((element: HTMLInputElement | null) => {
+ if (element) {
+ setTimeout(() => element.focus())
+ }
+ }, [])
+
return (
<>
-
-
- Confirm
-
+
+
+
+ Confirm
+
+
>
)
}
diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/TablePlugin.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/TablePlugin.tsx
index b14184f1a..e502af642 100644
--- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/TablePlugin.tsx
+++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/TablePlugin.tsx
@@ -8,12 +8,12 @@
import { INSERT_TABLE_COMMAND, TableNode, TableRowNode } from '@lexical/table'
import { $createParagraphNode, LexicalEditor } from 'lexical'
-import { useEffect, useState } from 'react'
-import Button from '../Lexical/UI/Button'
-import { DialogActions } from '../Lexical/UI/Dialog'
-import TextInput from '../Lexical/UI/TextInput'
+import { useCallback, useEffect, useState } from 'react'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { mergeRegister } from '@lexical/utils'
+import DecoratedInput from '@/Components/Input/DecoratedInput'
+import Button from '@/Components/Button/Button'
+import { isMobileScreen } from '../../../Utils'
export function InsertTableDialog({
activeEditor,
@@ -30,13 +30,27 @@ export function InsertTableDialog({
onClose()
}
+ const focusOnMount = useCallback((element: HTMLInputElement | null) => {
+ if (element) {
+ setTimeout(() => element.focus())
+ }
+ }, [])
+
return (
<>
-
-
-
- Confirm
-
+
+
+
+
+ Confirm
+
+
>
)
}
diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarPlugin.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarPlugin.tsx
index f1e29d18d..cb3092d8e 100644
--- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarPlugin.tsx
+++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarPlugin.tsx
@@ -76,6 +76,7 @@ import usePreference from '@/Hooks/usePreference'
import { ElementIds } from '@/Constants/ElementIDs'
import { $isDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
import LinkViewer from './LinkViewer'
+import { OPEN_FILE_UPLOAD_MODAL_COMMAND } from '../EncryptedFilePlugin/FilePlugin'
const TOGGLE_LINK_AND_EDIT_COMMAND = createCommand('TOGGLE_LINK_AND_EDIT_COMMAND')
@@ -1048,6 +1049,11 @@ const ToolbarPlugin = () => {
showModal('Insert Table', (onClose) => )
}
/>
+ activeEditor.dispatchCommand(OPEN_FILE_UPLOAD_MODAL_COMMAND, undefined)}
+ />