@wordpress/editor
Version:
Enhanced block editor for WordPress posts.
277 lines (258 loc) • 7.25 kB
JavaScript
/**
* 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>
);
}