UNPKG

@wordpress/editor

Version:
277 lines (258 loc) 7.25 kB
/** * WordPress dependencies */ import { useEffect, useMemo } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { useSelect, useDispatch } from '@wordpress/data'; import { Stack } from '@wordpress/ui'; import { store as blockEditorStore, privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; /** * Internal dependencies */ import { unlock } from '../../lock-unlock'; import { NoteThread } from './note-thread'; import { focusNoteThread } from './utils'; import { useFloatingBoard, useNoteActions } from './hooks'; import { AddNote } from './add-note'; import { store as editorStore } from '../../store'; const { useBlockElement } = unlock( blockEditorPrivateApis ); export function Notes( { notes, sidebarRef, isFloating = false, styles } ) { const { onCreate: onAddReply, onEdit: onEditNote, onDelete, } = useNoteActions(); const { selectNote } = unlock( useDispatch( editorStore ) ); const { selectBlock, toggleBlockSpotlight } = unlock( useDispatch( blockEditorStore ) ); const { blockNoteId, selectedBlockClientId, orderedBlockIds } = useSelect( ( select ) => { const { getBlockAttributes, getSelectedBlockClientId, getClientIdsWithDescendants, } = select( blockEditorStore ); const clientId = getSelectedBlockClientId(); return { blockNoteId: clientId ? getBlockAttributes( clientId )?.metadata?.noteId : null, selectedBlockClientId: clientId, orderedBlockIds: getClientIdsWithDescendants(), }; }, [] ); const { selectedNote, noteFocused } = useSelect( ( select ) => { const { getSelectedNote, isNoteFocused } = unlock( select( editorStore ) ); return { selectedNote: getSelectedNote(), noteFocused: isNoteFocused(), }; }, [] ); const relatedBlockElement = useBlockElement( selectedBlockClientId ); const threads = useMemo( () => { // In floating mode with a pending new note, splice a placeholder // entry at the selected block's position so the board can float it // alongside regular threads. if ( ! isFloating || selectedNote !== 'new' ) { return notes; } const newNoteThread = { id: 'new', blockClientId: selectedBlockClientId, content: { rendered: '' }, }; const out = []; orderedBlockIds.forEach( ( blockId ) => { if ( blockId === selectedBlockClientId ) { out.push( newNoteThread ); } else { const threadForBlock = notes.find( ( t ) => t.blockClientId === blockId ); if ( threadForBlock ) { out.push( threadForBlock ); } } } ); return out; }, [ notes, isFloating, selectedNote, selectedBlockClientId, orderedBlockIds, ] ); const handleDelete = async ( note ) => { const currentIndex = threads.findIndex( ( t ) => t.id === note.id ); const nextThread = threads[ currentIndex + 1 ]; const prevThread = threads[ currentIndex - 1 ]; await onDelete( note ); if ( note.parent !== 0 ) { // Move focus to the parent thread when a reply was deleted. selectNote( note.parent ); focusNoteThread( note.parent, sidebarRef.current ); return; } if ( nextThread ) { selectNote( nextThread.id ); focusNoteThread( nextThread.id, sidebarRef.current ); } else if ( prevThread ) { selectNote( prevThread.id ); focusNoteThread( prevThread.id, sidebarRef.current ); } else { selectNote( undefined ); toggleBlockSpotlight( note.blockClientId, false ); // Move focus to the related block. relatedBlockElement?.focus(); } }; // Auto-select the related note thread when a block is selected. useEffect( () => { selectNote( blockNoteId ?? undefined ); }, [ blockNoteId, selectNote ] ); // Focus the selected note when requested. useEffect( () => { if ( noteFocused && selectedNote ) { focusNoteThread( selectedNote, sidebarRef.current, selectedNote === 'new' ? 'textarea' : undefined ); // Clear focus flag to avoid re-triggering. selectNote( selectedNote ); } }, [ noteFocused, selectedNote, selectNote, sidebarRef ] ); const { notePositions, registerThread, unregisterThread } = useFloatingBoard( { threads, selectedNoteId: selectedNote, isFloating, sidebarRef, } ); const hasThreads = Array.isArray( threads ) && threads.length > 0; const navigate = ( event, thread, isSelected ) => { if ( event.defaultPrevented ) { return; } const currentIndex = threads.findIndex( ( t ) => t.id === thread.id ); const isSelfTarget = event.currentTarget === event.target; if ( ( event.key === 'Enter' || event.key === 'ArrowRight' ) && isSelfTarget && ! isSelected ) { // Expand thread. selectNote( thread.id ); if ( !! thread.blockClientId ) { // Pass `null` as the second parameter to prevent focusing the block. selectBlock( thread.blockClientId, null ); toggleBlockSpotlight( thread.blockClientId, true ); } } else if ( ( ( event.key === 'Enter' || event.key === 'ArrowLeft' ) && isSelfTarget && isSelected ) || event.key === 'Escape' ) { // Collapse thread. selectNote( undefined ); if ( thread.blockClientId ) { toggleBlockSpotlight( thread.blockClientId, false ); } focusNoteThread( thread.id, sidebarRef.current ); } else if ( event.key === 'ArrowDown' && currentIndex < threads.length - 1 && isSelfTarget ) { focusNoteThread( threads[ currentIndex + 1 ].id, sidebarRef.current ); } else if ( event.key === 'ArrowUp' && currentIndex > 0 && isSelfTarget ) { focusNoteThread( threads[ currentIndex - 1 ].id, sidebarRef.current ); } else if ( event.key === 'Home' && isSelfTarget ) { focusNoteThread( threads[ 0 ].id, sidebarRef.current ); } else if ( event.key === 'End' && isSelfTarget ) { focusNoteThread( threads[ threads.length - 1 ].id, sidebarRef.current ); } }; return ( <Stack className="editor-collab-sidebar-panel" style={ styles } role="tree" direction="column" gap="md" justify="flex-start" ref={ ( node ) => { // Sometimes previous sidebar unmounts after the new one mounts. // This ensures we always have the latest reference. if ( node ) { sidebarRef.current = node; } } } aria-label={ isFloating ? __( 'Unresolved notes' ) : __( 'All notes' ) } > { ! hasThreads && ! isFloating ? ( <AddNote onSubmit={ onAddReply } sidebarRef={ sidebarRef } /> ) : ( <> { ! isFloating && selectedNote === 'new' && ( <AddNote onSubmit={ onAddReply } sidebarRef={ sidebarRef } /> ) } { threads.map( ( thread ) => ( <NoteThread key={ thread.id } note={ thread } onAddReply={ onAddReply } onDeleteNote={ handleDelete } onEditNote={ onEditNote } isSelected={ selectedNote === thread.id } sidebarRef={ sidebarRef } floating={ isFloating ? { y: notePositions[ thread.id ], registerThread, unregisterThread, } : undefined } onKeyDown={ ( event ) => navigate( event, thread, selectedNote === thread.id ) } /> ) ) } </> ) } </Stack> ); }