148 lines
4.5 KiB
TypeScript
148 lines
4.5 KiB
TypeScript
/**
|
|
* External dependencies
|
|
*/
|
|
import { useCallback, useRef, useEffect } from '@wordpress/element';
|
|
import debugFactory from 'debug';
|
|
|
|
const debug = debugFactory( 'jetpack-ai-assistant:use-auto-scroll' );
|
|
|
|
const useAutoScroll = (
|
|
blockRef: React.MutableRefObject< HTMLElement | null >,
|
|
contentRef?: React.MutableRefObject< HTMLElement >,
|
|
useBlockAsTarget: boolean = false
|
|
) => {
|
|
const scrollElementRef = useRef< HTMLElement | Document | null >( null );
|
|
const styledScrollElementRef = useRef< HTMLElement | null >( null );
|
|
const autoScrollEnabled = useRef( false );
|
|
const ignoreScroll = useRef( false );
|
|
const startedAutoScroll = useRef( false );
|
|
const doingAutoScroll = useRef( false );
|
|
const scrollElementOriginalStyle = useRef< { scrollPadding: string; scrollMargin: string } >( {
|
|
scrollPadding: '',
|
|
scrollMargin: '',
|
|
} );
|
|
|
|
const enableIgnoreScroll = useCallback( () => {
|
|
debug( 'enabling ignore scroll' );
|
|
ignoreScroll.current = true;
|
|
}, [] );
|
|
|
|
const userScrollHandler = useCallback( () => {
|
|
if ( autoScrollEnabled.current && startedAutoScroll.current && ! ignoreScroll.current ) {
|
|
enableIgnoreScroll();
|
|
}
|
|
}, [ enableIgnoreScroll ] );
|
|
|
|
const enableAutoScroll = useCallback( () => {
|
|
autoScrollEnabled.current = true;
|
|
ignoreScroll.current = false;
|
|
startedAutoScroll.current = false;
|
|
doingAutoScroll.current = false;
|
|
scrollElementRef.current?.addEventListener( 'scroll', userScrollHandler );
|
|
debug( 'enabling auto scroll' );
|
|
}, [ userScrollHandler ] );
|
|
|
|
const disableAutoScroll = useCallback( () => {
|
|
autoScrollEnabled.current = false;
|
|
ignoreScroll.current = false;
|
|
startedAutoScroll.current = false;
|
|
doingAutoScroll.current = false;
|
|
scrollElementRef.current?.removeEventListener( 'scroll', userScrollHandler );
|
|
|
|
// Reset scroll padding and margin
|
|
if ( styledScrollElementRef.current?.style ) {
|
|
styledScrollElementRef.current.style.scrollPadding =
|
|
scrollElementOriginalStyle.current.scrollPadding;
|
|
styledScrollElementRef.current.style.scrollMargin =
|
|
scrollElementOriginalStyle.current.scrollMargin;
|
|
}
|
|
|
|
scrollElementRef.current = null;
|
|
debug( 'disabling auto scroll' );
|
|
}, [ userScrollHandler ] );
|
|
|
|
const snapToBottom = useCallback( () => {
|
|
if ( ! autoScrollEnabled.current || ignoreScroll.current ) {
|
|
return;
|
|
}
|
|
|
|
const lastParagraph = useBlockAsTarget
|
|
? blockRef?.current
|
|
: contentRef?.current?.firstElementChild?.lastElementChild;
|
|
|
|
if ( lastParagraph && ! doingAutoScroll.current ) {
|
|
startedAutoScroll.current = true;
|
|
doingAutoScroll.current = true;
|
|
|
|
scrollElementRef?.current?.removeEventListener?.( 'scroll', userScrollHandler );
|
|
lastParagraph?.scrollIntoView( { block: 'end', inline: 'end' } );
|
|
|
|
setTimeout( () => {
|
|
doingAutoScroll.current = false;
|
|
scrollElementRef?.current?.addEventListener?.( 'scroll', userScrollHandler );
|
|
}, 200 );
|
|
}
|
|
}, [ blockRef, contentRef, useBlockAsTarget, userScrollHandler ] );
|
|
|
|
const getScrollParent = useCallback(
|
|
( el: HTMLElement | null | undefined ): HTMLElement | Document | null => {
|
|
if ( el == null ) {
|
|
return null;
|
|
}
|
|
|
|
// If we arrived to the body, we should stop since it's the last event handler
|
|
if ( el?.nodeName === 'BODY' ) {
|
|
return el;
|
|
}
|
|
|
|
// Gutenberg on newest version run inside iframe, on that case the scroll parent is the iframe
|
|
if ( el?.ownerDocument !== document ) {
|
|
return el.ownerDocument;
|
|
}
|
|
|
|
const { overflow } = window.getComputedStyle( el );
|
|
|
|
if ( overflow.split( ' ' ).every( o => o === 'auto' || o === 'scroll' ) ) {
|
|
return el;
|
|
}
|
|
|
|
if ( ! el?.parentElement ) {
|
|
return el;
|
|
}
|
|
|
|
return getScrollParent( el?.parentElement );
|
|
},
|
|
[]
|
|
);
|
|
|
|
useEffect( () => {
|
|
const parent = getScrollParent( blockRef?.current?.parentElement );
|
|
|
|
if ( ! scrollElementRef.current && parent ) {
|
|
scrollElementRef.current = parent;
|
|
styledScrollElementRef.current =
|
|
parent instanceof HTMLElement ? parent : parent.documentElement;
|
|
|
|
// Save the original scroll padding and margin
|
|
scrollElementOriginalStyle.current = {
|
|
scrollPadding: styledScrollElementRef.current.style.scrollPadding,
|
|
scrollMargin: styledScrollElementRef.current.style.scrollMargin,
|
|
};
|
|
|
|
if ( autoScrollEnabled.current ) {
|
|
// Add scroll padding and margin to avoid the content to be hidden by the fixed input bar
|
|
styledScrollElementRef.current.style.scrollPadding = '80px';
|
|
styledScrollElementRef.current.style.scrollMargin = '10px';
|
|
}
|
|
}
|
|
}, [ blockRef, getScrollParent ] );
|
|
|
|
return {
|
|
snapToBottom,
|
|
enableAutoScroll,
|
|
disableAutoScroll,
|
|
};
|
|
};
|
|
|
|
export default useAutoScroll;
|