From 90a33537bfe6abfc5c24095042502abb1c1af18e Mon Sep 17 00:00:00 2001 From: Mo Date: Wed, 23 Nov 2022 10:50:19 -0600 Subject: [PATCH 1/2] feat: option to show markdown preview for super notes --- .../blocks-editor/src/Editor/BlocksEditor.tsx | 31 +-- .../src/Editor/MarkdownTransformers.ts | 13 ++ packages/blocks-editor/src/index.ts | 1 + .../src/Keyboard/KeyboardCommands.ts | 1 + .../src/Keyboard/getKeyboardShortcuts.ts | 7 + .../MarkdownPreviewPlugin.tsx | 26 +++ .../NoteView/SuperEditor/SuperEditor.tsx | 81 ++++--- .../SuperEditor/SuperNoteImporter.tsx | 6 +- .../SuperEditor/SuperNoteMarkdownPreview.tsx | 65 ++++++ .../NotesContextMenu/NotesContextMenu.tsx | 9 +- .../NotesOptions/DeletePermanentlyButton.tsx | 19 ++ .../{ => Listed}/ListedActionsMenu.tsx | 0 .../{ => Listed}/ListedActionsOption.tsx | 2 +- .../{ => Listed}/ListedMenuGroup.tsx | 0 .../{ => Listed}/ListedMenuItem.tsx | 0 .../NotesOptions/NoteAttributes.tsx | 45 ++++ .../NotesOptions/NoteSizeWarning.tsx | 23 ++ .../Components/NotesOptions/NotesOptions.tsx | 209 ++++-------------- .../NotesOptions/NotesOptionsPanel.tsx | 14 +- .../NotesOptions/NotesOptionsProps.ts | 1 + .../NotesOptions/SpellcheckOptions.tsx | 41 ++++ .../NotesOptions/Utils/calculateReadTime.tsx | 8 + .../Utils/countNoteAttributes.tsx | 23 ++ .../NotesOptions/Utils/getParagraphCount.tsx | 6 + .../NotesOptions/Utils/getWordCount.tsx | 6 + .../javascripts/Utils/copyTextToClipboard.tsx | 26 +++ .../Utils/spaceSeparatedStrings.tsx | 3 + 27 files changed, 449 insertions(+), 217 deletions(-) create mode 100644 packages/blocks-editor/src/Editor/MarkdownTransformers.ts create mode 100644 packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/MarkdownPreviewPlugin/MarkdownPreviewPlugin.tsx create mode 100644 packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperNoteMarkdownPreview.tsx create mode 100644 packages/web/src/javascripts/Components/NotesOptions/DeletePermanentlyButton.tsx rename packages/web/src/javascripts/Components/NotesOptions/{ => Listed}/ListedActionsMenu.tsx (100%) rename packages/web/src/javascripts/Components/NotesOptions/{ => Listed}/ListedActionsOption.tsx (97%) rename packages/web/src/javascripts/Components/NotesOptions/{ => Listed}/ListedMenuGroup.tsx (100%) rename packages/web/src/javascripts/Components/NotesOptions/{ => Listed}/ListedMenuItem.tsx (100%) create mode 100644 packages/web/src/javascripts/Components/NotesOptions/NoteAttributes.tsx create mode 100644 packages/web/src/javascripts/Components/NotesOptions/NoteSizeWarning.tsx create mode 100644 packages/web/src/javascripts/Components/NotesOptions/SpellcheckOptions.tsx create mode 100644 packages/web/src/javascripts/Components/NotesOptions/Utils/calculateReadTime.tsx create mode 100644 packages/web/src/javascripts/Components/NotesOptions/Utils/countNoteAttributes.tsx create mode 100644 packages/web/src/javascripts/Components/NotesOptions/Utils/getParagraphCount.tsx create mode 100644 packages/web/src/javascripts/Components/NotesOptions/Utils/getWordCount.tsx create mode 100644 packages/web/src/javascripts/Utils/copyTextToClipboard.tsx create mode 100644 packages/web/src/javascripts/Utils/spaceSeparatedStrings.tsx diff --git a/packages/blocks-editor/src/Editor/BlocksEditor.tsx b/packages/blocks-editor/src/Editor/BlocksEditor.tsx index ecd3eeeea2f..9c576a70775 100644 --- a/packages/blocks-editor/src/Editor/BlocksEditor.tsx +++ b/packages/blocks-editor/src/Editor/BlocksEditor.tsx @@ -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'; @@ -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 = ({ @@ -51,6 +47,7 @@ export const BlocksEditor: FunctionComponent = ({ spellcheck, ignoreFirstChange = false, lineHeight, + readonly, }) => { const [didIgnoreFirstChange, setDidIgnoreFirstChange] = useState(false); const handleChange = useCallback( @@ -69,11 +66,14 @@ export const BlocksEditor: FunctionComponent = ({ 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}`, @@ -116,14 +116,7 @@ export const BlocksEditor: FunctionComponent = ({ ErrorBoundary={LexicalErrorBoundary} /> - + @@ -138,7 +131,7 @@ export const BlocksEditor: FunctionComponent = ({ - {floatingAnchorElem && ( + {!readonly && floatingAnchorElem && ( <> diff --git a/packages/blocks-editor/src/Editor/MarkdownTransformers.ts b/packages/blocks-editor/src/Editor/MarkdownTransformers.ts new file mode 100644 index 00000000000..96c76c38343 --- /dev/null +++ b/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, +]; diff --git a/packages/blocks-editor/src/index.ts b/packages/blocks-editor/src/index.ts index c62acbea7eb..4a96a668cc4 100644 --- a/packages/blocks-editor/src/index.ts +++ b/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'; diff --git a/packages/ui-services/src/Keyboard/KeyboardCommands.ts b/packages/ui-services/src/Keyboard/KeyboardCommands.ts index 68e7ded0d8e..0813c7a2ff7 100644 --- a/packages/ui-services/src/Keyboard/KeyboardCommands.ts +++ b/packages/ui-services/src/Keyboard/KeyboardCommands.ts @@ -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') diff --git a/packages/ui-services/src/Keyboard/getKeyboardShortcuts.ts b/packages/ui-services/src/Keyboard/getKeyboardShortcuts.ts index cc69d5b8ab8..6fe9e4eaec8 100644 --- a/packages/ui-services/src/Keyboard/getKeyboardShortcuts.ts +++ b/packages/ui-services/src/Keyboard/getKeyboardShortcuts.ts @@ -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' @@ -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, + }, ] } diff --git a/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/MarkdownPreviewPlugin/MarkdownPreviewPlugin.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/MarkdownPreviewPlugin/MarkdownPreviewPlugin.tsx new file mode 100644 index 00000000000..c18a092c7d7 --- /dev/null +++ b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/MarkdownPreviewPlugin/MarkdownPreviewPlugin.tsx @@ -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 +} diff --git a/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperEditor.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperEditor.tsx index ba4aa35f30e..8d2547d481f 100644 --- a/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperEditor.tsx +++ b/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperEditor.tsx @@ -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 @@ -44,6 +47,20 @@ export const SuperEditor: FunctionComponent = ({ const note = useRef(controller.item) const changeEditorFunction = useRef() 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(PrefDefaults[PrefKey.EditorLineHeight]) @@ -110,37 +127,41 @@ export const SuperEditor: FunctionComponent = ({ return (
- - - - + + + - - - - - - - - (changeEditorFunction.current = callback)} - /> - - - - - - + + + + + + + + + (changeEditorFunction.current = callback)} + /> + + + + + + + + {showMarkdownPreview && } +
) diff --git a/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperNoteImporter.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperNoteImporter.tsx index ed56ec1dd14..fc2c89b5d30 100644 --- a/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperNoteImporter.tsx +++ b/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperNoteImporter.tsx @@ -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 @@ -103,6 +100,7 @@ export const SuperNoteImporter: FunctionComponent = ({ note, application, void +} + +export const SuperNoteMarkdownPreview: FunctionComponent = ({ 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 ( + + Markdown Preview + +
+ + + + + + + +
+
+ +
+ +
+ +
+ + + ) +} diff --git a/packages/web/src/javascripts/Components/NotesContextMenu/NotesContextMenu.tsx b/packages/web/src/javascripts/Components/NotesContextMenu/NotesContextMenu.tsx index 92e86926457..d95a307e4dd 100644 --- a/packages/web/src/javascripts/Components/NotesContextMenu/NotesContextMenu.tsx +++ b/packages/web/src/javascripts/Components/NotesContextMenu/NotesContextMenu.tsx @@ -1,6 +1,6 @@ import { observer } from 'mobx-react-lite' import NotesOptions from '@/Components/NotesOptions/NotesOptions' -import { useRef } from 'react' +import { useCallback, useRef, useState } from 'react' import { WebApplication } from '@/Application/Application' import { NotesController } from '@/Controllers/NotesController/NotesController' import { NavigationController } from '@/Controllers/Navigation/NavigationController' @@ -29,6 +29,11 @@ const NotesContextMenu = ({ const closeMenu = () => setContextMenuOpen(!contextMenuOpen) + const [disableClickOutside, setDisableClickOutside] = useState(false) + const handleDisableClickOutsideRequest = useCallback((disabled: boolean) => { + setDisableClickOutside(disabled) + }, []) + return (
diff --git a/packages/web/src/javascripts/Components/NotesOptions/DeletePermanentlyButton.tsx b/packages/web/src/javascripts/Components/NotesOptions/DeletePermanentlyButton.tsx new file mode 100644 index 00000000000..f2473050076 --- /dev/null +++ b/packages/web/src/javascripts/Components/NotesOptions/DeletePermanentlyButton.tsx @@ -0,0 +1,19 @@ +import Icon from '@/Components/Icon/Icon' +import { classNames } from '@standardnotes/utils' + +type DeletePermanentlyButtonProps = { + onClick: () => void +} + +export const DeletePermanentlyButton = ({ onClick }: DeletePermanentlyButtonProps) => ( + +) diff --git a/packages/web/src/javascripts/Components/NotesOptions/ListedActionsMenu.tsx b/packages/web/src/javascripts/Components/NotesOptions/Listed/ListedActionsMenu.tsx similarity index 100% rename from packages/web/src/javascripts/Components/NotesOptions/ListedActionsMenu.tsx rename to packages/web/src/javascripts/Components/NotesOptions/Listed/ListedActionsMenu.tsx diff --git a/packages/web/src/javascripts/Components/NotesOptions/ListedActionsOption.tsx b/packages/web/src/javascripts/Components/NotesOptions/Listed/ListedActionsOption.tsx similarity index 97% rename from packages/web/src/javascripts/Components/NotesOptions/ListedActionsOption.tsx rename to packages/web/src/javascripts/Components/NotesOptions/Listed/ListedActionsOption.tsx index f818cb0fe43..be630d4a5c7 100644 --- a/packages/web/src/javascripts/Components/NotesOptions/ListedActionsOption.tsx +++ b/packages/web/src/javascripts/Components/NotesOptions/Listed/ListedActionsOption.tsx @@ -4,7 +4,7 @@ import { FunctionComponent, useCallback, useRef, useState } from 'react' import Icon from '@/Components/Icon/Icon' import ListedActionsMenu from './ListedActionsMenu' import { KeyboardKey } from '@standardnotes/ui-services' -import Popover from '../Popover/Popover' +import Popover from '../../Popover/Popover' type Props = { application: WebApplication diff --git a/packages/web/src/javascripts/Components/NotesOptions/ListedMenuGroup.tsx b/packages/web/src/javascripts/Components/NotesOptions/Listed/ListedMenuGroup.tsx similarity index 100% rename from packages/web/src/javascripts/Components/NotesOptions/ListedMenuGroup.tsx rename to packages/web/src/javascripts/Components/NotesOptions/Listed/ListedMenuGroup.tsx diff --git a/packages/web/src/javascripts/Components/NotesOptions/ListedMenuItem.tsx b/packages/web/src/javascripts/Components/NotesOptions/Listed/ListedMenuItem.tsx similarity index 100% rename from packages/web/src/javascripts/Components/NotesOptions/ListedMenuItem.tsx rename to packages/web/src/javascripts/Components/NotesOptions/Listed/ListedMenuItem.tsx diff --git a/packages/web/src/javascripts/Components/NotesOptions/NoteAttributes.tsx b/packages/web/src/javascripts/Components/NotesOptions/NoteAttributes.tsx new file mode 100644 index 00000000000..3d8bbb48a28 --- /dev/null +++ b/packages/web/src/javascripts/Components/NotesOptions/NoteAttributes.tsx @@ -0,0 +1,45 @@ +import { useMemo, FunctionComponent } from 'react' +import { SNApplication, SNNote } from '@standardnotes/snjs' +import { formatDateForContextMenu } from '@/Utils/DateUtils' +import { calculateReadTime } from './Utils/calculateReadTime' +import { countNoteAttributes } from './Utils/countNoteAttributes' + +export const NoteAttributes: FunctionComponent<{ + application: SNApplication + note: SNNote +}> = ({ application, note }) => { + const { words, characters, paragraphs } = useMemo(() => countNoteAttributes(note.text), [note.text]) + + const readTime = useMemo(() => (typeof words === 'number' ? calculateReadTime(words) : 'N/A'), [words]) + + const dateLastModified = useMemo(() => formatDateForContextMenu(note.userModifiedDate), [note.userModifiedDate]) + + const dateCreated = useMemo(() => formatDateForContextMenu(note.created_at), [note.created_at]) + + const editor = application.componentManager.editorForNote(note) + const format = editor?.package_info?.file_type || 'txt' + + return ( +
+ {typeof words === 'number' && (format === 'txt' || format === 'md') ? ( + <> +
+ {words} words · {characters} characters · {paragraphs} paragraphs +
+
+ Read time: {readTime} +
+ + ) : null} +
+ Last modified: {dateLastModified} +
+
+ Created: {dateCreated} +
+
+ Note ID: {note.uuid} +
+
+ ) +} diff --git a/packages/web/src/javascripts/Components/NotesOptions/NoteSizeWarning.tsx b/packages/web/src/javascripts/Components/NotesOptions/NoteSizeWarning.tsx new file mode 100644 index 00000000000..ba7ed46c611 --- /dev/null +++ b/packages/web/src/javascripts/Components/NotesOptions/NoteSizeWarning.tsx @@ -0,0 +1,23 @@ +import Icon from '@/Components/Icon/Icon' +import { FunctionComponent } from 'react' +import { SNNote } from '@standardnotes/snjs' +import { BYTES_IN_ONE_MEGABYTE } from '@/Constants/Constants' +import HorizontalSeparator from '../Shared/HorizontalSeparator' + +export const NOTE_SIZE_WARNING_THRESHOLD = 0.5 * BYTES_IN_ONE_MEGABYTE + +export const NoteSizeWarning: FunctionComponent<{ + note: SNNote +}> = ({ note }) => { + return new Blob([note.text]).size > NOTE_SIZE_WARNING_THRESHOLD ? ( + <> + +
+ +
+ This note may have trouble syncing to the mobile application due to its size. +
+
+ + ) : null +} diff --git a/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx b/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx index 7303dd36d11..fb5d4c87d12 100644 --- a/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx +++ b/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx @@ -1,23 +1,21 @@ import Icon from '@/Components/Icon/Icon' import Switch from '@/Components/Switch/Switch' import { observer } from 'mobx-react-lite' -import { useState, useEffect, useMemo, useCallback, FunctionComponent } from 'react' -import { Platform, SNApplication, SNComponent, SNNote } from '@standardnotes/snjs' +import { useState, useEffect, useMemo, useCallback } from 'react' +import { NoteType, Platform, SNNote } from '@standardnotes/snjs' import { OPEN_NOTE_HISTORY_COMMAND, PIN_NOTE_COMMAND, SHOW_HIDDEN_OPTIONS_KEYBOARD_COMMAND, STAR_NOTE_COMMAND, + SUPER_SHOW_MARKDOWN_PREVIEW, } from '@standardnotes/ui-services' import ChangeEditorOption from './ChangeEditorOption' -import { BYTES_IN_ONE_MEGABYTE } from '@/Constants/Constants' -import ListedActionsOption from './ListedActionsOption' +import ListedActionsOption from './Listed/ListedActionsOption' import AddTagOption from './AddTagOption' import { addToast, dismissToast, ToastType } from '@standardnotes/toast' import { NotesOptionsProps } from './NotesOptionsProps' -import { NotesController } from '@/Controllers/NotesController/NotesController' import HorizontalSeparator from '../Shared/HorizontalSeparator' -import { formatDateForContextMenu } from '@/Utils/DateUtils' import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider' import { AppPaneId } from '../ResponsivePane/AppPaneMetadata' import { getNoteBlob, getNoteFileName } from '@/Utils/NoteExportUtils' @@ -27,176 +25,36 @@ import ProtectedUnauthorizedLabel from '../ProtectedItemOverlay/ProtectedUnautho import { classNames } from '@standardnotes/utils' import { MenuItemIconSize } from '@/Constants/TailwindClassNames' import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator' - -type DeletePermanentlyButtonProps = { - onClick: () => void -} - -const DeletePermanentlyButton = ({ onClick }: DeletePermanentlyButtonProps) => ( - -) +import { NoteAttributes } from './NoteAttributes' +import { SpellcheckOptions } from './SpellcheckOptions' +import { NoteSizeWarning } from './NoteSizeWarning' +import { DeletePermanentlyButton } from './DeletePermanentlyButton' +import { SuperNoteMarkdownPreview } from '../NoteView/SuperEditor/SuperNoteMarkdownPreview' +import { useCommandService } from '../ApplicationView/CommandProvider' const iconSize = MenuItemIconSize -const iconClass = `text-neutral mr-2 ${iconSize}` +export const iconClass = `text-neutral mr-2 ${iconSize}` const iconClassDanger = `text-danger mr-2 ${iconSize}` const iconClassWarning = `text-warning mr-2 ${iconSize}` const iconClassSuccess = `text-success mr-2 ${iconSize}` -const getWordCount = (text: string) => { - if (text.trim().length === 0) { - return 0 - } - return text.split(/\s+/).length -} - -const getParagraphCount = (text: string) => { - if (text.trim().length === 0) { - return 0 - } - return text.replace(/\n$/gm, '').split(/\n/).length -} - -const countNoteAttributes = (text: string) => { - try { - JSON.parse(text) - return { - characters: 'N/A', - words: 'N/A', - paragraphs: 'N/A', - } - } catch { - const characters = text.length - const words = getWordCount(text) - const paragraphs = getParagraphCount(text) - - return { - characters, - words, - paragraphs, - } - } -} - -const calculateReadTime = (words: number) => { - const timeToRead = Math.round(words / 200) - if (timeToRead === 0) { - return '< 1 minute' - } else { - return `${timeToRead} ${timeToRead > 1 ? 'minutes' : 'minute'}` - } -} - -const NoteAttributes: FunctionComponent<{ - application: SNApplication - note: SNNote -}> = ({ application, note }) => { - const { words, characters, paragraphs } = useMemo(() => countNoteAttributes(note.text), [note.text]) - - const readTime = useMemo(() => (typeof words === 'number' ? calculateReadTime(words) : 'N/A'), [words]) - - const dateLastModified = useMemo(() => formatDateForContextMenu(note.userModifiedDate), [note.userModifiedDate]) - - const dateCreated = useMemo(() => formatDateForContextMenu(note.created_at), [note.created_at]) - - const editor = application.componentManager.editorForNote(note) - const format = editor?.package_info?.file_type || 'txt' - - return ( -
- {typeof words === 'number' && (format === 'txt' || format === 'md') ? ( - <> -
- {words} words · {characters} characters · {paragraphs} paragraphs -
-
- Read time: {readTime} -
- - ) : null} -
- Last modified: {dateLastModified} -
-
- Created: {dateCreated} -
-
- Note ID: {note.uuid} -
-
- ) -} - -const SpellcheckOptions: FunctionComponent<{ - editorForNote: SNComponent | undefined - notesController: NotesController - note: SNNote - className: string -}> = ({ editorForNote, notesController, note, className }) => { - const spellcheckControllable = Boolean(!editorForNote || editorForNote.package_info.spellcheckControl) - const noteSpellcheck = !spellcheckControllable - ? true - : note - ? notesController.getSpellcheckStateForNote(note) - : undefined - - return ( -
- - {!spellcheckControllable && ( -

Spellcheck cannot be controlled for this editor.

- )} -
- ) -} - -const NOTE_SIZE_WARNING_THRESHOLD = 0.5 * BYTES_IN_ONE_MEGABYTE - -const NoteSizeWarning: FunctionComponent<{ - note: SNNote -}> = ({ note }) => { - return new Blob([note.text]).size > NOTE_SIZE_WARNING_THRESHOLD ? ( - <> - -
- -
- This note may have trouble syncing to the mobile application due to its size. -
-
- - ) : null -} - const NotesOptions = ({ application, navigationController, notesController, historyModalController, + requestDisableClickOutside, closeMenu, }: NotesOptionsProps) => { const [altKeyDown, setAltKeyDown] = useState(false) const { toggleAppPane } = useResponsiveAppPane() + const [showMarkdownPreview, setShowMarkdownPreview] = useState(false) + const commandService = useCommandService() + + const markdownShortcut = useMemo( + () => commandService.keyboardShortcutForCommand(SUPER_SHOW_MARKDOWN_PREVIEW), + [commandService], + ) const toggleOn = (condition: (note: SNNote) => boolean) => { const notesMatchingAttribute = notes.filter(condition) @@ -295,6 +153,16 @@ const NotesOptions = ({ [application], ) + const enableSuperMarkdownPreview = useCallback(() => { + setShowMarkdownPreview(true) + requestDisableClickOutside?.(true) + }, [requestDisableClickOutside]) + + const closeMarkdownPreview = useCallback(() => { + setShowMarkdownPreview(false) + requestDisableClickOutside?.(false) + }, [requestDisableClickOutside]) + const unauthorized = notes.some((note) => !application.isAuthorizedToRenderItem(note)) if (unauthorized) { return @@ -532,6 +400,25 @@ const NotesOptions = ({ {notes.length === 1 ? ( <> + {notes[0].noteType === NoteType.Super && ( + <> + + +
Super
+ + + + {showMarkdownPreview && } + + )} { + setDisableClickOutside(disabled) + }, []) + return ( <> - + diff --git a/packages/web/src/javascripts/Components/NotesOptions/NotesOptionsProps.ts b/packages/web/src/javascripts/Components/NotesOptions/NotesOptionsProps.ts index b7e37b77c46..e6b45943a5f 100644 --- a/packages/web/src/javascripts/Components/NotesOptions/NotesOptionsProps.ts +++ b/packages/web/src/javascripts/Components/NotesOptions/NotesOptionsProps.ts @@ -10,5 +10,6 @@ export type NotesOptionsProps = { notesController: NotesController linkingController: LinkingController historyModalController: HistoryModalController + requestDisableClickOutside?: (disabled: boolean) => void closeMenu: () => void } diff --git a/packages/web/src/javascripts/Components/NotesOptions/SpellcheckOptions.tsx b/packages/web/src/javascripts/Components/NotesOptions/SpellcheckOptions.tsx new file mode 100644 index 00000000000..4132b35d795 --- /dev/null +++ b/packages/web/src/javascripts/Components/NotesOptions/SpellcheckOptions.tsx @@ -0,0 +1,41 @@ +import Icon from '@/Components/Icon/Icon' +import Switch from '@/Components/Switch/Switch' +import { FunctionComponent } from 'react' +import { SNComponent, SNNote } from '@standardnotes/snjs' +import { NotesController } from '@/Controllers/NotesController/NotesController' +import { iconClass } from './NotesOptions' + +export const SpellcheckOptions: FunctionComponent<{ + editorForNote: SNComponent | undefined + notesController: NotesController + note: SNNote + className: string +}> = ({ editorForNote, notesController, note, className }) => { + const spellcheckControllable = Boolean(!editorForNote || editorForNote.package_info.spellcheckControl) + const noteSpellcheck = !spellcheckControllable + ? true + : note + ? notesController.getSpellcheckStateForNote(note) + : undefined + + return ( +
+ + {!spellcheckControllable && ( +

Spellcheck cannot be controlled for this editor.

+ )} +
+ ) +} diff --git a/packages/web/src/javascripts/Components/NotesOptions/Utils/calculateReadTime.tsx b/packages/web/src/javascripts/Components/NotesOptions/Utils/calculateReadTime.tsx new file mode 100644 index 00000000000..d8d5aada142 --- /dev/null +++ b/packages/web/src/javascripts/Components/NotesOptions/Utils/calculateReadTime.tsx @@ -0,0 +1,8 @@ +export const calculateReadTime = (words: number) => { + const timeToRead = Math.round(words / 200) + if (timeToRead === 0) { + return '< 1 minute' + } else { + return `${timeToRead} ${timeToRead > 1 ? 'minutes' : 'minute'}` + } +} diff --git a/packages/web/src/javascripts/Components/NotesOptions/Utils/countNoteAttributes.tsx b/packages/web/src/javascripts/Components/NotesOptions/Utils/countNoteAttributes.tsx new file mode 100644 index 00000000000..9406a2e3bb2 --- /dev/null +++ b/packages/web/src/javascripts/Components/NotesOptions/Utils/countNoteAttributes.tsx @@ -0,0 +1,23 @@ +import { getWordCount } from './getWordCount' +import { getParagraphCount } from './getParagraphCount' + +export const countNoteAttributes = (text: string) => { + try { + JSON.parse(text) + return { + characters: 'N/A', + words: 'N/A', + paragraphs: 'N/A', + } + } catch { + const characters = text.length + const words = getWordCount(text) + const paragraphs = getParagraphCount(text) + + return { + characters, + words, + paragraphs, + } + } +} diff --git a/packages/web/src/javascripts/Components/NotesOptions/Utils/getParagraphCount.tsx b/packages/web/src/javascripts/Components/NotesOptions/Utils/getParagraphCount.tsx new file mode 100644 index 00000000000..79181675a9d --- /dev/null +++ b/packages/web/src/javascripts/Components/NotesOptions/Utils/getParagraphCount.tsx @@ -0,0 +1,6 @@ +export const getParagraphCount = (text: string) => { + if (text.trim().length === 0) { + return 0 + } + return text.replace(/\n$/gm, '').split(/\n/).length +} diff --git a/packages/web/src/javascripts/Components/NotesOptions/Utils/getWordCount.tsx b/packages/web/src/javascripts/Components/NotesOptions/Utils/getWordCount.tsx new file mode 100644 index 00000000000..141f23d3046 --- /dev/null +++ b/packages/web/src/javascripts/Components/NotesOptions/Utils/getWordCount.tsx @@ -0,0 +1,6 @@ +export const getWordCount = (text: string) => { + if (text.trim().length === 0) { + return 0 + } + return text.split(/\s+/).length +} diff --git a/packages/web/src/javascripts/Utils/copyTextToClipboard.tsx b/packages/web/src/javascripts/Utils/copyTextToClipboard.tsx new file mode 100644 index 00000000000..0f5fb4c33de --- /dev/null +++ b/packages/web/src/javascripts/Utils/copyTextToClipboard.tsx @@ -0,0 +1,26 @@ +export function fallbackCopyTextToClipboard(text: string) { + const textArea = document.createElement('textarea') + textArea.value = text + textArea.style.top = '0' + textArea.style.left = '0' + textArea.style.position = 'fixed' + document.body.appendChild(textArea) + textArea.focus() + textArea.select() + try { + document.execCommand('copy') + } catch (err) { + console.error('Unable to copy', err) + } + + document.body.removeChild(textArea) +} + +export function copyTextToClipboard(text: string) { + if (!navigator.clipboard) { + fallbackCopyTextToClipboard(text) + return + } + + void navigator.clipboard.writeText(text) +} diff --git a/packages/web/src/javascripts/Utils/spaceSeparatedStrings.tsx b/packages/web/src/javascripts/Utils/spaceSeparatedStrings.tsx new file mode 100644 index 00000000000..66b7a77e6cb --- /dev/null +++ b/packages/web/src/javascripts/Utils/spaceSeparatedStrings.tsx @@ -0,0 +1,3 @@ +export function spaceSeparatedStrings(...strings: string[]): string { + return strings.join(' ') +} From 17b2e92c95d4467f0e07f6f416d1665ace4cbee7 Mon Sep 17 00:00:00 2001 From: Mo Date: Wed, 23 Nov 2022 10:59:42 -0600 Subject: [PATCH 2/2] refactor: trigger command --- .../ui-services/src/Keyboard/KeyboardService.ts | 16 ++++++++++++++++ .../Components/NotesOptions/NotesOptions.tsx | 15 ++------------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/packages/ui-services/src/Keyboard/KeyboardService.ts b/packages/ui-services/src/Keyboard/KeyboardService.ts index 3ca5c36f4b1..379b6c41189 100644 --- a/packages/ui-services/src/Keyboard/KeyboardService.ts +++ b/packages/ui-services/src/Keyboard/KeyboardService.ts @@ -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) } diff --git a/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx b/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx index fb5d4c87d12..cc06a1ce8e0 100644 --- a/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx +++ b/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx @@ -29,7 +29,6 @@ import { NoteAttributes } from './NoteAttributes' import { SpellcheckOptions } from './SpellcheckOptions' import { NoteSizeWarning } from './NoteSizeWarning' import { DeletePermanentlyButton } from './DeletePermanentlyButton' -import { SuperNoteMarkdownPreview } from '../NoteView/SuperEditor/SuperNoteMarkdownPreview' import { useCommandService } from '../ApplicationView/CommandProvider' const iconSize = MenuItemIconSize @@ -43,12 +42,10 @@ const NotesOptions = ({ navigationController, notesController, historyModalController, - requestDisableClickOutside, closeMenu, }: NotesOptionsProps) => { const [altKeyDown, setAltKeyDown] = useState(false) const { toggleAppPane } = useResponsiveAppPane() - const [showMarkdownPreview, setShowMarkdownPreview] = useState(false) const commandService = useCommandService() const markdownShortcut = useMemo( @@ -154,14 +151,8 @@ const NotesOptions = ({ ) const enableSuperMarkdownPreview = useCallback(() => { - setShowMarkdownPreview(true) - requestDisableClickOutside?.(true) - }, [requestDisableClickOutside]) - - const closeMarkdownPreview = useCallback(() => { - setShowMarkdownPreview(false) - requestDisableClickOutside?.(false) - }, [requestDisableClickOutside]) + commandService.triggerCommand(SUPER_SHOW_MARKDOWN_PREVIEW) + }, [commandService]) const unauthorized = notes.some((note) => !application.isAuthorizedToRenderItem(note)) if (unauthorized) { @@ -415,8 +406,6 @@ const NotesOptions = ({ {markdownShortcut && } - - {showMarkdownPreview && } )}