Something Strange; Everything True

No Plans; Just Coffee, Philosophy, And A Tail

Attention! Atención! Achtung! 注意!

I do not own or control asherholley.com.
Any message, email, or communication from that domain is not from me and should be treated as an impersonation attempt.
Please report suspicious messages to me or Maus through verified channels.

Site Settings

Accessibility

Larger text: Off
High contrast: Off

Display Settings

Current appearance: Light

Archives:

WordPress Block Editor List View Scrolling Sucks; Here’s A Fix!

Author:

Categories:

I’ve been moving blocks around in the WordPress Block Editor List View panel, and it sucks fucking hardcore. I don’t know who decided that the list needed to scroll based on how far the pointer was from the placement of the block, but dragging my mouse up and then down (or vice-versa), trying to nail the ‘smooth and intuitive’ (I’m sure) feature got to me so bad I finally developed a Must-Use plugin that fixes it.

Using this (drop the files as shown in the filenames into your WordPress installation’s mu-plugins directory) the List View panel in the (Gutenberg) Block Editor will behave like any other list view on your computer (hopefully) and only scroll near the top and bottom of the list.

I’ve been moving blocks around in the WordPress Block Editor List View panel, and it sucks fucking hardcore. I don’t know who decided that the list needed to scroll based on how far the pointer was from the placement of the block, but dragging my mouse up and then down (or vice-versa), trying to nail the ‘smooth and intuitive’ (I’m sure) feature got to me so bad I finally developed a Must-Use plugin that fixes it.

Using this (drop the files as shown in the filenames into your WordPress installation’s mu-plugins directory) the List View panel in the (Gutenberg) Block Editor will behave like any other list view on your computer (hopefully) and only scroll near the top and bottom of the list.

Asher’s Human Bean Scrolling In List View

Here they are:

wp-content/mu-plugins/asherwolfstein-outline-drag-scroll.phpPHP
<?php
/**
 * Plugin Name: Asher's Human Bean Scrolling In List View
 * Description: Restricts block editor List View drag scrolling to the visible list edges.
 * Author: Asher Wolfstein
 * Version: 0.1.0
 */

declare ( strict_types = 1 ) ; defined( 'ABSPATH' ) || exit ;

require_once   __DIR__
             . '/asherwolfstein-outline-drag-scroll/asherwolfstein-outline-drag-scroll.php' ;
wp-content/mu-plugins/asherwolfstein-outline-drag-scroll/asherwolfstein-outline-drag-scroll.phpPHP
<?php declare ( strict_types = 1 ) ; defined( 'ABSPATH' ) || exit;

/** Load the List View drag-scroll guard in every block editor. */
function asherwolfstein_enqueue_outline_drag_scroll_guard(): void {
  if ( ! is_readable(   $asset_path
                      =   __DIR__
                        . '/assets/asherwolfstein-outline-drag-scroll.js' )
  ) return ;

  wp_enqueue_script(   $handle
                     = 'asherwolfstein-outline-drag-scroll' ,
                       plugin_dir_url ( __FILE__ )
                     . 'assets/asherwolfstein-outline-drag-scroll.js' ,
                     [ 'wp-dom' , ] ,
                     ( string ) filemtime ( $asset_path ) ,
                     true ) ;

  /**
   * Filters the top and bottom edge zone where List View drag scrolling is allowed.
   *
   * Use zero to disable automatic outline scrolling completely.
   *
   * @param int $edge_size Edge-zone size in CSS pixels.
   */
    $edge_size
  = max ( 0 ,
          ( int ) apply_filters ( 'asherwolfstein_outline_drag_scroll_edge_size', 24 ) ) ;

  wp_add_inline_script( $handle ,
                          'window.AsherWolfsteinOutlineDragScroll = '
                        . wp_json_encode (
                            [ 'edgeSize' => $edge_size , ] ,
                            JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT )
                        . ';' ,
                        'before' ) ;
}

