Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/**
* linkThings - a script to let you alt + click on
* [[wiki links]] and {{template links}} in the CodeMirror
* and visual source editor
*
* @version 1.4.0
* @license https://opensource.org/licenses/MIT MIT
* @author https://github.com/TheresNoGit/linkThings/graphs/contributors
* @link https://github.com/TheresNoGit/linkThings
*/
/* global $, mw, ve */
/*jshint esversion: 6 */

// Configure
let version = "1.4.0";

// Init
$(function() {
    mw.loader.using(["mediawiki.Title"], setup);
});

/**
 * Gets the URL of the page, irrespective of the wiki this is on.
 * @param {string} page The page to get the URL of.
 */
function getUrl(page = "") {
    return new URL(
        mw.config.get("wgArticlePath").replace(/\$1/g, page),
        window.location.href
    ).toString();
}

/**
 * Set up the event listener
 * 
 * @returns bool
 */
function setup() {
    // Wait for VE Source to load
    mw.hook('ve.activationComplete').add(function () {
        if ($(".ve-ui-surface").length) {
            // Get VE object
            var surface = ve.init.target.getSurface();
            // Only run in source mode
            if (surface.getMode() === 'source') {
                // Set up event listener for an alt + click
                $('.ve-ui-surface').on('click', function (event) {
                    if (event.altKey) {
                        $(".cm-mw-pagename").each((i, e) => {
                            // Click "raycasting" in order to detect the click even if the VE
                            // elemnent is overhead.
                            if (isClickAboveElement(e, event)) {
                                if (parseLink(e)) {
                                    return true;
                                } else {
                                    // Assume the user alt + clicked on something they thought would work, and give error
                                    console.error(`linkThings v${version}: Clicked element was not detected as a page or template link`);
                                    return false;
                                }
                            }
                        });
                    }
                });
                console.info(`linkThings v${version}: Initialized OK, using ${getUrl()} in VE mode`);
            } else {
                console.debug(`linkThings v${version}: VE is not in source mode`);
            }
        } else {
            console.error(`linkThings v${version}: Could not initialize script - ve-ui-surface element not found?`);
            return false;
        }
    });

    // Wait for CodeMirror to load
    mw.hook('ext.CodeMirror.switch').add(function (enabled, cm) {
        if ($(".CodeMirror").length && enabled) {
            // Set up event listener for an alt + click
            CodeMirror.on(cm[0], "mousedown", function (event) {
                if (event.altKey) {
                    if (parseLink(event.target)) {
                        return true;
                    } else {
                        // Assume the user alt + clicked on something they thought would work, and give error
                        console.error(`linkThings v${version}: Clicked element was not detected as a page or template link`);
                        return false;
                    }
                }
            });
            console.info(`linkThings v${version}: Initialized OK, using ${getUrl()} in CodeMirror mode`);
        } else {
            console.error(`linkThings v${version}: Could not initialize script - CodeMirror element not found?`);
            return false;
        }
    });
}

/**
 * Parse a ctrl clicked *anything* (CodeMirror)
 * 
 * @param {HTMLElement} element Clicked HTML element
 * @returns bool
 */
function parseLink(element) {
    // Check if this is a page/template link
    if (!element.classList.contains("cm-mw-pagename")) {
        // Neither a template link nor a page link
        return false;
    } else if (
        element.classList.contains("cm-mw-template-name")
        || element.classList.contains("cm-mw-link-pagename")
    ) {
        // Get the page link
        const page = new mw.Title(
            element.innerHTML,
            element.classList.contains("cm-mw-template-name") ?  //
                mw.config.get("wgNamespaceIds")["template"] : undefined
        );
        const url = getUrl(page.getPrefixedDb());
        console.debug(`linkThings v${version}: opening ${url}`);
        openInTab(url);
        return true;
    }
}

/**
 * Check if a click was above an element.
 * 
 * @param {HTMLElement} element The element to check for
 * @param {MouseEvent} event The event to check against
 * @returns {boolean} Whether or not the click was above the element or not
 */
function isClickAboveElement(element, event) {
    const $e = $(element), $w = $(window);
    const { clientY: cTop, clientX: cLeft } = event;
    const { top: eTop, left: eLeft } = $e.offset();
    const eHeight = $e.height(), eWidth = $e.width();
    const scrollTop = $w.scrollTop(), scrollLeft = $w.scrollLeft();

    return (
        // Within bounds, top
        eTop - scrollTop <= cTop && eTop - scrollTop + eHeight >= cTop &&
        // Within bounds, left
        eLeft - scrollLeft <= cLeft && eLeft - scrollLeft + eWidth >= cLeft
    );
}

/**
 * Opens a URL in a new tab
 * 
 * @param {string} url URL to open
 * @returns bool
 */
function openInTab(url) {
    var newTab = window.open(url, '_blank');
    if (newTab) {
        newTab.focus();
        return true;
    } else {
        console.error(`linkThings v${version}: Browser did not open new tab. Check settings?`);
        alert('Please ensure popups are enabled for this site');
        return false;
    }
}