diff --git a/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.ts b/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.ts index 615f60d5f..5223dd6b0 100644 --- a/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.ts +++ b/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.ts @@ -111,7 +111,7 @@ export class NoteViewController implements ItemViewControllerInterface { this.needsInit = false - const addTagHierarchy = this.preferences.getValue(PrefKey.NoteAddToParentFolders, true) + const shouldAddTagHierarchy = this.preferences.getValue(PrefKey.NoteAddToParentFolders, true) if (!this.item) { log(LoggingDomain.NoteView, 'Initializing as template note') @@ -141,7 +141,7 @@ export class NoteViewController implements ItemViewControllerInterface { if (this.defaultTagUuid) { const tag = this.items.findItem(this.defaultTagUuid) as SNTag - await this.mutator.addTagToNote(note, tag, addTagHierarchy) + await this.mutator.addTagToNote(note, tag, shouldAddTagHierarchy) } this.notifyObservers(this.item, PayloadEmitSource.InitialObserverRegistrationPush) diff --git a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx index 3b5824a7e..8bbfc1c49 100644 --- a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx +++ b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx @@ -800,6 +800,10 @@ class NoteView extends AbstractComponent { } triggerSyncOnAction = () => { + if (!this.controller) { + // component might've already unmounted + return + } this.controller.syncNow() } diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/NoteFromSelectionPlugin.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/NoteFromSelectionPlugin.tsx new file mode 100644 index 000000000..697e5825f --- /dev/null +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/NoteFromSelectionPlugin.tsx @@ -0,0 +1,52 @@ +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { $getSelection, $isRangeSelection, COMMAND_PRIORITY_EDITOR, createCommand } from 'lexical' +import { useEffect } from 'react' +import { useApplication } from '../../ApplicationProvider' +import { NativeFeatureIdentifier, SNNote } from '@standardnotes/snjs' +import { $generateJSONFromSelectedNodes } from '@lexical/clipboard' +import { HeadlessSuperConverter } from '../Tools/HeadlessSuperConverter' +import { INSERT_BUBBLE_COMMAND } from './Commands' + +export const CREATE_NOTE_FROM_SELECTION_COMMAND = createCommand('CREATE_NOTE_FROM_SELECTION_COMMAND') + +export function NoteFromSelectionPlugin({ currentNote }: { currentNote: SNNote }) { + const application = useApplication() + const [editor] = useLexicalComposerContext() + + useEffect(() => { + async function insertAndLinkNewNoteFromJSON(json: string) { + editor.setEditable(false) + try { + const insertedNote = await application.notesController.createNoteWithContent( + NativeFeatureIdentifier.TYPES.SuperEditor, + application.itemListController.titleForNewNote(), + json, + ) + await application.linkingController.linkItems(currentNote, insertedNote) + editor.dispatchCommand(INSERT_BUBBLE_COMMAND, insertedNote.uuid) + } catch (error) { + console.error(error) + } finally { + editor.setEditable(true) + } + } + + return editor.registerCommand( + CREATE_NOTE_FROM_SELECTION_COMMAND, + function createNoteFromSelection() { + const selection = $getSelection() + if (!$isRangeSelection(selection)) { + return true + } + const { nodes } = $generateJSONFromSelectedNodes(editor, selection) + const converter = new HeadlessSuperConverter() + const json = converter.getStringifiedJSONFromSerializedNodes(nodes) + insertAndLinkNewNoteFromJSON(json).catch(console.error) + return true + }, + COMMAND_PRIORITY_EDITOR, + ) + }, [application.itemListController, application.linkingController, application.notesController, currentNote, editor]) + + return null +} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarPlugin.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarPlugin.tsx index 07e6e9514..c66a49bb0 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarPlugin.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarPlugin.tsx @@ -77,6 +77,7 @@ import { ElementIds } from '@/Constants/ElementIDs' import { $isDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode' import LinkViewer from './LinkViewer' import { OPEN_FILE_UPLOAD_MODAL_COMMAND } from '../EncryptedFilePlugin/FilePlugin' +import { CREATE_NOTE_FROM_SELECTION_COMMAND } from '../NoteFromSelectionPlugin' const TOGGLE_LINK_AND_EDIT_COMMAND = createCommand('TOGGLE_LINK_AND_EDIT_COMMAND') @@ -110,8 +111,8 @@ const blockTypeToIconName = { quote: 'quote', } -interface ToolbarButtonProps extends ComponentPropsWithoutRef<'button'> { - name: string +interface ToolbarButtonProps extends Omit, 'name'> { + name: NonNullable active?: boolean iconName?: string children?: ReactNode @@ -222,6 +223,8 @@ const ToolbarPlugin = () => { const [isCode, setIsCode] = useState(false) const [isHighlight, setIsHighlight] = useState(false) + const [hasNonCollapsedSelection, setHasNonCollapsedSelection] = useState(false) + const [linkNode, setLinkNode] = useState(null) const [linkTextNode, setLinkTextNode] = useState(null) const [isEditingLink, setIsEditingLink] = useState(false) @@ -312,6 +315,8 @@ const ToolbarPlugin = () => { return } + setHasNonCollapsedSelection(!selection.isCollapsed()) + const anchorNode = selection.anchor.getNode() const focusNode = selection.focus.getNode() const isAnchorSameAsFocus = anchorNode === focusNode @@ -796,6 +801,23 @@ const ToolbarPlugin = () => { )} + {hasNonCollapsedSelection && ( + +
Create new note from selection
+
+ Creates a new note containing the current selection and replaces the selection with a link to the + new note. +
+ + } + iconName="notes" + onSelect={() => { + editor.dispatchCommand(CREATE_NOTE_FROM_SELECTION_COMMAND, undefined) + }} + /> + )} {isMobile && (