Garmin Connect: sort activity course dropdown

Sorts Garmin connect activity course dropdown alphabetically

// ==UserScript==
// @name         Garmin Connect: sort activity course dropdown
// @namespace    http://tampermonkey.net/
// @description  Sorts Garmin connect activity course dropdown alphabetically
// @author       You
// @match        https://connect.garmin.com/modern/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=garmin.com
// @grant        window.onurlchange
// @license      MIT
// @version      0.4
// ==/UserScript==

(function () {
    'use strict';

    const urlPrefix = 'https://connect.garmin.com/modern/activity/'
    let currentPageMatchesUrl = false;

    function sortList(ul) {
        const new_ul = ul.cloneNode(false);
        const lis = [];
        for (let i = ul.childNodes.length; i--;) {
            if (ul.childNodes[i].nodeName === 'LI') {
                lis.push(ul.childNodes[i]);
            }
        }

        lis.sort(function (a, b) {
            // sort the "--" (no course) entry first
            if (a.getAttribute("data-value") === '-1') return -1;
            if (b.getAttribute("data-value") === '-1') return 1;

            const aText = a.childNodes[0].textContent;
            const bText = b.childNodes[0].textContent;

            // sorts case-insensitively and handles numbers correctly (e.g. "7" < "10")
            return aText.localeCompare(bText, undefined, {
                numeric: true,
                sensitivity: 'base'
            });
        });

        for (let i = 0; i < lis.length; i++) {
            new_ul.appendChild(lis[i]);
        }
        ul.parentNode.replaceChild(new_ul, ul);
    }

    const dropdownParent = '#course-dropdown + div.dropdown + div.dropdown';
    // don't know of way to be sure that the drop-down was fully populated,
    // so we sort on every click
    function installHandler(elem) {
        elem.addEventListener('click', function (e) {
            sortList(elem.querySelector('ul[role=menu]'));
        });
    }

    function runWhenReady(readySelector, callback) {
        let numAttempts = 0;
        let timer = undefined

        const tryNow = function () {
            const elem = document.querySelector(readySelector);
            if (elem) {
                callback(elem);
            } else {
                numAttempts++;
                if (numAttempts >= 34) {
                    console.warn('Giving up after 34 attempts. Could not find: ' + readySelector);
                } else {
                    timer = setTimeout(tryNow, 250 * Math.pow(1.1, numAttempts));
                }
            }
        };

        const stop = function () {
            clearTimeout(timer);
            timer = undefined
        }

        tryNow();
        return {
            stop
        }
    }

    let tasks = []    
    function init() {
        tasks = [];
        tasks.push(runWhenReady(dropdownParent, installHandler));
    }
    function deinit() {
        tasks.forEach(task => task.stop());
        tasks = [];
    }

    function waitForUrl() {
        // if (window.onurlchange == null) {
            // feature is supported
            window.addEventListener('urlchange', onUrlChange);
        // }
        onUrlChange();
    }

    function onUrlChange() {
        const urlMatches = window.location.href.startsWith(urlPrefix);
        if (!currentPageMatchesUrl) {
            if (urlMatches) {
                currentPageMatchesUrl = true;
                init();
            }
        } else {
            if (!urlMatches) {
                currentPageMatchesUrl = false;
                deinit();
            }
        }
    }

    waitForUrl()
})();