Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: super improvements #1995

Merged
merged 4 commits into from
Nov 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 8 additions & 2 deletions packages/blocks-editor/src/Editor/BlocksEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const BlockDragEnabled = false;
type BlocksEditorProps = {
onChange: (value: string, preview: string) => void;
className?: string;
children: React.ReactNode;
children?: React.ReactNode;
previewLength: number;
spellcheck?: boolean;
};
Expand All @@ -48,8 +48,14 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
previewLength,
spellcheck,
}) => {
const [didIgnoreFirstChange, setDidIgnoreFirstChange] = useState(false);
const handleChange = useCallback(
(editorState: EditorState, _editor: LexicalEditor) => {
if (!didIgnoreFirstChange) {
setDidIgnoreFirstChange(true);
return;
}

editorState.read(() => {
const childrenNodes = $getRoot().getAllTextNodes().slice(0, 2);
let previewText = '';
Expand All @@ -65,7 +71,7 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
onChange(stringifiedEditorState, previewText);
});
},
[onChange],
[onChange, didIgnoreFirstChange],
);

const [floatingAnchorElem, setFloatingAnchorElem] =
Expand Down
6 changes: 4 additions & 2 deletions packages/blocks-editor/src/Editor/BlocksEditorComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,19 @@ import {Klass, LexicalNode} from 'lexical';
type BlocksEditorComposerProps = {
initialValue: string;
children: React.ReactNode;
nodes: Array<Klass<LexicalNode>>;
nodes?: Array<Klass<LexicalNode>>;
readonly?: boolean;
};

export const BlocksEditorComposer: FunctionComponent<
BlocksEditorComposerProps
> = ({initialValue, children, nodes}) => {
> = ({initialValue, children, readonly, nodes = []}) => {
return (
<LexicalComposer
initialConfig={{
namespace: 'BlocksEditor',
theme: BlocksEditorTheme,
editable: !readonly,
onError: (error: Error) => console.error(error),
editorState:
initialValue && initialValue.length > 0 ? initialValue : undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { NodeObserverPlugin } from './Plugins/NodeObserverPlugin/NodeObserverPlu
import { FilesController } from '@/Controllers/FilesController'
import FilesControllerProvider from '@/Controllers/FilesControllerProvider'
import DatetimePlugin from './Plugins/DateTimePlugin/DateTimePlugin'
import AutoLinkPlugin from './Plugins/AutoLinkPlugin/AutoLinkPlugin'

const NotePreviewCharLimit = 160

Expand Down Expand Up @@ -58,7 +59,7 @@ export const BlockEditor: FunctionComponent<Props> = ({
<ErrorBoundary>
<LinkingControllerProvider controller={linkingController}>
<FilesControllerProvider controller={filesController}>
<BlocksEditorComposer initialValue={note.text} nodes={[FileNode, BubbleNode]}>
<BlocksEditorComposer readonly={note.locked} initialValue={note.text} nodes={[FileNode, BubbleNode]}>
<BlocksEditor
onChange={handleChange}
className="relative relative resize-none text-base focus:shadow-none focus:outline-none"
Expand All @@ -70,6 +71,7 @@ export const BlockEditor: FunctionComponent<Props> = ({
<ItemBubblePlugin />
<BlockPickerMenuPlugin />
<DatetimePlugin />
<AutoLinkPlugin />
<NodeObserverPlugin nodeType={BubbleNode} onRemove={handleBubbleRemove} />
<NodeObserverPlugin nodeType={FileNode} onRemove={handleBubbleRemove} />
</BlocksEditor>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { COMMAND_PRIORITY_EDITOR, KEY_MODIFIER_COMMAND, $getSelection } from 'lexical'
import { useEffect } from 'react'
import { TOGGLE_LINK_COMMAND } from '@lexical/link'
import { mergeRegister } from '@lexical/utils'

export default function AutoLinkPlugin(): JSX.Element | null {
const [editor] = useLexicalComposerContext()

useEffect(() => {
return mergeRegister(
editor.registerCommand(
KEY_MODIFIER_COMMAND,
(event: KeyboardEvent) => {
const isCmdK = event.key === 'k' && !event.altKey && (event.metaKey || event.ctrlKey)
if (isCmdK) {
const selection = $getSelection()
if (selection) {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, selection.getTextContent())
}
}

return false
},
COMMAND_PRIORITY_EDITOR,
),
)
}, [editor])

return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { useEffect } from 'react'
import { $convertFromMarkdownString, TRANSFORMERS } from '@lexical/markdown'
import { $generateNodesFromDOM } from '@lexical/html'
import { $createParagraphNode, $createRangeSelection } from 'lexical'

/** Note that markdown conversion does not insert new lines. See: https://github.com/facebook/lexical/issues/2815 */
export default function ImportPlugin({ text, format }: { text: string; format: 'md' | 'html' }): JSX.Element | null {
const [editor] = useLexicalComposerContext()

useEffect(() => {
editor.update(() => {
if (format === 'md') {
$convertFromMarkdownString(text, [...TRANSFORMERS])
} else {
const parser = new DOMParser()
const dom = parser.parseFromString(text, 'text/html')
const nodes = $generateNodesFromDOM(editor, dom)
const selection = $createRangeSelection()
const newLineNode = $createParagraphNode()
selection.insertNodes([newLineNode, ...nodes])
}
})
}, [editor, text, format])

return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { WebApplication } from '@/Application/Application'
import { NoteType, SNNote } from '@standardnotes/snjs'
import { FunctionComponent, useCallback, useState } from 'react'
import { BlockEditorController } from './BlockEditorController'
import { BlocksEditor, BlocksEditorComposer } from '@standardnotes/blocks-editor'
import { ErrorBoundary } from '@/Utils/ErrorBoundary'
import ModalDialog from '@/Components/Shared/ModalDialog'
import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
import Button from '@/Components/Button/Button'
import ImportPlugin from './Plugins/ImportPlugin/ImportPlugin'

export function spaceSeparatedStrings(...strings: string[]): string {
return strings.join(' ')
}

const NotePreviewCharLimit = 160

type Props = {
application: WebApplication
note: SNNote
closeDialog: () => void
onConvertComplete: () => void
}

export const SuperNoteImporter: FunctionComponent<Props> = ({ note, application, closeDialog, onConvertComplete }) => {
const [lastValue, setLastValue] = useState({ text: '', previewPlain: '' })

const format =
!note.noteType || [NoteType.Plain, NoteType.Markdown, NoteType.Code, NoteType.Task].includes(note.noteType)
? 'md'
: 'html'

const handleChange = useCallback((value: string, preview: string) => {
setLastValue({ text: value, previewPlain: preview })
}, [])

const confirmConvert = useCallback(() => {
const controller = new BlockEditorController(note, application)
void controller.save({ text: lastValue.text, previewPlain: lastValue.previewPlain, previewHtml: undefined })
closeDialog()
onConvertComplete()
}, [closeDialog, application, lastValue, note, onConvertComplete])

const convertAsIs = useCallback(async () => {
const confirmed = await application.alertService.confirm(
spaceSeparatedStrings(
"This option is useful if you switched this note's type from Super to another plaintext-based format, and want to return to Super.",
'To use this option, the preview in the convert window should display a language format known as JSON.',
'If this is not the case, cancel this prompt.',
),
'Are you sure?',
)
if (!confirmed) {
return
}

const controller = new BlockEditorController(note, application)
void controller.save({ text: note.text, previewPlain: note.preview_plain, previewHtml: undefined })
closeDialog()
onConvertComplete()
}, [closeDialog, application, note, onConvertComplete])

return (
<ModalDialog>
<ModalDialogLabel closeDialog={closeDialog}>
Convert to Super note
<p className="text-sm font-normal text-neutral">
The following is a preview of how your note will look when converted to Super. Super notes use a custom format
under the hood. Converting your note will transition it from plaintext to the custom Super format.
</p>
</ModalDialogLabel>
<ModalDialogDescription>
<div className="relative w-full">
<ErrorBoundary>
<BlocksEditorComposer readonly initialValue={''}>
<BlocksEditor
onChange={handleChange}
className="relative relative resize-none text-base focus:shadow-none focus:outline-none"
previewLength={NotePreviewCharLimit}
spellcheck={note.spellcheck}
>
<ImportPlugin text={note.text} format={format} />
</BlocksEditor>
</BlocksEditorComposer>
</ErrorBoundary>
</div>
</ModalDialogDescription>
<ModalDialogButtons>
<div className="flex w-full justify-between">
<div>
<Button onClick={convertAsIs}>Convert As-Is</Button>
</div>
<div className="flex">
<Button onClick={closeDialog}>Cancel</Button>
<div className="min-w-3" />
<Button primary onClick={confirmConvert}>
Convert to Super
</Button>
</div>
</div>
</ModalDialogButtons>
</ModalDialog>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const ChangeEditorButton: FunctionComponent<Props> = ({
return note ? application.componentManager.editorForNote(note) : undefined
})
const [selectedEditorIcon, selectedEditorIconTint] = getIconAndTintForNoteType(selectedEditor?.package_info.note_type)
const [isClickOutsideDisabled, setIsClickOutsideDisabled] = useState(false)

const toggleMenu = useCallback(async () => {
const willMenuOpen = !isOpen
Expand All @@ -35,6 +36,10 @@ const ChangeEditorButton: FunctionComponent<Props> = ({
setIsOpen(willMenuOpen)
}, [onClickPreprocessing, isOpen])

const disableClickOutside = useCallback(() => {
setIsClickOutsideDisabled(true)
}, [])

return (
<div ref={containerRef}>
<RoundIconButton
Expand All @@ -44,11 +49,18 @@ const ChangeEditorButton: FunctionComponent<Props> = ({
icon={selectedEditorIcon}
iconClassName={`text-accessory-tint-${selectedEditorIconTint}`}
/>
<Popover togglePopover={toggleMenu} anchorElement={buttonRef.current} open={isOpen} className="pt-2 md:pt-0">
<Popover
togglePopover={toggleMenu}
disableClickOutside={isClickOutsideDisabled}
anchorElement={buttonRef.current}
open={isOpen}
className="pt-2 md:pt-0"
>
<ChangeEditorMenu
application={application}
isVisible={isOpen}
note={note}
handleDisableClickoutsideRequest={disableClickOutside}
closeMenu={() => {
setIsOpen(false)
}}
Expand Down