How to have your Facebook chat box contacts ordered alphabetically

Sometime in May 2013, Facebook decided to change the chat box contacts ordering.
Since I wanted to have my contacts ordered alphabetically, I wrote a little bookmarklet.

Browser support

Tested on:

Installation

Create a bookmark, favorite, or whatever it is called in your browser, to this link: Chatbox sort, for example by dragging it to your bookmarks toolbar.

GreaseMonkey/Firefox and Tanpermonkey/Chrome users can install this script from userscripts.org.

Usage

Visit Facebook and wait for the page to be loaded.

Opening the chat box will cause Facebook to load the list of contacts and sort them using its own algorithm.

Clicking the bookmarklet that you installed above will sort the contacts alphabetically by first name.

Note that Facebook will periodically reload and reorder the list of contacts.

The javascript from the bookmarklet will automatically reorder the contacts alphabetically again.

Using the menu, automatic sorting can be turned on and off.

Technical information for geeks

(function (d, q, r, csid, interval, att) /* v1.26 */

    /* click to view the entire source code */

(function (d, q, r, csid, interval, att) /* v1.26 */
{
    /* Selector to find the 'Alphabetic Sort' link */
    var linkSelector = 'a[chat-sort-id="' + csid + '"]';
    /* Helper function to iterate over querySelectorAll() results.
     * The first time through, the chat box may not have been rendered
     * yet, so we may not be able to install ourselves directly.
     * We provide a retry option to keep trying the installation.
     */
    var nodeForAll = function (selector, callback, retry)
    {
        var nodes = d[r](selector);
        if (nodes.length)
        {
            for (var i = 0; i < nodes.length; i += 1) {
                callback(nodes[i], i);
            }
        } else if (retry) {
            /* Install failed. Try again in 500 ms. */
            setTimeout(function () {
                nodeForAll(selector, callback, retry);
            }, 500);
        }
    };
    /* Portable way to remove a class name */
    var removeClassName = function (node, cls)
    {
        if (node.classList) {
            node.classList.remove(cls);
        } else {
            /* MSIE8 does not support 'classList' */
            node.className = node.className.split(' ').filter(function (fragment) {
                return (fragment !== cls);
            }).join(' ');
        }
    };
    /* Portable way to add a class name */
    var addClassName = function (node, cls)
    {
        if (node.classList) {
            node.classList.add(cls);
        } else {
            node.className += cls;
        }
    };
    /* Portable way to install event listeners */
    var addEventListener = function (element, eventName, handler)
    {
        if (element.addEventListener) {
            element.addEventListener(eventName, handler, true);
        } else {
            element.attachEvent('on' + eventName, handler);
        }
    };
    /* Define a function for sorting the contacts in the chat list. */
    var contactSorter = function ()
    {
        /* Find the list of chat contacts. There may be more than one. */
        nodeForAll('ul.fbChatOrderedList', function (chatbox)
        {
            /* Find the contacts. Convert the nodelist to an array and sort it. */
            var chatterers = Array.prototype.filter.call(
                chatbox[r]('._42fz'),
                function () { return true; }
            ).sort(function (a, b)
            {
                /* Find the name and decide which goes first. Sorting by last name poses
                 * problems, as 'Barack Hussein Obama' and 'Maxima Zorreguieta Cerruti'
                 * need different interpretations. If you must, you could try:            */
                /* st.replace(/^(.+) (\S+)$/, function (m,f,s) { return s + ', ' + f; }); */
                var atext = a[q]('._55lr').innerHTML;
                var btext = b[q]('._55lr').innerHTML;
                return (atext < btext ? -1 : (atext > btext ? 1 : 0));
            });
            /* Iterate over the sorted list and sort the DOM accordingly. */
            for (var u = 0; u < chatterers.length; u += 1) {
                chatbox.appendChild(chatterers[u]);
            };
        });
        /* Hide the 'more friends' separator as it is now useless. */
        var more = d[q]('.moreOnlineFriends');
        if (more) {
            /* Remove it. Facebook code will add it again on the next reload */
            more.parentElement.removeChild(more);
        }
    };
    /* Toggle the interval timer that will sort the contacts.
     * If 'ev' is not an actual mouse click event, then the function was called
     * at install time, to turn sorting on. In that case, also keep retrying
     * should the initial call fail to find our menu item.
     */
    var toggleAutoSorting = function (ev)
    {
        var intId;
        var retry  = !ev;
        nodeForAll(linkSelector, function (linkNode, index)
        {
            var menuItem = linkNode.parentElement;
            /* Only on first pass: fetch existing interval timer id from the DOM. */
            if (index == 0)
            {
                intId = linkNode.getAttribute(att);
                /* Set the timer if there is none */
                if (!intId) {
                    intId = setInterval(contactSorter, interval);
                    /* Store the interval timer id in the DOM. */
                    linkNode.setAttribute(att, intId);
                    /* Place the check mark next to the menu item */
                    addClassName(menuItem, 'checked');
                    addClassName(menuItem, '_54nd');
                /* clear the timer, but only if triggered by user */
                } else if (ev) {
                    clearInterval(intId);
                    /* Remove the interval timer id from the DOM. */
                    linkNode.setAttribute(att, '');
                    /* Remove the check mark next to the menu item */
                    removeClassName(menuItem, 'checked');
                    removeClassName(menuItem, '_54nd');
                }
            }
        }, retry);
        /* Prevent following the link */
        return false;
    };
    /* Install the menu item */
    var installMenuItem = function ()
    {
        /* Don't install ourselves twice */
        if (!d[r](linkSelector).length)
        {
            var makeMenuItem = function ()
            {
                /* Create the new menu option element in a sandbox. */
                var sandbox = d.createElement('div');
                sandbox.innerHTML = '<li class="_54ni __MenuItem" aria-selected="false">' +
                    '<a class="_54nc" tabindex="0" role="menuitem" chat-sort-id="' + csid + '">' +
                    '<span class="_54nh">Alphabetic Sort</span></a></li>';
                /* Add listeners to the <li> and <a> */
                addEventListener(sandbox.firstChild.firstChild, 'click', toggleAutoSorting);
                addEventListener(sandbox.firstChild, 'mouseover', function () {
                    addClassName(this, '_54ne');
                    addClassName(this, 'selected');
                    this.setAttribute('aria-selected', 'true');
                });
                addEventListener(sandbox.firstChild, 'mouseout', function () {
                    removeClassName(this, '_54ne');
                    removeClassName(this, 'selected');
                    this.setAttribute('aria-selected', 'false');
                });
                sandbox.style.display = 'none';
                document.body.appendChild(sandbox);
                return sandbox.firstChild;
            };
            var menuItem = makeMenuItem();
            /* If one of the 'Options' buttons is clicked, make sure that there is a "sort" menu item. */
            nodeForAll('.fbChatSidebar a[aria-label="Options"], ' +
                '.fbChatSidebarDropdown a[aria-label="Options"], ' +
                '#BuddylistPagelet a[aria-label="Options"]', function (optionItem)
            {
                /* Add click listeners to the "Options" menu item */
                addEventListener(optionItem, 'click', function (ev) {
                    /* Give the chat menu a short time to render */
                    setTimeout(function () {
                        /* Try to find the chat menu */
                        nodeForAll('.uiContextualLayer ul', function (menu) {
                            /* Extend the current chat menu with the menu item for toggling the interval timer. */
                            if (!menu[q](linkSelector)) {
                                var placeEl = menu[q]('li[role="separator"]');
                                menu.insertBefore(menuItem, placeEl);
                            }
                        }, true);
                    }, 300);
                });
            });
        }
    };
    /* Give the sidebar some time to load */
    setTimeout(function () {
        /* Install menu item now */
        installMenuItem();
        /* Turn on now, if not already loaded, by using undefined as first argument */
        toggleAutoSorting();
    }, 500);
})(document, 'querySelector', 'querySelectorAll', 506, 800, 'data-sorter-timer-id');

Changelog

1.26
Updated to support the new chatbox CSS classes.
Added delay before initializing, to give Facebook some time to initialize the component
1.25
Updated to support the new chat options menu.
1.23
Update allowing for changed CSS class names.
1.22
Added automatic retry of the installation. It may have failed because the chat box was not yet rendered.
1.21
Allow the script to be started multiple times without confusing the browser.
1.18
Added an option 'Automatic Sort' in the chat menu;
Automatically sort the chat contacts list every 0.8 sec.
0.96
Automatically sort the chat contacts list when the mouse moves over it.

Legal stuff

This code is René Uittenbogaard.

You are free:

to Share: to copy, distribute and transmit the work

to Remix: to adapt the work

Under the following conditions:

Attribution: You must attribute the work in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the work).

Noncommercial: You may not use this work for commercial purposes.

Share Alike: If you alter, transform, or build upon this work, you may distribute the resulting work only under the same or similar license to this one.