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

feat: option to show markdown preview for super notes #2048

Merged
merged 2 commits into from Nov 23, 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
31 changes: 12 additions & 19 deletions packages/blocks-editor/src/Editor/BlocksEditor.tsx
Expand Up @@ -7,12 +7,6 @@ import {CheckListPlugin} from '@lexical/react/LexicalCheckListPlugin';
import {ClearEditorPlugin} from '@lexical/react/LexicalClearEditorPlugin';
import {MarkdownShortcutPlugin} from '@lexical/react/LexicalMarkdownShortcutPlugin';
import {TablePlugin} from '@lexical/react/LexicalTablePlugin';
import {
CHECK_LIST,
ELEMENT_TRANSFORMERS,
TEXT_FORMAT_TRANSFORMERS,
TEXT_MATCH_TRANSFORMERS,
} from '@lexical/markdown';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import {HashtagPlugin} from '@lexical/react/LexicalHashtagPlugin';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
Expand All @@ -32,15 +26,17 @@ import {truncateString} from './Utils';
import {SuperEditorContentId} from './Constants';
import {classNames} from '@standardnotes/utils';
import {EditorLineHeight} from '@standardnotes/snjs';
import {MarkdownTransformers} from './MarkdownTransformers';

type BlocksEditorProps = {
onChange: (value: string, preview: string) => void;
onChange?: (value: string, preview: string) => void;
className?: string;
children?: React.ReactNode;
previewLength: number;
previewLength?: number;
spellcheck?: boolean;
ignoreFirstChange?: boolean;
lineHeight?: EditorLineHeight;
readonly?: boolean;
};

export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
Expand All @@ -51,6 +47,7 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
spellcheck,
ignoreFirstChange = false,
lineHeight,
readonly,
}) => {
const [didIgnoreFirstChange, setDidIgnoreFirstChange] = useState(false);
const handleChange = useCallback(
Expand All @@ -69,11 +66,14 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
previewText += '\n';
}
});
previewText = truncateString(previewText, previewLength);

if (previewLength) {
previewText = truncateString(previewText, previewLength);
}

