greenelab/deep-review

View on GitHub
build/plugins/link-highlight.html

Summary

Maintainability
Test Coverage
<!-- link highlight plugin -->

<script>
    (function() {
        // /////////////////////////
        // DESCRIPTION
        // /////////////////////////

        // This Manubot plugin makes it such that when a user hovers or
        // focuses a link, other links that have the same target will be
        // highlighted. It also makes it such that when clicking a link, the
        // target of the link (eg reference, figure, table) is briefly
        // highlighted.

        // /////////////////////////
        // OPTIONS
        // /////////////////////////

        // plugin name prefix for url parameters
        const pluginName = 'linkHighlight';

        // default plugin options
        const options = {
            // whether to also highlight links that go to external urls
            externalLinks: 'false',
            // whether user must click off to unhighlight instead of just
            // un-hovering
            clickUnhighlight: 'false',
            // whether to also highlight links that are unique
            highlightUnique: 'true',
            // whether plugin is on or not
            enabled: 'true'
        };

        // change options above, or override with url parameter, eg:
        // 'manuscript.html?pluginName-enabled=false'

        // /////////////////////////
        // SCRIPT
        // /////////////////////////

        // start script
        function start() {
            const links = getLinks();
            for (const link of links) {
                // attach mouse and focus listeners to link
                link.addEventListener('mouseenter', onLinkFocus);
                link.addEventListener('focus', onLinkFocus);
                link.addEventListener('mouseleave', onLinkUnhover);
            }

            // attach click and hash change listeners to window
            window.addEventListener('click', onClick);
            window.addEventListener('touchstart', onClick);
            window.addEventListener('hashchange', onHashChange);

            // run hash change on window load in case user has navigated
            // directly to hash
            onHashChange();
        }

        // when link is focused (tabbed to) or hovered
        function onLinkFocus() {
            highlight(this);
        }

        // when link is unhovered
        function onLinkUnhover() {
            if (options.clickUnhighlight !== 'true')
                unhighlightAll();
        }

        // when the mouse is clicked anywhere in window
        function onClick(event) {
            unhighlightAll();
        }

        // when hash (eg manuscript.html#introduction) changes
        function onHashChange() {
            const target = getHashTarget();
            if (target)
                glowElement(target);
        }

        // get element that is target of link or url hash
        function getHashTarget(link) {
            const hash = link ? link.hash : window.location.hash;
            const id = hash.slice(1);
            let target = document.querySelector('[id="' + id + '"]');
            if (!target)
                return;

            return target;
        }

        // start glow sequence on an element
        function glowElement(element) {
            const startGlow = function() {
                onGlowEnd();
                element.dataset.glow = 'true';
                element.addEventListener('animationend', onGlowEnd);
            };
            const onGlowEnd = function() {
                element.removeAttribute('data-glow');
                element.removeEventListener('animationend', onGlowEnd);
            };
            startGlow();
        }

        // highlight link and all others with same target
        function highlight(link) {
            // force unhighlight all to start fresh
            unhighlightAll();

            // get links with same target
            if (!link)
                return;
            const sameLinks = getSameLinks(link);

            // if link unique and option is off, exit and don't highlight
            if (sameLinks.length <= 1 && options.highlightUnique !== 'true')
                return;

            // highlight all same links, and "select" (special highlight) this
            // one
            for (const sameLink of sameLinks) {
                if (sameLink === link)
                    sameLink.setAttribute('data-selected', 'true');
                else
                    sameLink.setAttribute('data-highlighted', 'true');
            }
        }

        // unhighlight all links
        function unhighlightAll() {
            const links = getLinks();
            for (const link of links) {
                link.setAttribute('data-selected', 'false');
                link.setAttribute('data-highlighted', 'false');
            }
        }

        // get links with same target
        function getSameLinks(link) {
            const results = [];
            const links = getLinks();
            for (const otherLink of links) {
                if (
                    otherLink.getAttribute('href') === link.getAttribute('href')
                )
                    results.push(otherLink);
            }
            return results;
        }

        // get all links of types we wish to handle
        function getLinks() {
            let query = 'a';
            if (options.externalLinks !== 'true')
                query += '[href^="#"]';
            // exclude buttons, anchor links, toc links, etc
            query +=
                ':not(.button):not(.icon_button):not(.anchor):not(.toc_link)';
            return document.querySelectorAll(query);
        }

        // load options from url parameters
        function loadOptions() {
            const url = window.location.search;
            const params = new URLSearchParams(url);
            for (const optionName of Object.keys(options)) {
                const paramName = pluginName + '-' + optionName;
                const param = params.get(paramName);
                if (param !== '' && param !== null)
                    options[optionName] = param;
            }
        }
        loadOptions();

        // start script when document is finished loading
        if (options.enabled === 'true')
            window.addEventListener('load', start);
    })();
</script>