interface visualViewport { height : number ; width : number ; } type ScrollLogicalPosition = 'start' | 'center' | 'end' | 'nearest' ; type ScrollMode = 'always' | 'if-needed' ; type SkipOverflowHiddenElements = boolean ; interface Options { block ? : ScrollLogicalPosition ; inline ? : ScrollLogicalPosition ; scrollMode ? : ScrollMode ; boundary ? : CustomScrollBoundary ; skipOverflowHiddenElements ? : SkipOverflowHiddenElements ; } type CustomScrollBoundaryCallback = ( parent : Element ) => boolean ; type CustomScrollBoundary = Element | CustomScrollBoundaryCallback | null ; interface CustomScrollAction { el : Element ; top : number ; left : number ; } function isElement ( el ) { return typeof el === 'object' && el != null && el . nodeType === 1 ; } function canOverflow ( overflow , skipOverflowHiddenElements ? ) { if ( skipOverflowHiddenElements && overflow === 'hidden' ) { return false ; } return overflow !== 'visible' && overflow !== 'clip' ; } function getFrameElement ( el ) { if ( ! el . ownerDocument || ! el . ownerDocument . defaultView ) { return null ; } try { return el . ownerDocument . defaultView . frameElement ; } catch ( e ) { return null ; } } function isHiddenByFrame ( el ) { const frame = getFrameElement ( el ) ; if ( ! frame ) { return false ; } return ( frame . clientHeight < el . scrollHeight || frame . clientWidth < el . scrollWidth ) ; } function isScrollable ( el , skipOverflowHiddenElements ? ) { if ( el . clientHeight < el . scrollHeight || el . clientWidth < el . scrollWidth ) { const style = getComputedStyle ( el , null ) ; return ( canOverflow ( style . overflowY , skipOverflowHiddenElements ) || canOverflow ( style . overflowX , skipOverflowHiddenElements ) || isHiddenByFrame ( el ) ) ; } return false ; } /**
 * Find out which edge to align against when logical scroll position is "nearest"
 * Interesting fact: "nearest" works similarily to "if-needed", if the element is fully visible it will not scroll it
 *
 * Legends:
 * ┌────────┐ ┏ ━ ━ ━ ┓
 * │ target │   frame
 * └────────┘ ┗ ━ ━ ━ ┛
 */ function alignNearest ( scrollingEdgeStart , scrollingEdgeEnd , scrollingSize , scrollingBorderStart , scrollingBorderEnd , elementEdgeStart , elementEdgeEnd , elementSize ) { /**
     * If element edge A and element edge B are both outside scrolling box edge A and scrolling box edge B
     *
     *          ┌──┐
     *        ┏━│━━│━┓
     *          │  │
     *        ┃ │  │ ┃        do nothing
     *          │  │
     *        ┗━│━━│━┛
     *          └──┘
     *
     *  If element edge C and element edge D are both outside scrolling box edge C and scrolling box edge D
     *
     *    ┏ ━ ━ ━ ━ ┓
     *   ┌───────────┐
     *   │┃         ┃│        do nothing
     *   └───────────┘
     *    ┗ ━ ━ ━ ━ ┛
     */ if ( ( elementEdgeStart < scrollingEdgeStart && elementEdgeEnd > scrollingEdgeEnd ) || ( elementEdgeStart > scrollingEdgeStart && elementEdgeEnd < scrollingEdgeEnd ) ) { return 0 ; } /**
     * If element edge A is outside scrolling box edge A and element height is less than scrolling box height
     *
     *          ┌──┐
     *        ┏━│━━│━┓         ┏━┌━━┐━┓
     *          └──┘             │  │
     *  from  ┃      ┃     to  ┃ └──┘ ┃
     *
     *        ┗━ ━━ ━┛         ┗━ ━━ ━┛
     *
     * If element edge B is outside scrolling box edge B and element height is greater than scrolling box height
     *
     *        ┏━ ━━ ━┓         ┏━┌━━┐━┓
     *                           │  │
     *  from  ┃ ┌──┐ ┃     to  ┃ │  │ ┃
     *          │  │             │  │
     *        ┗━│━━│━┛         ┗━│━━│━┛
     *          │  │             └──┘
     *          │  │
     *          └──┘
     *
     * If element edge C is outside scrolling box edge C and element width is less than scrolling box width
     *
     *       from                 to
     *    ┏ ━ ━ ━ ━ ┓         ┏ ━ ━ ━ ━ ┓
     *  ┌───┐                 ┌───┐
     *  │ ┃ │       ┃         ┃   │     ┃
     *  └───┘                 └───┘
     *    ┗ ━ ━ ━ ━ ┛         ┗ ━ ━ ━ ━ ┛
     *
     * If element edge D is outside scrolling box edge D and element width is greater than scrolling box width
     *
     *       from                 to
     *    ┏ ━ ━ ━ ━ ┓         ┏ ━ ━ ━ ━ ┓
     *        ┌───────────┐   ┌───────────┐
     *    ┃   │     ┃     │   ┃         ┃ │
     *        └───────────┘   └───────────┘
     *    ┗ ━ ━ ━ ━ ┛         ┗ ━ ━ ━ ━ ┛
     */ if ( ( elementEdgeStart <= scrollingEdgeStart && elementSize <= scrollingSize ) || ( elementEdgeEnd >= scrollingEdgeEnd && elementSize >= scrollingSize ) ) { return elementEdgeStart - scrollingEdgeStart - scrollingBorderStart ; } /**
     * If element edge B is outside scrolling box edge B and element height is less than scrolling box height
     *
     *        ┏━ ━━ ━┓         ┏━ ━━ ━┓
     *
     *  from  ┃      ┃     to  ┃ ┌──┐ ┃
     *          ┌──┐             │  │
     *        ┗━│━━│━┛         ┗━└━━┘━┛
     *          └──┘
     *
     * If element edge A is outside scrolling box edge A and element height is greater than scrolling box height
     *
     *          ┌──┐
     *          │  │
     *          │  │             ┌──┐
     *        ┏━│━━│━┓         ┏━│━━│━┓
     *          │  │             │  │
     *  from  ┃ └──┘ ┃     to  ┃ │  │ ┃
     *                           │  │
     *        ┗━ ━━ ━┛         ┗━└━━┘━┛
     *
     * If element edge C is outside scrolling box edge C and element width is greater than scrolling box width
     *
     *           from                 to
     *        ┏ ━ ━ ━ ━ ┓         ┏ ━ ━ ━ ━ ┓
     *  ┌───────────┐           ┌───────────┐
     *  │     ┃     │   ┃       │ ┃         ┃
     *  └───────────┘           └───────────┘
     *        ┗ ━ ━ ━ ━ ┛         ┗ ━ ━ ━ ━ ┛
     *
     * If element edge D is outside scrolling box edge D and element width is less than scrolling box width
     *
     *           from                 to
     *        ┏ ━ ━ ━ ━ ┓         ┏ ━ ━ ━ ━ ┓
     *                ┌───┐             ┌───┐
     *        ┃       │ ┃ │       ┃     │   ┃
     *                └───┘             └───┘
     *        ┗ ━ ━ ━ ━ ┛         ┗ ━ ━ ━ ━ ┛
     *
     */ if ( ( elementEdgeEnd > scrollingEdgeEnd && elementSize < scrollingSize ) || ( elementEdgeStart < scrollingEdgeStart && elementSize > scrollingSize ) ) { return elementEdgeEnd - scrollingEdgeEnd + scrollingBorderEnd ; } return 0 ; } export default ( target , options ) : CustomScrollAction [ ] => { const windowWithViewport = ( window as unknown ) as Window & { visualViewport : visualViewport ; } ; const { scrollMode , block , inline , boundary , skipOverflowHiddenElements , } = options ; const checkBoundary = typeof boundary === 'function' ? boundary : ( node ) => node !== boundary ; if ( ! isElement ( target ) ) { throw new TypeError ( 'Invalid target' ) ; } const scrollingElement = document . scrollingElement || document . documentElement ; const frames = [ ] ; let cursor = target ; while ( isElement ( cursor ) && checkBoundary ( cursor ) ) { cursor = cursor . parentElement ; if ( cursor === scrollingElement ) { frames . push ( cursor ) ; break ; } if ( cursor != null && cursor === document . body && isScrollable ( cursor ) && ! isScrollable ( document . documentElement ) ) { continue ; } if ( cursor != null && isScrollable ( cursor , skipOverflowHiddenElements ) ) { frames . push ( cursor ) ; } } const viewportWidth = windowWithViewport . visualViewport ? windowWithViewport . visualViewport . width : innerWidth ; const viewportHeight = windowWithViewport . visualViewport ? windowWithViewport . visualViewport . height : innerHeight ; const viewportX = window . scrollX || pageXOffset ; const viewportY = window . scrollY || pageYOffset ; const { height : targetHeight , width : targetWidth , top : targetTop , right : targetRight , bottom : targetBottom , left : targetLeft , } = target . getBoundingClientRect ( ) ; let targetBlock = block === 'start' || block === 'nearest' ? targetTop : block === 'end' ? targetBottom : targetTop + targetHeight / 2 ; let targetInline = inline === 'center' ? targetLeft + targetWidth / 2 : inline === 'end' ? targetRight : targetLeft ; const computations = [ ] ; for ( let index = 0 ; index < frames . length ; index ++ ) { const frame = frames [ index ] ; const { height , width , top , right , bottom , left , } = frame . getBoundingClientRect ( ) ; if ( scrollMode === 'if-needed' && targetTop >= 0 && targetLeft >= 0 && targetBottom <= viewportHeight && targetRight <= viewportWidth && targetTop >= top && targetBottom <= bottom && targetLeft >= left && targetRight <= right ) { return computations ; } const frameStyle = getComputedStyle ( frame ) ; const borderLeft = parseInt ( frameStyle . borderLeftWidth as string , 10 ) ; const borderTop = parseInt ( frameStyle . borderTopWidth as string , 10 ) ; const borderRight = parseInt ( frameStyle . borderRightWidth as string , 10 ) ; const borderBottom = parseInt ( frameStyle . borderBottomWidth as string , 10 ) ; let blockScroll = 0 ; let inlineScroll = 0 ; const scrollbarWidth = 'offsetWidth' in frame ? ( frame as HTMLElement ) . offsetWidth - ( frame as HTMLElement ) . clientWidth - borderLeft - borderRight : 0 ; const scrollbarHeight = 'offsetHeight' in frame ? ( frame as HTMLElement ) . offsetHeight - ( frame as HTMLElement ) . clientHeight - borderTop - borderBottom : 0 ; if ( scrollingElement === frame ) { if ( block === 'start' ) { blockScroll = targetBlock ; } else if ( block === 'end' ) { blockScroll = targetBlock - viewportHeight ; } else if ( block === 'nearest' ) { blockScroll = alignNearest ( viewportY , viewportY + viewportHeight , viewportHeight , borderTop , borderBottom , viewportY + targetBlock , viewportY + targetBlock + targetHeight , targetHeight ) ; } else { blockScroll = targetBlock - viewportHeight / 2 ; } if ( inline === 'start' ) { inlineScroll = targetInline ; } else if ( inline === 'center' ) { inlineScroll = targetInline - viewportWidth / 2 ; } else if ( inline === 'end' ) { inlineScroll = targetInline - viewportWidth ; } else { inlineScroll = alignNearest ( viewportX , viewportX + viewportWidth , viewportWidth , borderLeft , borderRight , viewportX + targetInline , viewportX + targetInline + targetWidth , targetWidth ) ; } blockScroll = Math . max ( 0 , blockScroll + viewportY ) ; inlineScroll = Math . max ( 0 , inlineScroll + viewportX ) ; } else { if ( block === 'start' ) { blockScroll = targetBlock - top - borderTop ; } else if ( block === 'end' ) { blockScroll = targetBlock - bottom + borderBottom + scrollbarHeight ; } else if ( block === 'nearest' ) { blockScroll = alignNearest ( top , bottom , height , borderTop , borderBottom + scrollbarHeight , targetBlock , targetBlock + targetHeight , targetHeight ) ; } else { blockScroll = targetBlock - ( top + height / 2 ) + scrollbarHeight / 2 ; } if ( inline === 'start' ) { inlineScroll = targetInline - left - borderLeft ; } else if ( inline === 'center' ) { inlineScroll = targetInline - ( left + width / 2 ) + scrollbarWidth / 2 ; } else if ( inline === 'end' ) { inlineScroll = targetInline - right + borderRight + scrollbarWidth ; } else { inlineScroll = alignNearest ( left , right , width , borderLeft , borderRight + scrollbarWidth , targetInline , targetInline + targetWidth , targetWidth ) ; } const { scrollLeft , scrollTop } = frame ; blockScroll = Math . max ( 0 , Math . min ( scrollTop + blockScroll , frame . scrollHeight - height + scrollbarHeight ) ) ; inlineScroll = Math . max ( 0 , Math . min ( scrollLeft + inlineScroll , frame . scrollWidth - width + scrollbarWidth ) ) ; targetBlock += scrollTop - blockScroll ; targetInline += scrollLeft - inlineScroll ; } computations . push ( { el : frame , top : blockScroll , left : inlineScroll } ) ; } return computations ; } ;