diff --git a/packages/ui-services/src/Keyboard/KeyboardCommandHandler.ts b/packages/ui-services/src/Keyboard/KeyboardCommandHandler.ts index a865eeac5..d40a541ff 100644 --- a/packages/ui-services/src/Keyboard/KeyboardCommandHandler.ts +++ b/packages/ui-services/src/Keyboard/KeyboardCommandHandler.ts @@ -2,8 +2,8 @@ import { KeyboardCommand } from './KeyboardCommands' export type KeyboardCommandHandler = { command: KeyboardCommand - onKeyDown?: (event: KeyboardEvent) => boolean | void - onKeyUp?: (event: KeyboardEvent) => boolean | void + onKeyDown?: (event: KeyboardEvent, data?: unknown) => boolean | void + onKeyUp?: (event: KeyboardEvent, data?: unknown) => boolean | void element?: HTMLElement elements?: HTMLElement[] notElement?: HTMLElement diff --git a/packages/ui-services/src/Keyboard/KeyboardCommands.ts b/packages/ui-services/src/Keyboard/KeyboardCommands.ts index 0813c7a2f..0982efe90 100644 --- a/packages/ui-services/src/Keyboard/KeyboardCommands.ts +++ b/packages/ui-services/src/Keyboard/KeyboardCommands.ts @@ -25,3 +25,6 @@ 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') +export const SUPER_EXPORT_JSON = createKeyboardCommand('SUPER_EXPORT_JSON') +export const SUPER_EXPORT_MARKDOWN = createKeyboardCommand('SUPER_EXPORT_MARKDOWN') +export const SUPER_EXPORT_HTML = createKeyboardCommand('SUPER_EXPORT_HTML') diff --git a/packages/ui-services/src/Keyboard/KeyboardService.ts b/packages/ui-services/src/Keyboard/KeyboardService.ts index 379b6c411..108deb109 100644 --- a/packages/ui-services/src/Keyboard/KeyboardService.ts +++ b/packages/ui-services/src/Keyboard/KeyboardService.ts @@ -165,7 +165,7 @@ export class KeyboardService { } } - public triggerCommand(command: KeyboardCommand): void { + public triggerCommand(command: KeyboardCommand, data?: unknown): void { for (const observer of this.commandHandlers) { if (observer.command !== command) { continue @@ -173,7 +173,7 @@ export class KeyboardService { const callback = observer.onKeyDown || observer.onKeyUp if (callback) { - const exclusive = callback(new KeyboardEvent('command-trigger')) + const exclusive = callback(new KeyboardEvent('command-trigger'), data) if (exclusive) { return } diff --git a/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ExportPlugin/ExportPlugin.ts b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ExportPlugin/ExportPlugin.ts new file mode 100644 index 000000000..5853147a8 --- /dev/null +++ b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ExportPlugin/ExportPlugin.ts @@ -0,0 +1,113 @@ +import { useApplication } from '@/Components/ApplicationView/ApplicationProvider' +import { downloadBlobOnAndroid } from '@/NativeMobileWeb/DownloadBlobOnAndroid' +import { shareBlobOnMobile } from '@/NativeMobileWeb/ShareBlobOnMobile' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { Platform } from '@standardnotes/snjs' +import { + sanitizeFileName, + SUPER_EXPORT_HTML, + SUPER_EXPORT_JSON, + SUPER_EXPORT_MARKDOWN, +} from '@standardnotes/ui-services' +import { useCallback, useEffect } from 'react' +import { $convertToMarkdownString } from '@lexical/markdown' +import { MarkdownTransformers } from '@standardnotes/blocks-editor' +import { $generateHtmlFromNodes } from '@lexical/html' +import { useCommandService } from '@/Components/ApplicationView/CommandProvider' + +export const ExportPlugin = () => { + const application = useApplication() + const [editor] = useLexicalComposerContext() + const commandService = useCommandService() + + const downloadData = useCallback( + (data: Blob, fileName: string) => { + if (!application.isNativeMobileWeb()) { + application.getArchiveService().downloadData(data, fileName) + return + } + + if (application.platform === Platform.Android) { + downloadBlobOnAndroid(application, data, fileName).catch(console.error) + } else { + shareBlobOnMobile(application, data, fileName).catch(console.error) + } + }, + [application], + ) + + const exportJson = useCallback( + (title: string) => { + const content = JSON.stringify(editor.toJSON()) + const blob = new Blob([content], { type: 'application/json' }) + downloadData(blob, `${sanitizeFileName(title)}.json`) + }, + [downloadData, editor], + ) + + const exportMarkdown = useCallback( + (title: string) => { + editor.getEditorState().read(() => { + const content = $convertToMarkdownString(MarkdownTransformers) + const blob = new Blob([content], { type: 'text/markdown' }) + downloadData(blob, `${sanitizeFileName(title)}.md`) + }) + }, + [downloadData, editor], + ) + + const exportHtml = useCallback( + (title: string) => { + editor.getEditorState().read(() => { + const content = $generateHtmlFromNodes(editor) + const blob = new Blob([content], { type: 'text/html' }) + downloadData(blob, `${sanitizeFileName(title)}.html`) + }) + }, + [downloadData, editor], + ) + + useEffect(() => { + return commandService.addCommandHandler({ + command: SUPER_EXPORT_JSON, + onKeyDown: (_, data) => { + if (!data) { + throw new Error('No data provided for export command') + } + + const title = data as string + exportJson(title) + }, + }) + }, [commandService, exportJson]) + + useEffect(() => { + return commandService.addCommandHandler({ + command: SUPER_EXPORT_MARKDOWN, + onKeyDown: (_, data) => { + if (!data) { + throw new Error('No data provided for export command') + } + + const title = data as string + exportMarkdown(title) + }, + }) + }, [commandService, exportMarkdown]) + + useEffect(() => { + return commandService.addCommandHandler({ + command: SUPER_EXPORT_HTML, + onKeyDown: (_, data) => { + if (!data) { + throw new Error('No data provided for export command') + } + + const title = data as string + exportHtml(title) + }, + }) + }, [commandService, exportHtml]) + + 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 8d2547d48..c86eb08f5 100644 --- a/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperEditor.tsx +++ b/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperEditor.tsx @@ -26,6 +26,7 @@ import { PrefDefaults } from '@/Constants/PrefDefaults' import { useCommandService } from '@/Components/ApplicationView/CommandProvider' import { SUPER_SHOW_MARKDOWN_PREVIEW } from '@standardnotes/ui-services' import { SuperNoteMarkdownPreview } from './SuperNoteMarkdownPreview' +import { ExportPlugin } from './Plugins/ExportPlugin/ExportPlugin' const NotePreviewCharLimit = 160 @@ -155,6 +156,7 @@ export const SuperEditor: FunctionComponent = ({ /> + diff --git a/packages/web/src/javascripts/Components/NotesOptions/ClassNames.ts b/packages/web/src/javascripts/Components/NotesOptions/ClassNames.ts new file mode 100644 index 000000000..849cb713d --- /dev/null +++ b/packages/web/src/javascripts/Components/NotesOptions/ClassNames.ts @@ -0,0 +1,13 @@ +import { MenuItemIconSize } from '@/Constants/TailwindClassNames' +import { classNames } from '@standardnotes/utils' + +export const menuItemTextClassNames = 'text-mobile-menu-item md:text-tablet-menu-item lg:text-menu-item' + +export const menuItemClassNames = classNames( + menuItemTextClassNames, + 'flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none', +) + +export const menuItemSwitchClassNames = classNames(menuItemTextClassNames, menuItemClassNames, 'justify-between') + +export const iconClass = `text-neutral mr-2 ${MenuItemIconSize}` diff --git a/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx b/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx index cc06a1ce8..fc8e8a948 100644 --- a/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx +++ b/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx @@ -30,9 +30,10 @@ import { SpellcheckOptions } from './SpellcheckOptions' import { NoteSizeWarning } from './NoteSizeWarning' import { DeletePermanentlyButton } from './DeletePermanentlyButton' import { useCommandService } from '../ApplicationView/CommandProvider' +import { menuItemClassNames, menuItemSwitchClassNames, iconClass } from './ClassNames' +import SuperNoteOptions from './SuperNoteOptions' const iconSize = MenuItemIconSize -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}` @@ -159,22 +160,13 @@ const NotesOptions = ({ return } - const textClassNames = 'text-mobile-menu-item md:text-tablet-menu-item lg:text-menu-item' - - const defaultClassNames = classNames( - textClassNames, - 'flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none', - ) - - const switchClassNames = classNames(textClassNames, defaultClassNames, 'justify-between') - const firstItemClass = 'pt-4' return ( <> {notes.length === 1 && ( <> - )} - - {application.platform === Platform.Android && ( - + {notes[0].noteType !== NoteType.Super && ( + <> + + {application.platform === Platform.Android && ( + + )} + )} - {unarchived && ( - + )} @@ -420,7 +408,7 @@ const NotesOptions = ({ void +} + +const SuperNoteOptions = ({ note, markdownShortcut, enableSuperMarkdownPreview }: Props) => { + const commandService = useCommandService() + + const exportButtonRef = useRef(null) + const [isExportMenuOpen, setIsExportMenuOpen] = useState(false) + + return ( + <> + + +
Super
+ + + + + { + setIsExportMenuOpen(!isExportMenuOpen) + }} + className="py-1" + > + + commandService.triggerCommand(SUPER_EXPORT_JSON, note.title)}> + + Export as JSON + + commandService.triggerCommand(SUPER_EXPORT_MARKDOWN, note.title)}> + + Export as Markdown + + commandService.triggerCommand(SUPER_EXPORT_HTML, note.title)}> + + Export as HTML + + + + + ) +} + +export default SuperNoteOptions