@wordpress/editor
Version:
Enhanced block editor for WordPress posts.
162 lines (150 loc) • 4.29 kB
JavaScript
/**
* WordPress dependencies
*/
import {
useState,
useMemo,
useRef,
useCallback,
useEffect,
} from '@wordpress/element';
import { useRefEffect, useMergeRefs } from '@wordpress/compose';
import { useSelect } from '@wordpress/data';
import {
privateApis as blockEditorPrivateApis,
store as blockEditorStore,
} from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { unlock } from '../../lock-unlock';
const { useBlockElementRef } = unlock( blockEditorPrivateApis );
/**
* Recursively collect blocks with diff status.
*
* @param {Array} blocks The blocks to search.
* @return {Array} Blocks with __revisionDiffStatus.
*/
function collectDiffBlocks( blocks ) {
const result = [];
for ( const block of blocks ) {
if ( block.__revisionDiffStatus?.status ) {
result.push( {
clientId: block.clientId,
status: block.__revisionDiffStatus.status,
} );
}
if ( block.innerBlocks?.length ) {
result.push( ...collectDiffBlocks( block.innerBlocks ) );
}
}
return result;
}
const STATUS_LABELS = {
added: __( 'Go to added block' ),
removed: __( 'Go to removed block' ),
modified: __( 'Go to modified block' ),
};
function calculatePosition( el ) {
if ( ! el ) {
return null;
}
const doc = el.ownerDocument;
const scrollHeight = doc.documentElement.scrollHeight;
const rect = el.getBoundingClientRect();
const scrollTop = doc.documentElement.scrollTop;
const top = rect.top + scrollTop;
return {
top: ( top / scrollHeight ) * 100,
height: ( rect.height / scrollHeight ) * 100,
};
}
/**
* Button component for a single diff marker.
*
* @param {Object} props Component props.
* @param {string} props.clientId The block client ID.
* @param {string} props.status The diff status (added/removed/modified).
* @param {Function} props.subscribe Function to subscribe to position updates.
* @return {React.JSX.Element} The diff marker button or null if position not calculated.
*/
function DiffMarkerButton( { clientId, status, subscribe } ) {
const blockRef = useRef();
useBlockElementRef( clientId, blockRef );
const [ position, setPosition ] = useState( () =>
calculatePosition( blockRef.current )
);
useEffect( () => {
return subscribe( () => {
setPosition( calculatePosition( blockRef.current ) );
} );
}, [ subscribe ] );
useEffect( () => {
setPosition( calculatePosition( blockRef.current ) );
}, [ status ] );
if ( ! position ) {
return null;
}
return (
<button
className={ `revision-diff-marker is-${ status }` }
style={ {
top: `${ position.top }%`,
height: `${ Math.max( position.height, 0.5 ) }%`,
} }
onClick={ () => blockRef.current?.focus() }
aria-label={ STATUS_LABELS[ status ] }
/>
);
}
/**
* Hook that provides diff markers functionality.
* Returns a ref callback for the content element and a DiffMarkers component.
* Must be used inside a BlockEditorProvider context.
*
* @return {Array} Tuple of [contentRef, DiffMarkersComponent].
*/
export function useDiffMarkers() {
const [ isMounted, setIsMounted ] = useState( false );
const subscribersRef = useRef( new Set() );
const blocks = useSelect(
( select ) => select( blockEditorStore ).getBlocks(),
[]
);
const diffBlocks = useMemo( () => collectDiffBlocks( blocks ), [ blocks ] );
const subscribe = useCallback( ( callback ) => {
subscribersRef.current.add( callback );
return () => subscribersRef.current.delete( callback );
}, [] );
const contentRef = useRefEffect( ( element ) => {
const { ownerDocument } = element;
const { defaultView } = ownerDocument;
const resizeObserver = new defaultView.ResizeObserver( () => {
subscribersRef.current.forEach( ( cb ) => cb() );
} );
resizeObserver.observe( ownerDocument.body );
return () => {
resizeObserver.disconnect();
};
}, [] );
return [
useMergeRefs( [ contentRef, setIsMounted ] ),
<div
key="diff-markers"
className="revision-diff-markers"
role="navigation"
aria-label={ __( 'Diff markers' ) }
>
{ isMounted &&
diffBlocks.map( ( { clientId, status } ) => (
<DiffMarkerButton
key={ clientId }
clientId={ clientId }
status={ status }
subscribe={ subscribe }
/>
) ) }
</div>,
];
}