try {
const stringifiedEditorState = JSON.stringify(editorState.toJSON());
onChange(stringifiedEditorState, previewText);
onChange?.(stringifiedEditorState, previewText);
} catch (error) {
window.alert(
`An invalid change was made inside the Super editor. Your change was not saved. Please report this error to the team: ${error}`,
Expand Down Expand Up @@ -116,14 +116,7 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
ErrorBoundary={LexicalErrorBoundary}
/>
<ListPlugin />
<MarkdownShortcutPlugin
transformers={[
CHECK_LIST,
...ELEMENT_TRANSFORMERS,
...TEXT_FORMAT_TRANSFORMERS,
...TEXT_MATCH_TRANSFORMERS,
]}
/>
<MarkdownShortcutPlugin transformers={MarkdownTransformers} />
<TablePlugin />
<OnChangePlugin onChange={handleChange} ignoreSelectionChange={true} />
<HistoryPlugin />
Expand All @@ -138,7 +131,7 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
<TwitterPlugin />
<YouTubePlugin />
<CollapsiblePlugin />
{floatingAnchorElem && (
{!readonly && floatingAnchorElem && (
<>
<FloatingTextFormatToolbarPlugin anchorElem={floatingAnchorElem} />
<FloatingLinkEditorPlugin />
Expand Down
13 changes: 13 additions & 0 deletions packages/blocks-editor/src/Editor/MarkdownTransformers.ts
@@ -0,0 +1,13 @@
import {
CHECK_LIST,
ELEMENT_TRANSFORMERS,
TEXT_FORMAT_TRANSFORMERS,
TEXT_MATCH_TRANSFORMERS,
} from '@lexical/markdown';

export const MarkdownTransformers = [
CHECK_LIST,
...ELEMENT_TRANSFORMERS,
...TEXT_FORMAT_TRANSFORMERS,
...TEXT_MATCH_TRANSFORMERS,
];
1 change: 1 addition & 0 deletions packages/blocks-editor/src/index.ts
@@ -1,3 +1,4 @@
export * from './Editor/BlocksEditor';
export * from './Editor/BlocksEditorComposer';
export * from './Editor/Constants';
export * from './Editor/MarkdownTransformers';
1 change: 1 addition & 0 deletions packages/ui-services/src/Keyboard/KeyboardCommands.ts
Expand Up @@ -24,3 +24,4 @@ export const OPEN_NOTE_HISTORY_COMMAND = createKeyboardCommand('OPEN_NOTE_HISTOR
export const CAPTURE_SAVE_COMMAND = createKeyboardCommand('CAPTURE_SAVE_COMMAND')
export const STAR_NOTE_COMMAND = createKeyboardCommand('STAR_NOTE_COMMAND')
export const PIN_NOTE_COMMAND = createKeyboardCommand('PIN_NOTE_COMMAND')
export const SUPER_SHOW_MARKDOWN_PREVIEW = createKeyboardCommand('SUPER_SHOW_MARKDOWN_PREVIEW')
16 changes: 16 additions & 0 deletions packages/ui-services/src/Keyboard/KeyboardService.ts
Expand Up @@ -165,6 +165,22 @@ export class KeyboardService {
}
}

public triggerCommand(command: KeyboardCommand): void {
for (const observer of this.commandHandlers) {
if (observer.command !== command) {
continue
}

const callback = observer.onKeyDown || observer.onKeyUp
if (callback) {
const exclusive = callback(new KeyboardEvent('command-trigger'))
if (exclusive) {
return
}
}
}
}

registerShortcut(shortcut: KeyboardShortcut): void {
this.commandMap.set(shortcut.command, shortcut)
}
Expand Down
7 changes: 7 additions & 0 deletions packages/ui-services/src/Keyboard/getKeyboardShortcuts.ts
Expand Up @@ -21,6 +21,7 @@ import {
CAPTURE_SAVE_COMMAND,
STAR_NOTE_COMMAND,
PIN_NOTE_COMMAND,
SUPER_SHOW_MARKDOWN_PREVIEW,
} from './KeyboardCommands'
import { KeyboardKey } from './KeyboardKey'
import { KeyboardModifier } from './KeyboardModifier'
Expand Down Expand Up @@ -132,5 +133,11 @@ export function getKeyboardShortcuts(platform: Platform, _environment: Environme
modifiers: [primaryModifier, KeyboardModifier.Shift],
preventDefault: true,
},
{
command: SUPER_SHOW_MARKDOWN_PREVIEW,
key: 'm',
modifiers: [primaryModifier, KeyboardModifier.Shift],
preventDefault: true,
},
]
}
@@ -0,0 +1,26 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { useEffect } from 'react'
import { $createCodeNode } from '@lexical/code'
import { $createTextNode, $getRoot } from 'lexical'
import { $convertToMarkdownString } from '@lexical/markdown'
import { MarkdownTransformers } from '@standardnotes/blocks-editor'

type Props = {
onMarkdown: (markdown: string) => void
}

export default function MarkdownPreviewPlugin({ onMarkdown }: Props): JSX.Element | null {
const [editor] = useLexicalComposerContext()

useEffect(() => {
editor.update(() => {
const root = $getRoot()
const markdown = $convertToMarkdownString(MarkdownTransformers)
root.clear().append($createCodeNode('markdown').append($createTextNode(markdown)))
root.selectEnd()
onMarkdown(markdown)
})
}, [editor, onMarkdown])

return null
}
Expand Up @@ -23,6 +23,9 @@ import {
} from './Plugins/ChangeContentCallback/ChangeContentCallback'
import PasswordPlugin from './Plugins/PasswordPlugin/PasswordPlugin'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { useCommandService } from '@/Components/ApplicationView/CommandProvider'
import { SUPER_SHOW_MARKDOWN_PREVIEW } from '@standardnotes/ui-services'
import { SuperNoteMarkdownPreview } from './SuperNoteMarkdownPreview'

const NotePreviewCharLimit = 160

Expand All @@ -44,6 +47,20 @@ export const SuperEditor: FunctionComponent<Props> = ({
const note = useRef(controller.item)
const changeEditorFunction = useRef<ChangeEditorFunction>()
const ignoreNextChange = useRef(false)
const [showMarkdownPreview, setShowMarkdownPreview] = useState(false)

const commandService = useCommandService()

useEffect(() => {
return commandService.addCommandHandler({
command: SUPER_SHOW_MARKDOWN_PREVIEW,
onKeyDown: () => setShowMarkdownPreview(true),
})
}, [commandService])

const closeMarkdownPreview = useCallback(() => {
setShowMarkdownPreview(false)
}, [])

const [lineHeight, setLineHeight] = useState<EditorLineHeight>(PrefDefaults[PrefKey.EditorLineHeight])

Expand Down Expand Up @@ -110,37 +127,41 @@ export const SuperEditor: FunctionComponent<Props> = ({
return (
<div className="relative h-full w-full">
<ErrorBoundary>
<LinkingControllerProvider controller={linkingController}>
<FilesControllerProvider controller={filesController}>
<BlocksEditorComposer
readonly={note.current.locked}
initialValue={note.current.text}
nodes={[FileNode, BubbleNode]}
>
<BlocksEditor
onChange={handleChange}
ignoreFirstChange={true}
className="relative h-full resize-none px-6 py-4 text-base focus:shadow-none focus:outline-none"
previewLength={NotePreviewCharLimit}
spellcheck={spellcheck}
lineHeight={lineHeight}
<>
<LinkingControllerProvider controller={linkingController}>
<FilesControllerProvider controller={filesController}>
<BlocksEditorComposer
readonly={note.current.locked}
initialValue={note.current.text}
nodes={[FileNode, BubbleNode]}
>
<ItemSelectionPlugin currentNote={note.current} />
<FilePlugin />
<ItemBubblePlugin />
<BlockPickerMenuPlugin />
<DatetimePlugin />
<PasswordPlugin />
<AutoLinkPlugin />
<ChangeContentCallbackPlugin
providerCallback={(callback) => (changeEditorFunction.current = callback)}
/>
<NodeObserverPlugin nodeType={BubbleNode} onRemove={handleBubbleRemove} />
<NodeObserverPlugin nodeType={FileNode} onRemove={handleBubbleRemove} />
</BlocksEditor>
</BlocksEditorComposer>
</FilesControllerProvider>
</LinkingControllerProvider>
<BlocksEditor
onChange={handleChange}
ignoreFirstChange={true}
className="relative h-full resize-none px-6 py-4 text-base focus:shadow-none focus:outline-none"
previewLength={NotePreviewCharLimit}
spellcheck={spellcheck}
lineHeight={lineHeight}
>
<ItemSelectionPlugin currentNote={note.current} />
<FilePlugin />
<ItemBubblePlugin />
<BlockPickerMenuPlugin />
<DatetimePlugin />
<PasswordPlugin />
<AutoLinkPlugin />
<ChangeContentCallbackPlugin
providerCallback={(callback) => (changeEditorFunction.current = callback)}
/>
<NodeObserverPlugin nodeType={BubbleNode} onRemove={handleBubbleRemove} />
<NodeObserverPlugin nodeType={FileNode} onRemove={handleBubbleRemove} />
</BlocksEditor>
</BlocksEditorComposer>
</FilesControllerProvider>
</LinkingControllerProvider>

{showMarkdownPreview && <SuperNoteMarkdownPreview note={note.current} closeDialog={closeMarkdownPreview} />}
</>
</ErrorBoundary>
</div>
)
Expand Down
Expand Up @@ -10,10 +10,7 @@ import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
import Button from '@/Components/Button/Button'
import ImportPlugin from './Plugins/ImportPlugin/ImportPlugin'
import { NoteViewController } from '../Controller/NoteViewController'

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

const NotePreviewCharLimit = 160

Expand Down Expand Up @@ -103,6 +100,7 @@ export const SuperNoteImporter: FunctionComponent<Props> = ({ note, application,
<ErrorBoundary>
<BlocksEditorComposer readonly initialValue={undefined}>
<BlocksEditor
readonly
onChange={handleChange}
ignoreFirstChange={false}
className="relative resize-none text-base focus:shadow-none focus:outline-none"
Expand Down
@@ -0,0 +1,65 @@
import { SNNote } from '@standardnotes/snjs'
import { FunctionComponent, useCallback, useState } from 'react'
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 MarkdownPreviewPlugin from './Plugins/MarkdownPreviewPlugin/MarkdownPreviewPlugin'
import { FileNode } from './Plugins/EncryptedFilePlugin/Nodes/FileNode'
import { BubbleNode } from './Plugins/ItemBubblePlugin/Nodes/BubbleNode'
import { copyTextToClipboard } from '../../../Utils/copyTextToClipboard'

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

export const SuperNoteMarkdownPreview: FunctionComponent<Props> = ({ note, closeDialog }) => {
const [markdown, setMarkdown] = useState('')
const [didCopy, setDidCopy] = useState(false)

const copy = useCallback(() => {
copyTextToClipboard(markdown)
setDidCopy(true)
setTimeout(() => {
setDidCopy(false)
}, 1500)
}, [markdown])

const onMarkdown = useCallback((markdown: string) => {
setMarkdown(markdown)
}, [])

return (
<ModalDialog>
<ModalDialogLabel closeDialog={closeDialog}>Markdown Preview</ModalDialogLabel>
<ModalDialogDescription>
<div className="relative w-full">
<ErrorBoundary>
<BlocksEditorComposer readonly initialValue={note.text} nodes={[FileNode, BubbleNode]}>
<BlocksEditor
readonly
className="relative resize-none text-base focus:shadow-none focus:outline-none"
spellcheck={note.spellcheck}
>
<MarkdownPreviewPlugin onMarkdown={onMarkdown} />
</BlocksEditor>
</BlocksEditorComposer>
</ErrorBoundary>
</div>
</ModalDialogDescription>
<ModalDialogButtons>
<div className="flex">
<Button onClick={closeDialog}>Close</Button>
<div className="min-w-3" />
<Button primary onClick={copy}>
{didCopy ? 'Copied' : 'Copy'}
</Button>
</div>
</ModalDialogButtons>
</ModalDialog>
)
}