Steve Howell

(this article is still under construction)

2018.

Those were simpler times.

padded widget

Consider the following piece of code that still exists in the Zulip codebase as of 2026.

import $ from "jquery";

export function update_padding(opts: {
    content_selector: string;
    padding_selector: string;
    total_rows: number;
    shown_rows: number;
}): void {
    const $content = $(opts.content_selector);
    const $padding = $(opts.padding_selector);
    const total_rows = opts.total_rows;
    const shown_rows = opts.shown_rows;
    const hidden_rows = total_rows - shown_rows;

    if (shown_rows === 0) {
        $padding.height(0);
        return;
    }

    const ratio = hidden_rows / shown_rows;

    const content_height = $content.height();
    if (content_height === undefined) {
        return;
    }

    const new_padding_height = ratio * content_height;

    $padding.height(new_padding_height);
    $padding.width(1);
}

This code seems pretty simple on the surface. It makes a simple geometry calculation and updates some kind of padding-related DOM element (via jQuery) to have a new height and width. I will put that into context later.

It’s written in TypeScript.

I actually wrote the initial version of this function not only before Zulip used TypeScript but also before Zulip used ES6.

But the essential code survives.

You can see my initial version of padded_widget.js below. It’s the same code apart from missing some syntax sugar and explicit type definitions:

var padded_widget = (function () {

var exports = {};

exports.update_padding = function (opts) {
    var content = $(opts.content_sel);
    var padding = $(opts.padding_sel);
    var total_rows = opts.total_rows;
    var shown_rows = opts.shown_rows;
    var hidden_rows = total_rows - shown_rows;

    if (shown_rows === 0) {
        padding.height(0);
        return;
    }

    var ratio = hidden_rows / shown_rows;

    var content_height = content.height();
    var new_padding_height = ratio * content_height;

    padding.height(new_padding_height);
    padding.width(1);
};


return exports;

}());
if (typeof module !== 'undefined') {
    module.exports = padded_widget;
}
window.padded_widget = padded_widget;

The context of the above code is that we needed to progressively render our buddy list. At the time (2018), it was considered too expensive to render the entire contents of a buddy list until the user scrolled to the bottom of the list. We sometimes had up to 20k users on a single realm, so there could indeed be lots to render.

In order to make progressive scrolling not mess with the scrollbar, we wanted a little off-screen part of the buddy list to be the “padding” that the user couldn’t see, but which made the scrollbar not put the thumb bar at the wrong place.

The above code was the gist of the solution, but you also needed the list itself to have some extra logic.

That logic resided in buddy_list.js. Here is the 2018 version for full context. Again, please excuse the 2018 lack of syntax sugar and explicit typing:

/* eslint indent: "off" */

