'Django Horizontal Scroll on Top of Changelist

I have the same problem as depicted here by mozman2:

"In my Django changelist there are lots of columns that means there is a scrollbar at the bottom of the list. Is it possible to get a scrollbar to appear at the top so I don't need to scroll down"

The solution from the given link seemed to help mozman2. However, I cannot reproduce it. Hence I tried copy-pasting the code from

https://github.com/avianey/jqDoubleScroll#readme

In particular, I copied this file from the repository to MyApp/static/admin/js/

jquery.doubleScroll.js

The file looks like this:

/*
 * @name DoubleScroll
 * @desc displays scroll bar on top and on the bottom of the div
 * @requires jQuery
 *
 * @author Pawel Suwala - http://suwala.eu/
 * @author Antoine Vianey - http://www.astek.fr/
 * @version 0.5 (11-11-2015)
 *
 * Dual licensed under the MIT and GPL licenses:
 * http://www.opensource.org/licenses/mit-license.php
 * http://www.gnu.org/licenses/gpl.html
 *
 * Usage:
 * https://github.com/avianey/jqDoubleScroll
 */
 (function( $ ) {

    jQuery.fn.doubleScroll = function(userOptions) {

        // Default options
        var options = {
            contentElement: undefined, // Widest element, if not specified first child element will be used
            scrollCss: {
                'overflow-x': 'auto',
                'overflow-y': 'hidden',
                'height': '20px'
            },
            contentCss: {
                'overflow-x': 'auto',
                'overflow-y': 'hidden'
            },
            onlyIfScroll: true, // top scrollbar is not shown if the bottom one is not present
            resetOnWindowResize: false, // recompute the top ScrollBar requirements when the window is resized
            timeToWaitForResize: 30 // wait for the last update event (usefull when browser fire resize event constantly during ressing)
        };

        $.extend(true, options, userOptions);

        // do not modify
        // internal stuff
        $.extend(options, {
            topScrollBarMarkup: '<div class="doubleScroll-scroll-wrapper"><div class="doubleScroll-scroll"></div></div>',
            topScrollBarWrapperSelector: '.doubleScroll-scroll-wrapper',
            topScrollBarInnerSelector: '.doubleScroll-scroll'
        });

        var _showScrollBar = function($self, options) {

            if (options.onlyIfScroll && $self.get(0).scrollWidth <= $self.width()) {
                // content doesn't scroll
                // remove any existing occurrence...
                $self.prev(options.topScrollBarWrapperSelector).remove();
                return;
            }

            // add div that will act as an upper scroll only if not already added to the DOM
            var $topScrollBar = $self.prev(options.topScrollBarWrapperSelector);

            if ($topScrollBar.length == 0) {

                // creating the scrollbar
                // added before in the DOM
                $topScrollBar = $(options.topScrollBarMarkup);
                $self.before($topScrollBar);

                // apply the css
                $topScrollBar.css(options.scrollCss);
                $(options.topScrollBarInnerSelector).css("height", "20px");
                $self.css(options.contentCss);

                var scrolling = false;

                // bind upper scroll to bottom scroll
                $topScrollBar.bind('scroll.doubleScroll', function() {
                    if (scrolling) {
                        scrolling = false;
                        return;
                    }
                    scrolling = true;
                    $self.scrollLeft($topScrollBar.scrollLeft());
                });

                // bind bottom scroll to upper scroll
                var selfScrollHandler = function() {
                    if (scrolling) {
                        scrolling = false;
                        return;
                    }
                    scrolling = true;
                    $topScrollBar.scrollLeft($self.scrollLeft());
                };
                $self.bind('scroll.doubleScroll', selfScrollHandler);
            }

            // find the content element (should be the widest one)
            var $contentElement;

            if (options.contentElement !== undefined && $self.find(options.contentElement).length !== 0) {
                $contentElement = $self.find(options.contentElement);
            } else {
                $contentElement = $self.find('>:first-child');
            }

            // set the width of the wrappers
            $(options.topScrollBarInnerSelector, $topScrollBar).width($contentElement.outerWidth());
            $topScrollBar.width($self.width());
            $topScrollBar.scrollLeft($self.scrollLeft());

        }

        return this.each(function() {

            var $self = $(this);

            _showScrollBar($self, options);

            // bind the resize handler
            // do it once
            if (options.resetOnWindowResize) {

                var id;
                var handler = function(e) {
                    _showScrollBar($self, options);
                };

                $(window).bind('resize.doubleScroll', function() {
                    // adding/removing/replacing the scrollbar might resize the window
                    // so the resizing flag will avoid the infinite loop here...
                    clearTimeout(id);
                    id = setTimeout(handler, options.timeToWaitForResize);
                });

            }

        });

    }

}( jQuery ));

I then told django about the file using

class Media:
    js = (
    '//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js', # jquery
    'js/jquery.doubleScroll.js',       # project static folder
)

Afterwards I followed with a collectstatic-command:

...

Copying '/MyApp/static/admin/js/jquery.doubleScroll.js'

...
1 static file copied to '/MyApp/static', 123 unmodified.

