feat: Markdown, HTML & JSON export options for super notes (#2054)
This commit is contained in:
@@ -2,8 +2,8 @@ import { KeyboardCommand } from './KeyboardCommands'
|
|||||||
|
|
||||||
export type KeyboardCommandHandler = {
|
export type KeyboardCommandHandler = {
|
||||||
command: KeyboardCommand
|
command: KeyboardCommand
|
||||||
onKeyDown?: (event: KeyboardEvent) => boolean | void
|
onKeyDown?: (event: KeyboardEvent, data?: unknown) => boolean | void
|
||||||
onKeyUp?: (event: KeyboardEvent) => boolean | void
|
onKeyUp?: (event: KeyboardEvent, data?: unknown) => boolean | void
|
||||||
element?: HTMLElement
|
element?: HTMLElement
|
||||||
elements?: HTMLElement[]
|
elements?: HTMLElement[]
|
||||||
notElement?: HTMLElement
|
notElement?: HTMLElement
|
||||||
|
|||||||
@@ -25,3 +25,6 @@ export const CAPTURE_SAVE_COMMAND = createKeyboardCommand('CAPTURE_SAVE_COMMAND'
|
|||||||
export const STAR_NOTE_COMMAND = createKeyboardCommand('STAR_NOTE_COMMAND')
|
export const STAR_NOTE_COMMAND = createKeyboardCommand('STAR_NOTE_COMMAND')
|
||||||
export const PIN_NOTE_COMMAND = createKeyboardCommand('PIN_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_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')
|
||||||
|
|||||||
@@ -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) {
|
for (const observer of this.commandHandlers) {
|
||||||
if (observer.command !== command) {
|
if (observer.command !== command) {
|
||||||
continue
|
continue
|
||||||
@@ -173,7 +173,7 @@ export class KeyboardService {
|
|||||||
|
|
||||||
const callback = observer.onKeyDown || observer.onKeyUp
|
const callback = observer.onKeyDown || observer.onKeyUp
|
||||||
if (callback) {
|
if (callback) {
|
||||||
const exclusive = callback(new KeyboardEvent('command-trigger'))
|
const exclusive = callback(new KeyboardEvent('command-trigger'), data)
|
||||||
if (exclusive) {
|
if (exclusive) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ import { PrefDefaults } from '@/Constants/PrefDefaults'
|
|||||||
import { useCommandService } from '@/Components/ApplicationView/CommandProvider'
|
import { useCommandService } from '@/Components/ApplicationView/CommandProvider'
|
||||||
import { SUPER_SHOW_MARKDOWN_PREVIEW } from '@standardnotes/ui-services'
|
import { SUPER_SHOW_MARKDOWN_PREVIEW } from '@standardnotes/ui-services'
|
||||||
import { SuperNoteMarkdownPreview } from './SuperNoteMarkdownPreview'
|
import { SuperNoteMarkdownPreview } from './SuperNoteMarkdownPreview'
|
||||||
|
import { ExportPlugin } from './Plugins/ExportPlugin/ExportPlugin'
|
||||||
|
|
||||||
const NotePreviewCharLimit = 160
|
const NotePreviewCharLimit = 160
|
||||||
|
|
||||||
@@ -155,6 +156,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
|
|||||||
/>
|
/>
|
||||||
<NodeObserverPlugin nodeType={BubbleNode} onRemove={handleBubbleRemove} />
|
<NodeObserverPlugin nodeType={BubbleNode} onRemove={handleBubbleRemove} />
|
||||||
<NodeObserverPlugin nodeType={FileNode} onRemove={handleBubbleRemove} />
|
<NodeObserverPlugin nodeType={FileNode} onRemove={handleBubbleRemove} />
|
||||||
|
<ExportPlugin />
|
||||||
</BlocksEditor>
|
</BlocksEditor>
|
||||||
</BlocksEditorComposer>
|
</BlocksEditorComposer>
|
||||||
</FilesControllerProvider>
|
</FilesControllerProvider>
|
||||||
|
|||||||
@@ -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}`
|
||||||
@@ -30,9 +30,10 @@ import { SpellcheckOptions } from './SpellcheckOptions'
|
|||||||
import { NoteSizeWarning } from './NoteSizeWarning'
|
import { NoteSizeWarning } from './NoteSizeWarning'
|
||||||
import { DeletePermanentlyButton } from './DeletePermanentlyButton'
|
import { DeletePermanentlyButton } from './DeletePermanentlyButton'
|
||||||
import { useCommandService } from '../ApplicationView/CommandProvider'
|
import { useCommandService } from '../ApplicationView/CommandProvider'
|
||||||
|
import { menuItemClassNames, menuItemSwitchClassNames, iconClass } from './ClassNames'
|
||||||
|
import SuperNoteOptions from './SuperNoteOptions'
|
||||||
|
|
||||||
const iconSize = MenuItemIconSize
|
const iconSize = MenuItemIconSize
|
||||||
export const iconClass = `text-neutral mr-2 ${iconSize}`
|
|
||||||
const iconClassDanger = `text-danger mr-2 ${iconSize}`
|
const iconClassDanger = `text-danger mr-2 ${iconSize}`
|
||||||
const iconClassWarning = `text-warning mr-2 ${iconSize}`
|
const iconClassWarning = `text-warning mr-2 ${iconSize}`
|
||||||
const iconClassSuccess = `text-success mr-2 ${iconSize}`
|
const iconClassSuccess = `text-success mr-2 ${iconSize}`
|
||||||
@@ -159,22 +160,13 @@ const NotesOptions = ({
|
|||||||
return <ProtectedUnauthorizedLabel />
|
return <ProtectedUnauthorizedLabel />
|
||||||
}
|
}
|
||||||
|
|
||||||
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'
|
const firstItemClass = 'pt-4'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{notes.length === 1 && (
|
{notes.length === 1 && (
|
||||||
<>
|
<>
|
||||||
<button className={classNames(defaultClassNames, firstItemClass)} onClick={openRevisionHistoryModal}>
|
<button className={classNames(menuItemClassNames, firstItemClass)} onClick={openRevisionHistoryModal}>
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<span className="flex">
|
<span className="flex">
|
||||||
<Icon type="history" className={iconClass} />
|
<Icon type="history" className={iconClass} />
|
||||||
@@ -187,7 +179,7 @@ const NotesOptions = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
className={switchClassNames}
|
className={menuItemSwitchClassNames}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
notesController.setLockSelectedNotes(!locked)
|
notesController.setLockSelectedNotes(!locked)
|
||||||
}}
|
}}
|
||||||
@@ -199,7 +191,7 @@ const NotesOptions = ({
|
|||||||
<Switch className="px-0" checked={locked} />
|
<Switch className="px-0" checked={locked} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={switchClassNames}
|
className={menuItemSwitchClassNames}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
notesController.setHideSelectedNotePreviews(!hidePreviews)
|
notesController.setHideSelectedNotePreviews(!hidePreviews)
|
||||||
}}
|
}}
|
||||||
@@ -211,7 +203,7 @@ const NotesOptions = ({
|
|||||||
<Switch className="px-0" checked={!hidePreviews} />
|
<Switch className="px-0" checked={!hidePreviews} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={switchClassNames}
|
className={menuItemSwitchClassNames}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
notesController.setProtectSelectedNotes(!protect).catch(console.error)
|
notesController.setProtectSelectedNotes(!protect).catch(console.error)
|
||||||
}}
|
}}
|
||||||
@@ -227,7 +219,7 @@ const NotesOptions = ({
|
|||||||
<HorizontalSeparator classes="my-2" />
|
<HorizontalSeparator classes="my-2" />
|
||||||
<ChangeEditorOption
|
<ChangeEditorOption
|
||||||
iconClassName={iconClass}
|
iconClassName={iconClass}
|
||||||
className={switchClassNames}
|
className={menuItemSwitchClassNames}
|
||||||
application={application}
|
application={application}
|
||||||
note={notes[0]}
|
note={notes[0]}
|
||||||
/>
|
/>
|
||||||
@@ -237,14 +229,14 @@ const NotesOptions = ({
|
|||||||
{navigationController.tagsCount > 0 && (
|
{navigationController.tagsCount > 0 && (
|
||||||
<AddTagOption
|
<AddTagOption
|
||||||
iconClassName={iconClass}
|
iconClassName={iconClass}
|
||||||
className={switchClassNames}
|
className={menuItemSwitchClassNames}
|
||||||
navigationController={navigationController}
|
navigationController={navigationController}
|
||||||
notesController={notesController}
|
notesController={notesController}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className={defaultClassNames}
|
className={menuItemClassNames}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
notesController.setStarSelectedNotes(!starred)
|
notesController.setStarSelectedNotes(!starred)
|
||||||
}}
|
}}
|
||||||
@@ -260,7 +252,7 @@ const NotesOptions = ({
|
|||||||
|
|
||||||
{unpinned && (
|
{unpinned && (
|
||||||
<button
|
<button
|
||||||
className={defaultClassNames}
|
className={menuItemClassNames}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
notesController.setPinSelectedNotes(true)
|
notesController.setPinSelectedNotes(true)
|
||||||
}}
|
}}
|
||||||
@@ -276,7 +268,7 @@ const NotesOptions = ({
|
|||||||
)}
|
)}
|
||||||
{pinned && (
|
{pinned && (
|
||||||
<button
|
<button
|
||||||
className={defaultClassNames}
|
className={menuItemClassNames}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
notesController.setPinSelectedNotes(false)
|
notesController.setPinSelectedNotes(false)
|
||||||
}}
|
}}
|
||||||
@@ -290,28 +282,34 @@ const NotesOptions = ({
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
{notes[0].noteType !== NoteType.Super && (
|
||||||
className={defaultClassNames}
|
<>
|
||||||
onClick={() => {
|
<button
|
||||||
application.isNativeMobileWeb() ? void shareSelectedNotes(application, notes) : void downloadSelectedItems()
|
className={menuItemClassNames}
|
||||||
}}
|
onClick={() => {
|
||||||
>
|
application.isNativeMobileWeb()
|
||||||
<Icon type={application.platform === Platform.Android ? 'share' : 'download'} className={iconClass} />
|
? void shareSelectedNotes(application, notes)
|
||||||
{application.platform === Platform.Android ? 'Share' : 'Export'}
|
: void downloadSelectedItems()
|
||||||
</button>
|
}}
|
||||||
{application.platform === Platform.Android && (
|
>
|
||||||
<button className={defaultClassNames} onClick={() => downloadSelectedNotesOnAndroid(application, notes)}>
|
<Icon type={application.platform === Platform.Android ? 'share' : 'download'} className={iconClass} />
|
||||||
<Icon type="download" className={iconClass} />
|
{application.platform === Platform.Android ? 'Share' : 'Export'}
|
||||||
Export
|
</button>
|
||||||
</button>
|
{application.platform === Platform.Android && (
|
||||||
|
<button className={menuItemClassNames} onClick={() => downloadSelectedNotesOnAndroid(application, notes)}>
|
||||||
|
<Icon type="download" className={iconClass} />
|
||||||
|
Export
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<button className={defaultClassNames} onClick={duplicateSelectedItems}>
|
<button className={menuItemClassNames} onClick={duplicateSelectedItems}>
|
||||||
<Icon type="copy" className={iconClass} />
|
<Icon type="copy" className={iconClass} />
|
||||||
Duplicate
|
Duplicate
|
||||||
</button>
|
</button>
|
||||||
{unarchived && (
|
{unarchived && (
|
||||||
<button
|
<button
|
||||||
className={defaultClassNames}
|
className={menuItemClassNames}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await notesController.setArchiveSelectedNotes(true).catch(console.error)
|
await notesController.setArchiveSelectedNotes(true).catch(console.error)
|
||||||
closeMenuAndToggleNotesList()
|
closeMenuAndToggleNotesList()
|
||||||
@@ -323,7 +321,7 @@ const NotesOptions = ({
|
|||||||
)}
|
)}
|
||||||
{archived && (
|
{archived && (
|
||||||
<button
|
<button
|
||||||
className={defaultClassNames}
|
className={menuItemClassNames}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await notesController.setArchiveSelectedNotes(false).catch(console.error)
|
await notesController.setArchiveSelectedNotes(false).catch(console.error)
|
||||||
closeMenuAndToggleNotesList()
|
closeMenuAndToggleNotesList()
|
||||||
@@ -343,7 +341,7 @@ const NotesOptions = ({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
className={defaultClassNames}
|
className={menuItemClassNames}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await notesController.setTrashSelectedNotes(true)
|
await notesController.setTrashSelectedNotes(true)
|
||||||
closeMenuAndToggleNotesList()
|
closeMenuAndToggleNotesList()
|
||||||
@@ -356,7 +354,7 @@ const NotesOptions = ({
|
|||||||
{trashed && (
|
{trashed && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
className={defaultClassNames}
|
className={menuItemClassNames}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await notesController.setTrashSelectedNotes(false)
|
await notesController.setTrashSelectedNotes(false)
|
||||||
closeMenuAndToggleNotesList()
|
closeMenuAndToggleNotesList()
|
||||||
@@ -372,7 +370,7 @@ const NotesOptions = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className={defaultClassNames}
|
className={menuItemClassNames}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await notesController.emptyTrash()
|
await notesController.emptyTrash()
|
||||||
closeMenuAndToggleNotesList()
|
closeMenuAndToggleNotesList()
|
||||||
@@ -392,27 +390,17 @@ const NotesOptions = ({
|
|||||||
{notes.length === 1 ? (
|
{notes.length === 1 ? (
|
||||||
<>
|
<>
|
||||||
{notes[0].noteType === NoteType.Super && (
|
{notes[0].noteType === NoteType.Super && (
|
||||||
<>
|
<SuperNoteOptions
|
||||||
<HorizontalSeparator classes="my-2" />
|
note={notes[0]}
|
||||||
|
markdownShortcut={markdownShortcut}
|
||||||
<div className="my-1 px-3 text-base font-semibold uppercase text-text lg:text-xs">Super</div>
|
enableSuperMarkdownPreview={enableSuperMarkdownPreview}
|
||||||
|
/>
|
||||||
<button className={defaultClassNames} onClick={enableSuperMarkdownPreview}>
|
|
||||||
<div className="flex w-full items-center justify-between">
|
|
||||||
<span className="flex">
|
|
||||||
<Icon type="markdown" className={iconClass} />
|
|
||||||
Show Markdown
|
|
||||||
</span>
|
|
||||||
{markdownShortcut && <KeyboardShortcutIndicator className={''} shortcut={markdownShortcut} />}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
<HorizontalSeparator classes="my-2" />
|
<HorizontalSeparator classes="my-2" />
|
||||||
|
|
||||||
<ListedActionsOption
|
<ListedActionsOption
|
||||||
iconClassName={iconClass}
|
iconClassName={iconClass}
|
||||||
className={switchClassNames}
|
className={menuItemSwitchClassNames}
|
||||||
application={application}
|
application={application}
|
||||||
note={notes[0]}
|
note={notes[0]}
|
||||||
/>
|
/>
|
||||||
@@ -420,7 +408,7 @@ const NotesOptions = ({
|
|||||||
<HorizontalSeparator classes="my-2" />
|
<HorizontalSeparator classes="my-2" />
|
||||||
|
|
||||||
<SpellcheckOptions
|
<SpellcheckOptions
|
||||||
className={switchClassNames}
|
className={menuItemSwitchClassNames}
|
||||||
editorForNote={editorForNote}
|
editorForNote={editorForNote}
|
||||||
notesController={notesController}
|
notesController={notesController}
|
||||||
note={notes[0]}
|
note={notes[0]}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Switch from '@/Components/Switch/Switch'
|
|||||||
import { FunctionComponent } from 'react'
|
import { FunctionComponent } from 'react'
|
||||||
import { SNComponent, SNNote } from '@standardnotes/snjs'
|
import { SNComponent, SNNote } from '@standardnotes/snjs'
|
||||||
import { NotesController } from '@/Controllers/NotesController/NotesController'
|
import { NotesController } from '@/Controllers/NotesController/NotesController'
|
||||||
import { iconClass } from './NotesOptions'
|
import { iconClass } from './ClassNames'
|
||||||
|
|
||||||
export const SpellcheckOptions: FunctionComponent<{
|
export const SpellcheckOptions: FunctionComponent<{
|
||||||
editorForNote: SNComponent | undefined
|
editorForNote: SNComponent | undefined
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { SNNote } from '@standardnotes/snjs'
|
||||||
|
import {
|
||||||
|
PlatformedKeyboardShortcut,
|
||||||
|
SUPER_EXPORT_HTML,
|
||||||
|
SUPER_EXPORT_JSON,
|
||||||
|
SUPER_EXPORT_MARKDOWN,
|
||||||
|
} from '@standardnotes/ui-services'
|
||||||
|
import { useRef, useState } from 'react'
|
||||||
|
import { useCommandService } from '../ApplicationView/CommandProvider'
|
||||||
|
import Icon from '../Icon/Icon'
|
||||||
|
import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator'
|
||||||
|
import Menu from '../Menu/Menu'
|
||||||
|
import MenuItem from '../Menu/MenuItem'
|
||||||
|
import Popover from '../Popover/Popover'
|
||||||
|
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
||||||
|
import { iconClass, menuItemClassNames, menuItemSwitchClassNames } from './ClassNames'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
note: SNNote
|
||||||
|
markdownShortcut?: PlatformedKeyboardShortcut
|
||||||
|
enableSuperMarkdownPreview: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const SuperNoteOptions = ({ note, markdownShortcut, enableSuperMarkdownPreview }: Props) => {
|
||||||
|
const commandService = useCommandService()
|
||||||
|
|
||||||
|
const exportButtonRef = useRef<HTMLButtonElement>(null)
|
||||||
|
const [isExportMenuOpen, setIsExportMenuOpen] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<HorizontalSeparator classes="my-2" />
|
||||||
|
|
||||||
|
<div className="my-1 px-3 text-base font-semibold uppercase text-text lg:text-xs">Super</div>
|
||||||
|
|
||||||
|
<button className={menuItemClassNames} onClick={enableSuperMarkdownPreview}>
|
||||||
|
<div className="flex w-full items-center justify-between">
|
||||||
|
<span className="flex">
|
||||||
|
<Icon type="markdown" className={iconClass} />
|
||||||
|
Show Markdown
|
||||||
|
</span>
|
||||||
|
{markdownShortcut && <KeyboardShortcutIndicator shortcut={markdownShortcut} />}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
ref={exportButtonRef}
|
||||||
|
className={menuItemSwitchClassNames}
|
||||||
|
onClick={() => {
|
||||||
|
setIsExportMenuOpen((open) => !open)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Icon type="download" className={iconClass} />
|
||||||
|
Export
|
||||||
|
</div>
|
||||||
|
<Icon type="chevron-right" className="text-neutral" />
|
||||||
|
</button>
|
||||||
|
<Popover
|
||||||
|
side="left"
|
||||||
|
align="start"
|
||||||
|
open={isExportMenuOpen}
|
||||||
|
anchorElement={exportButtonRef.current}
|
||||||
|
togglePopover={() => {
|
||||||
|
setIsExportMenuOpen(!isExportMenuOpen)
|
||||||
|
}}
|
||||||
|
className="py-1"
|
||||||
|
>
|
||||||
|
<Menu a11yLabel={'Super note export menu'} isOpen={isExportMenuOpen}>
|
||||||
|
<MenuItem onClick={() => commandService.triggerCommand(SUPER_EXPORT_JSON, note.title)}>
|
||||||
|
<Icon type="code" className={iconClass} />
|
||||||
|
Export as JSON
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => commandService.triggerCommand(SUPER_EXPORT_MARKDOWN, note.title)}>
|
||||||
|
<Icon type="markdown" className={iconClass} />
|
||||||
|
Export as Markdown
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => commandService.triggerCommand(SUPER_EXPORT_HTML, note.title)}>
|
||||||
|
<Icon type="rich-text" className={iconClass} />
|
||||||
|
Export as HTML
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</Popover>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SuperNoteOptions
|
||||||
Reference in New Issue
Block a user