var buddy_list = (function () {
    var self = {};

    self.container_sel = '#user_presences';
    self.scroll_container_sel = '#buddy_list_wrapper';
    self.item_sel = 'li.user_sidebar_entry';
    self.padding_sel = '#buddy_list_wrapper_padding';

    self.items_to_html = function (opts) {
        var user_info = opts.items;
        var html = templates.render('user_presence_rows', {users: user_info});
        return html;
    };

    self.item_to_html = function (opts) {
        var html = templates.render('user_presence_row', opts.item);
        return html;
    };

    self.get_li_from_key = function (opts) {
        var user_id = opts.key;
        var sel = self.item_sel + "[data-user-id='" + user_id + "']";
        return self.container.find(sel);
    };

    self.get_key_from_li = function (opts) {
        var user_id = opts.li.expectOne().attr('data-user-id');
        return user_id;
    };

    self.get_data_from_keys = function (opts) {
        var keys = opts.keys;
        var data = buddy_data.get_items_for_users(keys);
        return data;
    };

    self.compare_function = buddy_data.compare_function;

    self.height_to_fill = function () {
        // Because the buddy list gets sized dynamically, we err on the side
        // of using the height of the entire viewport for deciding
        // how much content to render.  Even on tall monitors this should
        // still be a significant optimization for orgs with thousands of
        // users.
        var height = message_viewport.height();
        return height;
    };

    // Try to keep code below this line generic, so that we can
    // extract a widget.

    self.keys = [];

    self.populate = function (opts) {
        self.render_count = 0;
        self.container.html('');

        // We rely on our caller to give us items
        // in already-sorted order.
        self.keys = _.map(opts.keys, function (k) {
            return k.toString();
        });

        self.fill_screen_with_content();
    };

    self.render_more = function (opts) {
        var chunk_size = opts.chunk_size;

        var begin = self.render_count;
        var end = begin + chunk_size;

        var more_keys = self.keys.slice(begin, end);

        if (more_keys.length === 0) {
            return;
        }

        var items = self.get_data_from_keys({
            keys: more_keys,
        });

        var html = self.items_to_html({
            items: items,
        });
        self.container = $(self.container_sel);
        self.container.append(html);

        // Invariant: more_keys.length >= items.length.
        // (Usually they're the same, but occasionally keys
        // won't return valid items.  Even though we don't
        // actually render these keys, we still "count" them
        // as rendered.

        self.render_count += more_keys.length;
        self.update_padding();
    };

    self.get_items = function () {
        var obj = self.container.find(self.item_sel);
        return obj.map(function (i, elem) {
            return $(elem);
        });
    };

    self.first_key = function () {
        return self.keys[0];
    };

    self.prev_key = function (key) {
        var i = self.keys.indexOf(key.toString());

        if (i <= 0) {
            return;
        }

        return self.keys[i - 1];
    };

    self.next_key = function (key) {
        var i = self.keys.indexOf(key.toString());

        if (i < 0) {
            return;
        }

        return self.keys[i + 1];
    };

    self.maybe_remove_key = function (opts) {
        var pos = self.keys.indexOf(opts.key);

        if (pos < 0) {
            return;
        }

        self.keys.splice(pos, 1);

        if (pos < self.render_count) {
            self.render_count -= 1;
            var li = self.find_li({key: opts.key});
            li.remove();
            self.update_padding();
        }
    };

    self.find_position = function (opts) {
        var key = opts.key;
        var i;

        for (i = 0; i < self.keys.length; i += 1) {
            var list_key = self.keys[i];

            if (self.compare_function(key, list_key) < 0) {
                return i;
            }
        }

        return self.keys.length;
    };

    self.force_render = function (opts) {
        var pos = opts.pos;

        // Try to render a bit optimistically here.
        var cushion_size = 3;
        var chunk_size = pos + cushion_size - self.render_count;

        if (chunk_size <= 0) {
            blueslip.error('cannot show key at this position: ' + pos);
        }

        self.render_more({
            chunk_size: chunk_size,
        });
    };

    self.find_li = function (opts) {
        var key = opts.key.toString();

        // Try direct DOM lookup first for speed.
        var li = self.get_li_from_key({
            key: key,
        });

        if (li.length === 1) {
            return li;
        }

        if (!opts.force_render) {
            // Most callers don't force us to render a list
            // item that wouldn't be on-screen anyway.
            return li;
        }

        var pos = self.keys.indexOf(key);

        if (pos < 0) {
            // TODO: See list_cursor.get_row() for why this is
            //       a bit janky now.
            return [];
        }

        self.force_render({
            pos: pos,
        });

        li = self.get_li_from_key({
            key: key,
        });

        return li;
    };

    self.insert_new_html = function (opts) {
        var other_key = opts.other_key;
        var html = opts.html;
        var pos = opts.pos;

        if (other_key === undefined) {
            if (pos === self.render_count) {
                self.render_count += 1;
                self.container.append(html);
                self.update_padding();
            }
            return;
        }

        if (pos < self.render_count) {
            self.render_count += 1;
            var li = self.find_li({key: other_key});
            li.before(html);
            self.update_padding();
        }
    };

    self.insert_or_move = function (opts) {
        var key = opts.key.toString();
        var item = opts.item;

        self.maybe_remove_key({key: key});

        var pos = self.find_position({
            key: key,
        });

        // Order is important here--get the other_key
        // before mutating our list.  An undefined value
        // corresponds to appending.
        var other_key = self.keys[pos];

        self.keys.splice(pos, 0, key);

        var html = self.item_to_html({item: item});
        self.insert_new_html({
            pos: pos,
            html: html,
            other_key: other_key,
        });
    };

    self.fill_screen_with_content = function () {
        var height_to_fill = self.height_to_fill();

        var elem = $(self.scroll_container_sel).expectOne()[0];

        // Add a fudge factor.
        height_to_fill += 10;

        while (self.render_count < self.keys.length) {
            var padding_height = $(self.padding_sel).height();
            var bottom_offset = elem.scrollHeight - elem.scrollTop - padding_height;

            if (bottom_offset > height_to_fill) {
                break;
            }

            var chunk_size = 20;

            self.render_more({
                chunk_size: chunk_size,
            });
        }
    };

    // This is a bit of a hack to make sure we at least have
    // an empty list to start, before we get the initial payload.
    self.container = $(self.container_sel);

    self.start_scroll_handler = function () {
        // We have our caller explicitly call this to make
        // sure everything's in place.
        var scroll_container = $(self.scroll_container_sel);

        scroll_container.scroll(function () {
            self.fill_screen_with_content();
        });
    };

    self.update_padding = function () {
        padded_widget.update_padding({
            shown_rows: self.render_count,
            total_rows: self.keys.length,
            content_sel: self.container_sel,
            padding_sel: self.padding_sel,
        });
    };

    return self;
}());

if (typeof module !== 'undefined') {
    module.exports = buddy_list;
}

window.buddy_list = buddy_list;