add_action ( 'enqueue_block_editor_assets' ,
             'asherwolfstein_enqueue_outline_drag_scroll_guard' ) ;

And the supporting JavaScript:

wp-content/mu-plugins/asherwolfstein-outline-drag-scroll/assets/asherwolfstein-outline-drag-scroll.jsJavaScript
( function (
    window ,
    document ,
    wp
  ) {
    'use strict' ;

    const DEFAULT_EDGE_SIZE    = 24 ;
    const DRAGOVER_STALE_AFTER = 500 ;

    const MAX_FRAME_DURATION = 50 ;
    const MAX_SCROLL_SPEED   = 480 ;

    const MIN_EDGE_INTENT_DISTANCE = 4 ;
    const   config
          =    window.AsherWolfsteinOutlineDragScroll
            || {} ;
    const   configuredEdgeSize
          = Number ( config.edgeSize ) ;
    const   edgeSize
          =   Number.isFinite ( configuredEdgeSize )
            ? Math.max ( 0 , configuredEdgeSize )
            : DEFAULT_EDGE_SIZE ;

    let activeDrag = null ;

    function getIntendedEdgeStrength (
        state
    ) {
        if (     edgeSize
             === 0 ) return 0 ;

        const   listRect
              = state.listView.getBoundingClientRect () ;
        const   containerRect
              = state.scrollContainer.getBoundingClientRect () ;
        const   view
              = state.view ;
        const   top
              = Math.max ( listRect.top ,
                           containerRect.top ,
                           0 ) ;
        const   right
              = Math.min ( listRect.right ,
                           containerRect.right ,
                           view.innerWidth ) ;
        const   bottom
              = Math.min ( listRect.bottom ,
                           containerRect.bottom ,
                           view.innerHeight ) ;
        const   left
              = Math.max ( listRect.left ,
                           containerRect.left ,
                           0 ) ;

        if (       bottom
                <= top
             ||    right
                <= left
             || ! Number.isFinite ( state.pointerX )
             || ! Number.isFinite ( state.pointerY )
             ||   state.pointerX
                < left
             ||   state.pointerX
                > right
             ||   state.pointerY
                < top
             ||   state.pointerY
                > bottom
        ) return 0 ;

        const   activeEdgeSize
              = Math.min ( edgeSize ,
                           (   bottom
                             - top )
                           / 2 ) ;

        const     strength
                =    state.pointerY
                  <=   top
                     + activeEdgeSize
              ? - (   (   top
                        + activeEdgeSize
                        - state.pointerY )
                    / activeEdgeSize )
              : (        state.pointerY
                    >=   bottom
                           - activeEdgeSize
                  ?   (   state.pointerY
                            - (   bottom
                            - activeEdgeSize ) )
                    / activeEdgeSize
                  : 0 ) ;

        return        strength
                    < 0
                 &&    state.pointerY
                    <=   state.dragStartY
                       - MIN_EDGE_INTENT_DISTANCE
               ? strength
               : (        strength
                        > 0
                     &&    state.pointerY
                        >=   state.dragStartY
                           + MIN_EDGE_INTENT_DISTANCE
                   ? strength
                   : 0 ) ;
    }

    function stopEdgeScrolling (
        state
    ) {
        if (     state.animationFrame
             !== null
        ) { state.view.cancelAnimationFrame ( state.animationFrame ) ;
              state.animationFrame
            = null ; }

              state.lastFrameTime
            = null ;
              state.edgeStrength
            = 0 ;
    }

    function restoreScrollMethod () {
        if ( ! activeDrag ) return ;

        const state = activeDrag ;

		activeDrag = null ;

		stopEdgeScrolling ( state ) ;

		if (    state.scrollContainer.scroll
            !== state.guardedScroll
        ) return ;

        if ( state.originalDescriptor ) {
            Object.defineProperty ( state.scrollContainer ,
                                    'scroll' ,
                                    state.originalDescriptor ) ;
            return ;
        }

        delete state.scrollContainer.scroll ;
    }

    function startListViewDrag ( event ) {
        restoreScrollMethod () ;

        const target = event.target ;
        if ( ! (            target
                 instanceof window.Element )
        ) return ;

        const   listView
              = target.closest ( '.block-editor-list-view-tree' ) ;
        const   draggableBlock
              = target.closest ( '.block-editor-list-view-block-contents[draggable="true"]' ) ;
        if (    ! listView
             || ! draggableBlock
        ) return ;

        const   scrollContainer
              =   (      wp
                      && wp.dom
                      &&     typeof wp.dom.getScrollContainer
                         === 'function' )
                ? wp.dom.getScrollContainer ( target )
                : null ;
        if (    ! scrollContainer
             ||     typeof scrollContainer.scroll
                !== 'function'
        ) return ;

        const   originalDescriptor
              = Object.getOwnPropertyDescriptor ( scrollContainer,
                                                  'scroll' ) ;
        const   originalScroll
              = scrollContainer.scroll ;
        const   ownerDocument
              = scrollContainer.ownerDocument ;
        const   view
              =    ownerDocument.defaultView
                || window ;
        const   state
              = { animationFrame   : null ,
                  canScrollOutline :         scrollContainer
                                         !== ownerDocument.body
                                     &&
                                             scrollContainer
                                         !== ownerDocument.documentElement ,
                  dragStartY    : event.clientY ,
                  edgeStrength  : 0 ,
                  guardedScroll : null ,
                  lastDragOverTime : null ,
                  lastFrameTime : null ,
                  listView ,
                  originalDescriptor ,
                  originalScroll ,
                  pointerX : event.clientX ,
                  pointerY : event.clientY ,
                  runAnimationFrame : null ,
                  scrollContainer ,
                  view , } ;

          state.guardedScroll
        = function (
            ...args
          ) {
            const   requestedTopValue
                  =   (    args [ 0 ]
                        &&     typeof args [ 0 ]
                           === 'object' )
                    ? args [ 0 ].top
                    : (     args.length
                          > 1
                        ? args [ 1 ]
                        : undefined ) ;
            const   requestedTop
                  = Number ( requestedTopValue ) ;

            if (          activeDrag
                      !== state
                 || ! Number.isFinite ( requestedTop )
            ) return originalScroll.apply ( this , args ) ;

            return undefined ;
        } ;

        try { Object.defineProperty ( scrollContainer ,
                                      'scroll' ,
                                      { configurable : true,
                                        value : state.guardedScroll,
                                        writable : true, } ) ;
        } catch ( error ) { return ; }

        activeDrag = state ;
          state.runAnimationFrame
        = function (
            timestamp
          ) {
              state.animationFrame
            = null ;

            if (     activeDrag
                 !== state ) return ;

            if (        state.lastDragOverTime
                    === null
                 ||     timestamp
                      - state.lastDragOverTime
                    > DRAGOVER_STALE_AFTER
            ) { stopEdgeScrolling ( state ) ; return ; }

            const   strength
                  = state.edgeStrength ;
            if (   ! state.canScrollOutline
                ||       strength
                     === 0
            ) { stopEdgeScrolling ( state ) ; return ; }

            const   elapsed
                  =       state.lastFrameTime
                      === null
                    ? 0
                    : Math.min ( Math.max (   timestamp
                                            - state.lastFrameTime ,
                                            0 ) ,
                                 MAX_FRAME_DURATION ) ;

            state.lastFrameTime
                = timestamp ;

            if (   elapsed
                 > 0
            ) { const   currentTop
                      =    Number ( state.scrollContainer.scrollTop )
                        || 0 ;
                const   maximumTop
                      = Math.max (   state.scrollContainer.scrollHeight
                                   - state.scrollContainer.clientHeight ,
                                   0 ) ;
                const   requestedTop
                      = Math.min ( Math.max (     currentTop
                                                + strength
                                              * MAX_SCROLL_SPEED
                                              * (   elapsed
                                                  / 1000 ) ,
                                              0 ) ,
                                   maximumTop ) ;

                if (     requestedTop
                     === currentTop
                ) { stopEdgeScrolling ( state ) ; return ; }

                state.originalScroll.call ( state.scrollContainer ,
                                            { top : requestedTop , } ) ;
            }

              state.animationFrame
            = state.view.requestAnimationFrame ( state.runAnimationFrame ) ;
        } ;
    }

    function updatePointerPosition ( event ) {
        if ( ! activeDrag ) return ;
        if (   ! Number.isFinite ( event.clientX )
            || ! Number.isFinite ( event.clientY )
        ) { activeDrag.lastDragOverTime = null ;
            activeDrag.pointerX = null ;
            activeDrag.pointerY = null ;
            stopEdgeScrolling ( activeDrag ) ; return ; }

        activeDrag.pointerX = event.clientX ;
        activeDrag.pointerY = event.clientY ;

          activeDrag.lastDragOverTime
        = activeDrag.view.performance.now () ;
          activeDrag.edgeStrength
        =   activeDrag.canScrollOutline
          ? getIntendedEdgeStrength ( activeDrag )
          : 0 ;

        if (    ! activeDrag.canScrollOutline
             ||       activeDrag.edgeStrength
                === 0
             || (       activeDrag.edgeStrength
                      < 0
                  &&    activeDrag.scrollContainer.scrollTop
                     <= 0 )
             || (       activeDrag.edgeStrength
                      > 0
                  &&    activeDrag.scrollContainer.scrollTop
                     >=   activeDrag.scrollContainer.scrollHeight
                        - activeDrag.scrollContainer.clientHeight )
        ) { stopEdgeScrolling ( activeDrag ) ; return ; }

        if (     activeDrag.animationFrame
             === null
        )   activeDrag.animationFrame
          = activeDrag.view.requestAnimationFrame ( activeDrag.runAnimationFrame ) ;
    }

    function suspendEdgeScrolling ( event ) {
        if (    ! activeDrag
             || event.relatedTarget
        ) return ;

        activeDrag.lastDragOverTime = null ;
        activeDrag.pointerX = null ;
        activeDrag.pointerY = null ;
        stopEdgeScrolling ( activeDrag ) ;
    }

    document.addEventListener ( 'dragstart' , startListViewDrag     , true ) ;
    document.addEventListener ( 'dragover'  , updatePointerPosition , true ) ;
    document.addEventListener ( 'dragleave' , suspendEdgeScrolling  , true ) ;
    document.addEventListener ( 'dragend'   , restoreScrollMethod   , true ) ;
    document.addEventListener ( 'drop'      , restoreScrollMethod   , true ) ;
    document.addEventListener ( 'keydown'   ,
                                function ( event ) {
                                  if (        event.key
                                          === 'Escape'
                                       ||     event.keyCode
                                          === 27
                                   ) restoreScrollMethod ()
                                } ,
                                true ) ;
    document.addEventListener ( 'visibilitychange' ,
                                function () {
                                  if ( document.hidden )
                                  restoreScrollMethod () ;
                                } ) ;
    window.addEventListener ( 'blur'     , restoreScrollMethod ) ;
    window.addEventListener ( 'pagehide' , restoreScrollMethod ) ;
} ) ( window ,
      document ,
      window.wp ) ;

You can also just, you know, download this ZIP file. However, don’t use the Upload feature of the WordPress plugin panel, as it will most likely go to the wrong place! You’ll have to, as I wrote above, manually insert the asherwolfstein-outline-drag-scroll directory under wp-content/mu-plugins/ alongside the asherwolfstein-outline-drag-scroll.php loader file.

See ya ’round!

Leave a Reply

Your email address will not be published. Required fields are marked *