feat: Added "Upload file" option to the super note Insert menu and block picker menu (#2869)
This commit is contained in:
@@ -41,7 +41,7 @@ export default function useModal(): [
|
|||||||
title: string,
|
title: string,
|
||||||
// eslint-disable-next-line no-shadow
|
// eslint-disable-next-line no-shadow
|
||||||
getContent: (onClose: () => void) => JSX.Element,
|
getContent: (onClose: () => void) => JSX.Element,
|
||||||
closeOnClickOutside = false,
|
closeOnClickOutside = true,
|
||||||
) => {
|
) => {
|
||||||
setModalContent({
|
setModalContent({
|
||||||
closeOnClickOutside,
|
closeOnClickOutside,
|
||||||
|
|||||||
@@ -1,36 +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.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
.Button__root {
|
|
||||||
padding-top: 10px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
padding-left: 15px;
|
|
||||||
padding-right: 15px;
|
|
||||||
border: 0px;
|
|
||||||
background-color: var(--sn-stylekit-contrast-background-color);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.Button__root:hover {
|
|
||||||
background-color: var(--sn-stylekit-info-color);
|
|
||||||
color: var(--sn-stylekit-info-contrast-color);
|
|
||||||
}
|
|
||||||
.Button__small {
|
|
||||||
padding-top: 5px;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
padding-left: 10px;
|
|
||||||
padding-right: 10px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
.Button__disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
.Button__disabled:hover {
|
|
||||||
background-color: var(--sn-stylekit-secondary-background-color);
|
|
||||||
}
|
|
||||||
@@ -1,44 +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 './Button.css'
|
|
||||||
|
|
||||||
import { ReactNode } from 'react'
|
|
||||||
|
|
||||||
import joinClasses from '../Utils/join-classes'
|
|
||||||
|
|
||||||
export default function Button({
|
|
||||||
'data-test-id': dataTestId,
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
onClick,
|
|
||||||
disabled,
|
|
||||||
small,
|
|
||||||
title,
|
|
||||||
}: {
|
|
||||||
'data-test-id'?: string
|
|
||||||
children: ReactNode
|
|
||||||
className?: string
|
|
||||||
disabled?: boolean
|
|
||||||
onClick: () => void
|
|
||||||
small?: boolean
|
|
||||||
title?: string
|
|
||||||
}): JSX.Element {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
disabled={disabled}
|
|
||||||
className={joinClasses('Button__root', disabled && 'Button__disabled', small && 'Button__small', className)}
|
|
||||||
onClick={onClick}
|
|
||||||
title={title}
|
|
||||||
aria-label={title}
|
|
||||||
{...(dataTestId && { 'data-test-id': dataTestId })}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
.DialogActions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: right;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.DialogButtonsList {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: right;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.DialogButtonsList button {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
@@ -1,28 +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 './Dialog.css'
|
|
||||||
|
|
||||||
import { ReactNode } from 'react'
|
|
||||||
|
|
||||||
type Props = Readonly<{
|
|
||||||
'data-test-id'?: string
|
|
||||||
children: ReactNode
|
|
||||||
}>
|
|
||||||
|
|
||||||
export function DialogButtonsList({ children }: Props): JSX.Element {
|
|
||||||
return <div className="DialogButtonsList">{children}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DialogActions({ 'data-test-id': dataTestId, children }: Props): JSX.Element {
|
|
||||||
return (
|
|
||||||
<div className="DialogActions" data-test-id={dataTestId}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,33 +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.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
.Input__wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.Input__label {
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
color: #666;
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
.Input__input {
|
|
||||||
display: flex;
|
|
||||||
flex: 2;
|
|
||||||
border: 1px solid var(--sn-stylekit-contrast-border-color);
|
|
||||||
background-color: var(--sn-stylekit-contrast-background-color);
|
|
||||||
padding-top: 7px;
|
|
||||||
padding-bottom: 7px;
|
|
||||||
padding-left: 10px;
|
|
||||||
padding-right: 10px;
|
|
||||||
font-size: 16px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
@@ -1,64 +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.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
.Modal__overlay {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
position: absolute;
|
|
||||||
flex-direction: column;
|
|
||||||
top: 0px;
|
|
||||||
bottom: 0px;
|
|
||||||
left: 0px;
|
|
||||||
right: 0px;
|
|
||||||
background-color: rgba(0, 0, 0, 0.7);
|
|
||||||
flex-grow: 0px;
|
|
||||||
flex-shrink: 1px;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
.Modal__modal {
|
|
||||||
padding: 20px;
|
|
||||||
min-height: 100px;
|
|
||||||
min-width: 300px;
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 0px;
|
|
||||||
background-color: var(--sn-stylekit-background-color);
|
|
||||||
flex-direction: column;
|
|
||||||
position: relative;
|
|
||||||
box-shadow: 0 0px 0 var(--sn-stylekit-shadow-color);
|
|
||||||
border-radius: 0px;
|
|
||||||
}
|
|
||||||
.Modal__title {
|
|
||||||
color: var(--sn-stylekit-foreground-color);
|
|
||||||
margin: 0px;
|
|
||||||
padding-bottom: 15px;
|
|
||||||
border-bottom: 1px solid var(--sn-stylekit-border-color);
|
|
||||||
}
|
|
||||||
.Modal__closeButton {
|
|
||||||
border: 0px;
|
|
||||||
position: absolute;
|
|
||||||
right: 20px;
|
|
||||||
top: 15px;
|
|
||||||
border-radius: 20px;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: var(--sn-stylekit-contrast-background-color);
|
|
||||||
}
|
|
||||||
.Modal__closeButton:hover {
|
|
||||||
background-color: var(--sn-stylekit-info-color);
|
|
||||||
color: var(--sn-stylekit-info-contrast-color);
|
|
||||||
}
|
|
||||||
.Modal__content {
|
|
||||||
padding-top: 20px;
|
|
||||||
}
|
|
||||||
@@ -7,10 +7,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
import './Modal.css'
|
|
||||||
|
|
||||||
import { ReactNode, useEffect, useRef, useState } from 'react'
|
import { ReactNode, useEffect, useRef, useState } from 'react'
|
||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
|
import Icon from '@/Components/Icon/Icon'
|
||||||
|
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||||
|
|
||||||
function PortalImpl({
|
function PortalImpl({
|
||||||
onClose,
|
onClose,
|
||||||
@@ -33,42 +34,51 @@ function PortalImpl({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let modalOverlayElement: HTMLElement | null = null
|
let modalOverlayElement: HTMLElement | null = null
|
||||||
const handler = (event: KeyboardEvent) => {
|
|
||||||
if (event.keyCode === 27) {
|
const keydownHandler = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === KeyboardKey.Escape) {
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const clickOutsideHandler = (event: MouseEvent) => {
|
const clickOutsideHandler = (event: MouseEvent) => {
|
||||||
const target = event.target
|
const target = event.target
|
||||||
if (modalRef.current !== null && !modalRef.current.contains(target as Node) && closeOnClickOutside) {
|
if (modalRef.current !== null && !modalRef.current.contains(target as Node) && closeOnClickOutside) {
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modalRef.current !== null) {
|
if (modalRef.current !== null) {
|
||||||
modalOverlayElement = modalRef.current?.parentElement
|
modalOverlayElement = modalRef.current.parentElement
|
||||||
if (modalOverlayElement !== null) {
|
if (modalOverlayElement !== null) {
|
||||||
modalOverlayElement?.addEventListener('click', clickOutsideHandler)
|
modalOverlayElement.addEventListener('click', clickOutsideHandler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('keydown', handler)
|
window.addEventListener('keydown', keydownHandler)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', handler)
|
window.removeEventListener('keydown', keydownHandler)
|
||||||
if (modalOverlayElement !== null) {
|
if (modalOverlayElement !== null) {
|
||||||
modalOverlayElement?.removeEventListener('click', clickOutsideHandler)
|
modalOverlayElement.removeEventListener('click', clickOutsideHandler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [closeOnClickOutside, onClose])
|
}, [closeOnClickOutside, onClose])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="Modal__overlay" role="dialog">
|
<div className="absolute inset-0 flex flex-col items-center justify-center bg-[rgba(0,0,0,0.65)]" role="dialog">
|
||||||
<div className="Modal__modal" tabIndex={-1} ref={modalRef}>
|
<div
|
||||||
<h2 className="Modal__title">{title}</h2>
|
className="relative flex min-w-[min(80vw,_20rem)] flex-col rounded border border-border bg-default"
|
||||||
<button className="Modal__closeButton" aria-label="Close modal" type="button" onClick={onClose}>
|
tabIndex={-1}
|
||||||
✕
|
ref={modalRef}
|
||||||
</button>
|
>
|
||||||
<div className="Modal__content">{children}</div>
|
<div className="flex items-center justify-between border-b border-border px-3.5 py-2">
|
||||||
|
<div className="text-sm font-semibold">{title}</div>
|
||||||
|
<button tabIndex={0} className="ml-2 rounded p-1 font-bold hover:bg-contrast" onClick={onClose}>
|
||||||
|
<Icon type="close" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="px-3.5 py-3">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -78,7 +88,7 @@ export default function Modal({
|
|||||||
onClose,
|
onClose,
|
||||||
children,
|
children,
|
||||||
title,
|
title,
|
||||||
closeOnClickOutside = false,
|
closeOnClickOutside = true,
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
closeOnClickOutside?: boolean
|
closeOnClickOutside?: boolean
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<div className="Input__wrapper">
|
|
||||||
<label className="Input__label">{label}</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="Input__input"
|
|
||||||
placeholder={placeholder}
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => {
|
|
||||||
onChange(e.target.value)
|
|
||||||
}}
|
|
||||||
data-test-id={dataTestId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -16,15 +16,16 @@ import {
|
|||||||
URL_MATCHER,
|
URL_MATCHER,
|
||||||
} from '@lexical/react/LexicalAutoEmbedPlugin'
|
} from '@lexical/react/LexicalAutoEmbedPlugin'
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
import { useMemo, useState } from 'react'
|
import { useCallback, useMemo, useState } from 'react'
|
||||||
import * as ReactDOM from 'react-dom'
|
import * as ReactDOM from 'react-dom'
|
||||||
|
|
||||||
import useModal from '../../Lexical/Hooks/useModal'
|
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_TWEET_COMMAND } from '../TwitterPlugin'
|
||||||
import { INSERT_YOUTUBE_COMMAND } from '../YouTubePlugin'
|
import { INSERT_YOUTUBE_COMMAND } from '../YouTubePlugin'
|
||||||
import { classNames } from '@standardnotes/snjs'
|
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 {
|
interface PlaygroundEmbedConfig extends EmbedConfig {
|
||||||
// Human readable name of the embeded content e.g. Tweet or Google Map.
|
// 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 (
|
return (
|
||||||
<div className="w-[600px] max-w-[90vw]">
|
<>
|
||||||
<div className="Input__wrapper">
|
<label className="flex flex-col gap-1.5">
|
||||||
<input
|
URL:
|
||||||
type="text"
|
<DecoratedInput
|
||||||
className="Input__input"
|
|
||||||
placeholder={embedConfig.exampleUrl}
|
|
||||||
value={text}
|
value={text}
|
||||||
data-test-id={`${embedConfig.type}-embed-modal-url`}
|
onChange={(text) => {
|
||||||
onChange={(e) => {
|
setText(text)
|
||||||
const { value } = e.target
|
validateText(text)
|
||||||
setText(value)
|
|
||||||
validateText(value)
|
|
||||||
}}
|
}}
|
||||||
|
ref={focusOnMount}
|
||||||
/>
|
/>
|
||||||
</div>
|
</label>
|
||||||
<DialogActions>
|
<div className="mt-2.5 flex justify-end">
|
||||||
<Button disabled={!embedResult} onClick={onClick} data-test-id={`${embedConfig.type}-embed-modal-submit-btn`}>
|
<Button disabled={!embedResult} onClick={onClick} small={isMobileScreen()}>
|
||||||
Embed
|
Embed
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</div>
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { GetQuoteBlockOption } from '../Blocks/Quote'
|
|||||||
import { GetDividerBlockOption } from '../Blocks/Divider'
|
import { GetDividerBlockOption } from '../Blocks/Divider'
|
||||||
import { GetCollapsibleBlockOption } from '../Blocks/Collapsible'
|
import { GetCollapsibleBlockOption } from '../Blocks/Collapsible'
|
||||||
import { GetEmbedsBlockOptions } from '../Blocks/Embeds'
|
import { GetEmbedsBlockOptions } from '../Blocks/Embeds'
|
||||||
|
import { GetUploadFileOption } from '../Blocks/File'
|
||||||
|
|
||||||
export default function BlockPickerMenuPlugin({ popoverZIndex }: { popoverZIndex?: string }): JSX.Element {
|
export default function BlockPickerMenuPlugin({ popoverZIndex }: { popoverZIndex?: string }): JSX.Element {
|
||||||
const [editor] = useLexicalComposerContext()
|
const [editor] = useLexicalComposerContext()
|
||||||
@@ -57,6 +58,7 @@ export default function BlockPickerMenuPlugin({ popoverZIndex }: { popoverZIndex
|
|||||||
GetRemoteImageBlockOption(() => {
|
GetRemoteImageBlockOption(() => {
|
||||||
showModal('Insert image from URL', (onClose) => <InsertRemoteImageDialog onClose={onClose} />)
|
showModal('Insert image from URL', (onClose) => <InsertRemoteImageDialog onClose={onClose} />)
|
||||||
}),
|
}),
|
||||||
|
GetUploadFileOption(editor),
|
||||||
GetNumberedListBlockOption(editor),
|
GetNumberedListBlockOption(editor),
|
||||||
GetBulletedListBlockOption(editor),
|
GetBulletedListBlockOption(editor),
|
||||||
GetChecklistBlockOption(editor),
|
GetChecklistBlockOption(editor),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { INSERT_FILE_COMMAND } from '../Commands'
|
import { INSERT_FILE_COMMAND } from '../Commands'
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
|
|
||||||
import { useEffect } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { FileNode } from './Nodes/FileNode'
|
import { FileNode } from './Nodes/FileNode'
|
||||||
import {
|
import {
|
||||||
$createParagraphNode,
|
$createParagraphNode,
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
COMMAND_PRIORITY_NORMAL,
|
COMMAND_PRIORITY_NORMAL,
|
||||||
PASTE_COMMAND,
|
PASTE_COMMAND,
|
||||||
$isRootOrShadowRoot,
|
$isRootOrShadowRoot,
|
||||||
|
createCommand,
|
||||||
} from 'lexical'
|
} from 'lexical'
|
||||||
import { $createFileNode } from './Nodes/FileUtils'
|
import { $createFileNode } from './Nodes/FileUtils'
|
||||||
import { mergeRegister, $wrapNodeInElement } from '@lexical/utils'
|
import { mergeRegister, $wrapNodeInElement } from '@lexical/utils'
|
||||||
@@ -18,6 +19,70 @@ import { FilesControllerEvent } from '@/Controllers/FilesController'
|
|||||||
import { useLinkingController } from '@/Controllers/LinkingControllerProvider'
|
import { useLinkingController } from '@/Controllers/LinkingControllerProvider'
|
||||||
import { useApplication } from '@/Components/ApplicationProvider'
|
import { useApplication } from '@/Components/ApplicationProvider'
|
||||||
import { SNNote } from '@standardnotes/snjs'
|
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<File>()
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
onChange={(event) => {
|
||||||
|
const filesList = event.target.files
|
||||||
|
if (filesList && filesList.length === 1) {
|
||||||
|
setFile(filesList[0])
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="mt-1.5 flex justify-end">
|
||||||
|
{isUploadingFile ? (
|
||||||
|
<Spinner className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Button onClick={onClick} disabled={!file} small={isMobileScreen()}>
|
||||||
|
Upload
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JSX.Element | null {
|
export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JSX.Element | null {
|
||||||
const application = useApplication()
|
const application = useApplication()
|
||||||
@@ -25,6 +90,8 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS
|
|||||||
const filesController = useFilesController()
|
const filesController = useFilesController()
|
||||||
const linkingController = useLinkingController()
|
const linkingController = useLinkingController()
|
||||||
|
|
||||||
|
const [showFileUploadModal, setShowFileUploadModal] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor.hasNodes([FileNode])) {
|
if (!editor.hasNodes([FileNode])) {
|
||||||
throw new Error('FilePlugin: FileNode not registered on editor')
|
throw new Error('FilePlugin: FileNode not registered on editor')
|
||||||
@@ -63,6 +130,14 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS
|
|||||||
},
|
},
|
||||||
COMMAND_PRIORITY_EDITOR,
|
COMMAND_PRIORITY_EDITOR,
|
||||||
),
|
),
|
||||||
|
editor.registerCommand(
|
||||||
|
OPEN_FILE_UPLOAD_MODAL_COMMAND,
|
||||||
|
() => {
|
||||||
|
setShowFileUploadModal(true)
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
COMMAND_PRIORITY_NORMAL,
|
||||||
|
),
|
||||||
editor.registerCommand(
|
editor.registerCommand(
|
||||||
PASTE_COMMAND,
|
PASTE_COMMAND,
|
||||||
(payload) => {
|
(payload) => {
|
||||||
@@ -103,5 +178,13 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS
|
|||||||
return disposer
|
return disposer
|
||||||
}, [filesController, editor])
|
}, [filesController, editor])
|
||||||
|
|
||||||
|
if (showFileUploadModal) {
|
||||||
|
return (
|
||||||
|
<Modal onClose={() => setShowFileUploadModal(false)} title="Upload File">
|
||||||
|
<UploadFileDialog currentNote={currentNote} onClose={() => setShowFileUploadModal(false)} />
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
import { $createParagraphNode, $insertNodes, $isRootOrShadowRoot, COMMAND_PRIORITY_NORMAL } from 'lexical'
|
import { $createParagraphNode, $insertNodes, $isRootOrShadowRoot, COMMAND_PRIORITY_NORMAL } from 'lexical'
|
||||||
import { useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import Button from '../../Lexical/UI/Button'
|
|
||||||
import { DialogActions } from '../../Lexical/UI/Dialog'
|
|
||||||
import TextInput from '../../Lexical/UI/TextInput'
|
|
||||||
import { INSERT_REMOTE_IMAGE_COMMAND } from '../Commands'
|
import { INSERT_REMOTE_IMAGE_COMMAND } from '../Commands'
|
||||||
import { $createRemoteImageNode, RemoteImageNode } from './RemoteImageNode'
|
import { $createRemoteImageNode, RemoteImageNode } from './RemoteImageNode'
|
||||||
import { mergeRegister, $wrapNodeInElement } from '@lexical/utils'
|
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 }) {
|
export function InsertRemoteImageDialog({ onClose }: { onClose: () => void }) {
|
||||||
const [url, setURL] = useState('')
|
const [url, setURL] = useState('')
|
||||||
@@ -21,12 +21,23 @@ export function InsertRemoteImageDialog({ onClose }: { onClose: () => void }) {
|
|||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const focusOnMount = useCallback((element: HTMLInputElement | null) => {
|
||||||
|
if (element) {
|
||||||
|
setTimeout(() => element.focus())
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TextInput label="URL:" onChange={setURL} value={url} />
|
<label className="flex flex-col gap-1.5">
|
||||||
<DialogActions>
|
URL:
|
||||||
<Button onClick={onClick}>Confirm</Button>
|
<DecoratedInput value={url} onChange={setURL} ref={focusOnMount} />
|
||||||
</DialogActions>
|
</label>
|
||||||
|
<div className="mt-2.5 flex justify-end">
|
||||||
|
<Button onClick={onClick} disabled={!url} small={isMobileScreen()}>
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,12 @@
|
|||||||
|
|
||||||
import { INSERT_TABLE_COMMAND, TableNode, TableRowNode } from '@lexical/table'
|
import { INSERT_TABLE_COMMAND, TableNode, TableRowNode } from '@lexical/table'
|
||||||
import { $createParagraphNode, LexicalEditor } from 'lexical'
|
import { $createParagraphNode, LexicalEditor } from 'lexical'
|
||||||
import { useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import Button from '../Lexical/UI/Button'
|
|
||||||
import { DialogActions } from '../Lexical/UI/Dialog'
|
|
||||||
import TextInput from '../Lexical/UI/TextInput'
|
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
import { mergeRegister } from '@lexical/utils'
|
import { mergeRegister } from '@lexical/utils'
|
||||||
|
import DecoratedInput from '@/Components/Input/DecoratedInput'
|
||||||
|
import Button from '@/Components/Button/Button'
|
||||||
|
import { isMobileScreen } from '../../../Utils'
|
||||||
|
|
||||||
export function InsertTableDialog({
|
export function InsertTableDialog({
|
||||||
activeEditor,
|
activeEditor,
|
||||||
@@ -30,13 +30,27 @@ export function InsertTableDialog({
|
|||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const focusOnMount = useCallback((element: HTMLInputElement | null) => {
|
||||||
|
if (element) {
|
||||||
|
setTimeout(() => element.focus())
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TextInput label="Number of rows" onChange={setRows} value={rows} />
|
<label className="mb-2.5 flex items-center justify-between gap-3">
|
||||||
<TextInput label="Number of columns" onChange={setColumns} value={columns} />
|
Rows:
|
||||||
<DialogActions data-test-id="table-model-confirm-insert">
|
<DecoratedInput type="number" value={rows} onChange={setRows} ref={focusOnMount} />
|
||||||
<Button onClick={onClick}>Confirm</Button>
|
</label>
|
||||||
</DialogActions>
|
<label className="mb-2.5 flex items-center justify-between gap-3">
|
||||||
|
Columns:
|
||||||
|
<DecoratedInput type="number" value={columns} onChange={setColumns} />
|
||||||
|
</label>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button onClick={onClick} small={isMobileScreen()}>
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ import usePreference from '@/Hooks/usePreference'
|
|||||||
import { ElementIds } from '@/Constants/ElementIDs'
|
import { ElementIds } from '@/Constants/ElementIDs'
|
||||||
import { $isDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
|
import { $isDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
|
||||||
import LinkViewer from './LinkViewer'
|
import LinkViewer from './LinkViewer'
|
||||||
|
import { OPEN_FILE_UPLOAD_MODAL_COMMAND } from '../EncryptedFilePlugin/FilePlugin'
|
||||||
|
|
||||||
const TOGGLE_LINK_AND_EDIT_COMMAND = createCommand<string | null>('TOGGLE_LINK_AND_EDIT_COMMAND')
|
const TOGGLE_LINK_AND_EDIT_COMMAND = createCommand<string | null>('TOGGLE_LINK_AND_EDIT_COMMAND')
|
||||||
|
|
||||||
@@ -1048,6 +1049,11 @@ const ToolbarPlugin = () => {
|
|||||||
showModal('Insert Table', (onClose) => <InsertTableDialog activeEditor={editor} onClose={onClose} />)
|
showModal('Insert Table', (onClose) => <InsertTableDialog activeEditor={editor} onClose={onClose} />)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<ToolbarMenuItem
|
||||||
|
name="Upload file"
|
||||||
|
iconName="file"
|
||||||
|
onClick={() => activeEditor.dispatchCommand(OPEN_FILE_UPLOAD_MODAL_COMMAND, undefined)}
|
||||||
|
/>
|
||||||
<ToolbarMenuItem
|
<ToolbarMenuItem
|
||||||
name="Image from URL"
|
name="Image from URL"
|
||||||
iconName="image"
|
iconName="image"
|
||||||
|
|||||||
Reference in New Issue
Block a user