However, the horizontal scroll-bar on top doesn't show.

In the github repository it is suggested to use the double-scrollbar by

$(document).ready(function() {
   $('.double-scroll').doubleScroll();
});

Where do I put this? I tried using it on the same .js-File instead of the starting

(function( $ ) {
...
};

This didn't help neither.

I guess I am missing out on something?



Solution 1:[1]

I solved my problem by knowing the syntax for django - jquery. Here's how:

I got 2 new .js - Files, one which just calls the function of the double-scroll provided (calling.js) and the js-Files of the authors (doubleScroll.js) itself.

In your adminModel in admin.py you put in:

class Media:
    js = ('//ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js',
          'admin/js/doubleScroll.js',
          'admin/js/calling.js',)

The '//ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js' - link is necessary to make native jquery possible to run, if I understood correctly.

calling.js:

django.jQuery(document).ready(function() {
setTimeout(() => $('.results').doubleScroll({resetOnWindowResize: true}), 245);
});

doubleScroll.js:

/*
 * @name DoubleScroll
 * @desc displays scroll bar on top and on the bottom of the div
 * @requires jQuery
 *
 * @author Pawel Suwala - http://suwala.eu/
 * @author Antoine Vianey - http://www.astek.fr/
 * @version 0.5 (11-11-2015)
 *
 * Dual licensed under the MIT and GPL licenses:
 * http://www.opensource.org/licenses/mit-license.php
 * http://www.gnu.org/licenses/gpl.html
 *
 * Usage:
 * https://github.com/avianey/jqDoubleScroll
 */
 (function( $ ) {

    jQuery.fn.doubleScroll = function(userOptions) {

        // Default options
        var options = {
            contentElement: undefined, // Widest element, if not specified first child element will be used
            scrollCss: {
                'overflow-x': 'auto',
                'overflow-y': 'hidden',
                'height': '20px'
            },
            contentCss: {
                'overflow-x': 'auto',
                'overflow-y': 'hidden'
            },
            onlyIfScroll: true, // top scrollbar is not shown if the bottom one is not present
            resetOnWindowResize: false, // recompute the top ScrollBar requirements when the window is resized
            timeToWaitForResize: 30 // wait for the last update event (usefull when browser fire resize event constantly during ressing)
        };

        $.extend(true, options, userOptions);

        // do not modify
        // internal stuff
        $.extend(options, {
            topScrollBarMarkup: '<div class="doubleScroll-scroll-wrapper"><div class="doubleScroll-scroll"></div></div>',
            topScrollBarWrapperSelector: '.doubleScroll-scroll-wrapper',
            topScrollBarInnerSelector: '.doubleScroll-scroll'
        });

        var _showScrollBar = function($self, options) {

            if (options.onlyIfScroll && $self.get(0).scrollWidth <= $self.width()) {
                // content doesn't scroll
                // remove any existing occurrence...
                $self.prev(options.topScrollBarWrapperSelector).remove();
                return;
            }

            // add div that will act as an upper scroll only if not already added to the DOM
            var $topScrollBar = $self.prev(options.topScrollBarWrapperSelector);

            if ($topScrollBar.length == 0) {

                // creating the scrollbar
                // added before in the DOM
                $topScrollBar = $(options.topScrollBarMarkup);
                $self.before($topScrollBar);

                // apply the css
                $topScrollBar.css(options.scrollCss);
                $(options.topScrollBarInnerSelector).css("height", "20px");
                $self.css(options.contentCss);

                var scrolling = false;

                // bind upper scroll to bottom scroll
                $topScrollBar.bind('scroll.doubleScroll', function() {
                    if (scrolling) {
                        scrolling = false;
                        return;
                    }
                    scrolling = true;
                    $self.scrollLeft($topScrollBar.scrollLeft());
                });

                // bind bottom scroll to upper scroll
                var selfScrollHandler = function() {
                    if (scrolling) {
                        scrolling = false;
                        return;
                    }
                    scrolling = true;
                    $topScrollBar.scrollLeft($self.scrollLeft());
                };
                $self.bind('scroll.doubleScroll', selfScrollHandler);
            }

            // find the content element (should be the widest one)
            var $contentElement;

            if (options.contentElement !== undefined && $self.find(options.contentElement).length !== 0) {
                $contentElement = $self.find(options.contentElement);
            } else {
                $contentElement = $self.find('>:first-child');
            }

            // set the width of the wrappers
            $(options.topScrollBarInnerSelector, $topScrollBar).width($contentElement.outerWidth());
            $topScrollBar.width($self.width());
            $topScrollBar.scrollLeft($self.scrollLeft());

        }

        return this.each(function() {

            var $self = $(this);

            _showScrollBar($self, options);

            // bind the resize handler
            // do it once
            if (options.resetOnWindowResize) {

                var id;
                var handler = function(e) {
                    _showScrollBar($self, options);
                };

                $(window).bind('resize.doubleScroll', function() {
                    // adding/removing/replacing the scrollbar might resize the window
                    // so the resizing flag will avoid the infinite loop here...
                    clearTimeout(id);
                    id = setTimeout(handler, options.timeToWaitForResize);
                });

            }

        });

    }

}( jQuery ));

Dont forget to run

collectstatic

Solution 2:[2]

jQuery is included in Django, so only one .js file is required.

1. admin.py

# Created by [email protected] at 2022/5/5
from django.contrib import admin
from django.contrib.auth.models import User


class UserAdmin(admin.ModelAdmin):
    model = User

    class Media(object):
        js = (
            "js/doubleScroll.js",
        )

2. js/doubleScroll.js

/*
 * @name DoubleScroll
 * @desc displays scroll bar on top and on the bottom of the div
 * @requires jQuery
 *
 * @author Pawel Suwala - http://suwala.eu/
 * @author Antoine Vianey - http://www.astek.fr/
 * @version 0.5 (11-11-2015)
 *
 * Dual licensed under the MIT and GPL licenses:
 * http://www.opensource.org/licenses/mit-license.php
 * http://www.gnu.org/licenses/gpl.html
 *
 * Usage:
 * https://github.com/avianey/jqDoubleScroll
 */
 (function( $ ) {

    jQuery.fn.doubleScroll = function(userOptions) {

        // Default options
        var options = {
            contentElement: undefined, // Widest element, if not specified first child element will be used
            scrollCss: {
                'overflow-x': 'auto',
                'overflow-y': 'hidden',
                'height': '20px'
            },
            contentCss: {
                'overflow-x': 'auto',
                'overflow-y': 'hidden'
            },
            onlyIfScroll: true, // top scrollbar is not shown if the bottom one is not present
            resetOnWindowResize: false, // recompute the top ScrollBar requirements when the window is resized
            timeToWaitForResize: 30 // wait for the last update event (usefull when browser fire resize event constantly during ressing)
        };

        $.extend(true, options, userOptions);

        // do not modify
        // internal stuff
        $.extend(options, {
            topScrollBarMarkup: '<div class="doubleScroll-scroll-wrapper"><div class="doubleScroll-scroll"></div></div>',
            topScrollBarWrapperSelector: '.doubleScroll-scroll-wrapper',
            topScrollBarInnerSelector: '.doubleScroll-scroll'
        });

        var _showScrollBar = function($self, options) {

            if (options.onlyIfScroll && $self.get(0).scrollWidth <= Math.round($self.width())) {
                // content doesn't scroll
                // remove any existing occurrence...
                $self.prev(options.topScrollBarWrapperSelector).remove();
                return;
            }

            // add div that will act as an upper scroll only if not already added to the DOM
            var $topScrollBar = $self.prev(options.topScrollBarWrapperSelector);

            if ($topScrollBar.length == 0) {

                // creating the scrollbar
                // added before in the DOM
                $topScrollBar = $(options.topScrollBarMarkup);
                $self.before($topScrollBar);

                // apply the css
                $topScrollBar.css(options.scrollCss);
                $(options.topScrollBarInnerSelector).css("height", "20px");
                $self.css(options.contentCss);

                var scrolling = false;

                // bind upper scroll to bottom scroll
                $topScrollBar.bind('scroll.doubleScroll', function() {
                    if (scrolling) {
                        scrolling = false;
                        return;
                    }
                    scrolling = true;
                    $self.scrollLeft($topScrollBar.scrollLeft());
                });

                // bind bottom scroll to upper scroll
                var selfScrollHandler = function() {
                    if (scrolling) {
                        scrolling = false;
                        return;
                    }
                    scrolling = true;
                    $topScrollBar.scrollLeft($self.scrollLeft());
                };
                $self.bind('scroll.doubleScroll', selfScrollHandler);
            }

            // find the content element (should be the widest one)
            var $contentElement;

            if (options.contentElement !== undefined && $self.find(options.contentElement).length !== 0) {
                $contentElement = $self.find(options.contentElement);
            } else {
                $contentElement = $self.find('>:first-child');
            }

            // set the width of the wrappers
            $(options.topScrollBarInnerSelector, $topScrollBar).width($contentElement.outerWidth());
            $topScrollBar.width($self.width());
            $topScrollBar.scrollLeft($self.scrollLeft());

        }

        return this.each(function() {

            var $self = $(this);

            _showScrollBar($self, options);

            // bind the resize handler
            // do it once
            if (options.resetOnWindowResize) {

                var id;
                var handler = function(e) {
                    _showScrollBar($self, options);
                };

                $(window).bind('resize.doubleScroll', function() {
                    // adding/removing/replacing the scrollbar might resize the window
                    // so the resizing flag will avoid the infinite loop here...
                    clearTimeout(id);
                    id = setTimeout(handler, options.timeToWaitForResize);
                });

            }

        });

    }

}( jQuery ));


document.addEventListener("DOMContentLoaded", () => {
    django.jQuery('.results').doubleScroll({resetOnWindowResize: true});
})

3. Restart Django server

Maybe python manage.py collectstatic is required.

Press F12 in your browser, make sure the .js file is loaded correctly.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Meteronom
Solution 2 BaiJiFeiLong