コンテンツにスキップ

「利用者:Dragoniez/scripts/AN Reporter.js」の版間の差分

削除された内容 追加された内容
v6.2: 技術的更新(暫定版、コード記録用)
v6.3: 技術的更新(完全版)
2行目: 2行目:
* AN Reporter (ANR) *
* AN Reporter (ANR) *
* Author: Dragoniez *
* Author: Dragoniez *
* Version: 6.2 *
* Version: 6.3 *
************************************/
************************************/
//<nowiki>
//<nowiki>
45行目: 45行目:
'portletLink': false,
'portletLink': false,
'causeIntentionalError': false,
'causeIntentionalError': false,
'drPreviewSections': 'tarSectionsI' // I, S, 3RR, SubpagedLTA
'drPreviewSections': 'tarSectionsS' // I, S, 3RR, SubpagedLTA
};
};
const scriptAd = debugMode.scriptAd ? ' ([[User:Dragoniez/AN Reporter|AN Reporter Experimental]])' : ' ([[User:Dragoniez/AN Reporter|AN Reporter]])';
const scriptAd = debugMode.scriptAd ? ' ([[User:Dragoniez/AN Reporter|AN Reporter Experimental]])' : ' ([[User:Dragoniez/AN Reporter|AN Reporter]])';
1,574行目: 1,574行目:
if (ep.sectionToEdit === 'その他' && wikitext.indexOf(miscHeader) === -1) ep.reportText = '; ' + miscHeader + '\n\n' + ep.reportText;
if (ep.sectionToEdit === 'その他' && wikitext.indexOf(miscHeader) === -1) ep.reportText = '; ' + miscHeader + '\n\n' + ep.reportText;


// Define comment-outs
// Get the report text to submit
const tail = '\n\n<!-- ◆以下は消さないで下さい。新規依頼はこの上へ -->}}';
let head;
if (ep.pageToEdit === ISECHIKA) {
head = '<!-- ◆ここから下に新しい報告を記入して下さい -->\n<!-- 不適切な利用者名を載せないでください。何時何分に作成された等の依頼方法でお願いします。 -->\n\n';
} else {
head = '<!-- ◆ここから下に新しい報告を記入して下さい -->\n\n';
}

// Extract the '報告' parameter of SockInfo and get a new wikitext for the section including the report to be submitted
let sockInfo = findTemplates(wikitext, 'sockinfo'); // Array
let sockInfo = findTemplates(wikitext, 'sockinfo'); // Array
let sockInfoRep, matched;
if (sockInfo.length === 1) { // One section on WP:AN/S should have one SockInfo
if (sockInfo.length === 1) { // One section on WP:AN/S should have one SockInfo

sockInfo = sockInfo[0];
sockInfo = sockInfo[0];
if (matched = sockInfo.match(/\|\s*報告[^\S\r\n]*=\s*/g)) { // If the template has a '報告' parameter
const sockInfoNoClosure = sockInfo.substring(0, sockInfo.length - 2).trimANR();
reportText = wikitext.replace(sockInfo, sockInfoNoClosure + '\n\n' + ep.reportText + '\n\n}}');

if (matched.length === 1) {

sockInfoRep = sockInfo.split(matched[0])[1]; // The content of the '報告' parameter
if (matched = sockInfoRep.match(/(\(UTC\))+([^\S\r\n]*<\/*\w+[^\S\r\n]*>)*/g)) { // If there're other reports

const lastUtc = matched[matched.length - 1]; // The last occurrence of 'UTC' (the string itself)
const splitIndex = wikitext.lastIndexOf(lastUtc) + lastUtc.length; // The repot should be inserted at the nth character
reportText = wikitext.substring(0, splitIndex).trimANR() + '\n\n' + ep.reportText + '\n\n' + wikitext.substring(splitIndex).trimANR() + '\n\n';

} else { // If there're no other reports
reportText = wikitext.replace(sockInfoRep, head + ep.reportText + tail);
}

}
}
}

if (reportText) {
return reportText;
return reportText;
} else {
} else { // There's a problem with SockInfo
// Show error and quit the procedure
msg = // Show error and quit the procedure
msg =
toggleLoadingSpinner('remove') +
toggleLoadingSpinner('remove') +
'<p style="color: MediumVioletRed">報告に失敗しました</p><br>' +
'<p style="color: MediumVioletRed">報告に失敗しました</p><br>' +

2022年5月5日 (木) 17:37時点における版

/************************************
 *  AN Reporter (ANR)               *
 *  Author: Dragoniez               *
 *  Version: 6.3                    *
 ************************************/
//<nowiki>

// ******************** CONFIGS ********************

/* Config
anrConfig: {
    predefinedReasons: {},
    addToWatchlist: true,
    headerColor: '#FEC493',
    backgroundColor: '#FFF0E4',
    portletlinkPosition: 'skin-dependent',
    fontSize: 'skin-dependent',
    dropdownFontSize: 'skin-dependent'
}                                               */

if (typeof anrConfig === 'undefined') var anrConfig = {};
if (typeof anrPredefinedReasons !== 'undefined' && Array.isArray(anrPredefinedReasons)) { // Predefined reasons were previously defined as an array
    if (anrPredefinedReasons.length !== 0) {
        anrConfig.predefinedReasons = {};
        for (let i = 0; i < anrPredefinedReasons.length; i++) {
            anrConfig.predefinedReasons[i] = anrPredefinedReasons[i];
        }
    }
}
if (!anrConfig.headerColor) anrConfig.headerColor = '#FEC493';
if (!anrConfig.backgroundColor) anrConfig.backgroundColor = '#FFF0E4';


// ******************** SCRIPT BODY ********************

(function(){ // Create a function scope

    // ******************** VARIABLES ********************

    // Debugging Mode
    const debugMode = {
        'scriptAd': false, // 'AN Reporter Experimental' if true
        'editSummary': false, // 'Test edit via mediawiki API' + scriptAd if true
        'editTarget': false, // 'User:Dragoniez/test' if true
        'portletLink': false,
        'causeIntentionalError': false,
        'drPreviewSections': 'tarSectionsS' // I, S, 3RR, SubpagedLTA
    };
    const scriptAd = debugMode.scriptAd ? ' ([[User:Dragoniez/AN Reporter|AN Reporter Experimental]])' : ' ([[User:Dragoniez/AN Reporter|AN Reporter]])';
    const portletLinkText = debugMode.portletLink ? '報告β' : '報告';
    const developerLink = `<a href="${mw.util.getUrl('User talk:Dragoniez/AN Reporter')}" target="_blank">開発者</a>`;

    // Page names
    const ANI = 'Wikipedia:管理者伝言板/投稿ブロック';
    const ANS = 'Wikipedia:管理者伝言板/投稿ブロック/ソックパペット';
    const AN3RR = 'Wikipedia:管理者伝言板/3RR';
    const VIP = 'Wikipedia:進行中の荒らし行為';
    const Iccic = 'Wikipedia:進行中の荒らし行為/長期/Iccic/投稿ブロック依頼'; //SockInfo
    const ISECHIKA = 'Wikipedia:管理者伝言板/投稿ブロック/いせちか';
    const KAGE = 'Wikipedia:管理者伝言板/投稿ブロック/影武者';
    const KIYOSHIMA = 'Wikipedia:管理者伝言板/投稿ブロック/清島達郎';
    const SHINJU = 'Wikipedia:管理者伝言板/投稿ブロック/真珠王子';
    const TEST = '利用者:Dragoniez/test';

    /**
     * Object to store logids for usernames {user1: logid, user2: logid...}
     */
    const Logids = {};

    // Related to dialog creation
    var userDiv; // What to append when the 'add' button is hit
    var userCnt = 1; // *ID number of the elements in the appended userDiv


    // ******************** DOM READY FUNCTION ********************

    // Wait for the required dependencies to be ready
    $.when(
        $.getScript('https://cdn.jsdelivr.net/npm/[email protected]/dist/js/select2.min.js'),
        mw.loader.using(['jquery.ui', 'mediawiki.util']),
        $.ready
    ).then(function(){

        // Load CSS source for Select2
        $('head').append('<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/select2.min.css">');

        // Run the script only if the user is autoconfirmed and the page is not an edit page
        if (isInArray('autoconfirmed', mw.config.get('wgUserGroups')) && mw.config.get('wgAction') !== 'edit') {
            addAnrPortletLink();
        }

    });


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

    function addAnrPortletLink() {

        // Define the position of the portletlink (skin-dependent)
        var lkPosition;
        if (anrConfig.portletlinkPosition) {
            lkPosition = anrConfig.portletlinkPosition;
        } else {
            switch(mw.config.get('skin')) {
                case 'vector':
                case 'vector-2022':
                    lkPosition = 'p-views';
                    break;
                case 'minerva':
                    lkPosition = 'p-personal';
                    break;
                default: // monobook, timeless, or something else
                    lkPosition = 'p-cactions';
            }
        }

        // Add a portletlink for ANR
        $(mw.util.addPortletLink(lkPosition, '#', portletLinkText, 'ca-anr', '管理者伝言板に利用者を報告', null, '#ca-move')).click(openAnrDialog);

    }

    // Set font size
    var fontSize, select2FontSize
    if (anrConfig.fontSize) {
        fontSize = anrConfig.fontSize;
    } else {
        switch(mw.config.get('skin')) {
            case 'vector':
            case 'vector-2022':
            case 'minerva':
                fontSize = '80%';
                break;
            case 'monobook':
                fontSize = '110%';
                break;
            case 'timeless':
                fontSize = '90%';
                break;
            default:
                fontSize = '80%';
        }
    }
    if (anrConfig.dropdownFontSize) {
        select2FontSize = anrConfig.dropdownFontSize;
    } else {
        switch(mw.config.get('skin')) {
            case 'vector':
            case 'vector-2022':
            case 'minerva':
                select2FontSize = '0.9em';
                break;
            case 'monobook':
                select2FontSize = '1.03em';
                break;
            case 'timeless':
                select2FontSize = '0.94em';
                break;
            default:
                select2FontSize = '0.9em';
        }
    }

    var styleAppended = false;
    function openAnrDialog(e) {
        e.preventDefault();

        // Default CSS for Select2 and classes shared by selectors
        if (!styleAppended) {
            $('head').append(
                `<style>
                    .select2-selection__rendered {
                        padding: 1px 2px;
                        font-size: 1em;
                        line-height: normal !important;
                    }
                    .select2-results__option, .select2-results__group {
                        padding: 1px 8px;
                        font-size: ${select2FontSize};
                        margin: 0;
                    }
                    .select2-container, .select2-selection--single {
                        height: auto !important;
                    }
                    .anr-dialog-label {
                        display: inline-block;
                        width: 8ch;
                    }
                    .anr-dialog-select, .anr-dialog-input {
                        border: 1px solid #d3d3d3;
                        border-radius: 1%;
                        background-color: white;
                        padding: 2px 4px;
                    }
                    .anr-dialog-button {
                        color: black;
                        font-weight: normal;
                        border: 1px solid #d3d3d3;
                        background-color: white;
                        padding: 0.2em 0.5em;
                        border-radius: 10%;
                    }
                    .anr-dialog-textarea {
                        width: 100%;
                        box-sizing: border-box;
                    }
                    .anr-dialog-needmargin {
                        margin: 1em 0;
                    }
                </style>`
            );
            styleAppended = true;
        }

        // The whole html contour
        const modalHtml =
        `<div id="anr-modal-dialog" title="AN Reporter" style="max-height: 80vh;">` +
        `   <div id="anr-modal-header">` +
        `       <h2>利用者を報告</h2>` +
        `   </div>` +
        `   <div id="anr-modal-body">` +
        `       <form>` +
        `           <div id="anr-target-div" class="anr-dialog-needmargin">` +
        `               <label for="anr-target-options" id="anr-target-options-label" class="anr-dialog-label">報告先</label>` +
        `               <select id="anr-target-options" class="anr-dialog-select">` +
        `                   <option selected disabled hidden>選択してください</option>` +
        `                   <option>${ANI}</option>` +
        `                   <option>${ANS}</option>` +
        `                   <option>${AN3RR}</option>` +
        `               </select>` +
        `               <div id="anr-target-pagelink-div" style="display: none;">` +
        `                   <label class="anr-emptylabel anr-dialog-label" for="anr-target-pagelink"></label>` +
        `                   <a id="anr-target-pagelink" href="" target="_blank">報告先を確認</a>` +
        `               </div>` +
        `           </div>` +
        `           <div id="anr-section-i-div" class="anr-dialog-needmargin" style="display: none;">` +
        `               <label for="anr-section-i-select" class="anr-dialog-label">節</label>` +
        `               <select id="anr-section-i-select" class="anr-dialog-select">` +
        `                   <option selected disabled hidden class="anr-section-options-initial">選択してください</option>` +
        `                   <option id="anr-section-i-options-date"></option>` +
        `                   <option>不適切な利用者名</option>` +
        `                   <option>公開アカウント</option>` +
        `                   <option>公開プロキシ・ゾンビマシン・ボット・不特定多数</option>` +
        `                   <option>犯罪行為またはその疑いのある投稿</option>` +
        `               </select>` +
        `           </div>` +
        `           <div id="anr-section-s-div" class="anr-dialog-needmargin" style="display: none;">` +
        `               <label for="anr-section-s-select" class="anr-dialog-label">節</label>` +
        `               <select id="anr-section-s-select" class="anr-dialog-select">` +
        `                   <option selected disabled hidden class="anr-section-options-initial">選択してください</option>` +
        `                   <optgroup label="系列が立てられていないもの">` +
        `                       <option>著作権侵害・犯罪予告</option>` +
        `                       <option>名誉毀損・なりすまし・個人情報</option>` +
        `                       <option>妨害編集・いたずら</option>` +
        `                       <option>その他</option>` +
        `                   </optgroup>` +
        `                   <optgroup id="anr-section-s-lta" label="LTA">` +
        //                      getSectionsS()
        `                   </optgroup>` +
        `               </select>` +
        `           </div>` +
        `           <div id="anr-user-div" class="anr-dialog-needmargin">` +
        `               <div id="anr-user1-div">` +
        `                   <div id="anr-user1-input-div">` +
        `                       <label for="anr-user1-input" class="anr-dialog-label">利用者</label>` +
        `                       <input id="anr-user1-input" class="anr-dialog-input" style="width: 34ch;">` +
        `                       <select disabled id="anr-user1-select" class="anr-dialog-select">` +
        `                           <option class="anr-opt-UNL">UNL</option>` +
        `                           <option class="anr-opt-User2">User2</option>` +
        `                           <option class="anr-opt-IP2">IP2</option>` +
        `                           <option class="anr-opt-logid">logid</option>` +
        `                           <option class="anr-opt-diff">diff</option>` +
        `                           <option selected class="anr-opt-none">none</option>` +
        `                       </select>` +
        `                   </div>` +
        `                   <div id="anr-user1-checkbox-div" style="display: none;">` +
        `                       <label class="anr-emptylabel anr-dialog-label"></label>` +
        `                       <input type="checkbox" id="anr-user1-checkbox">` +
        `                       <label for="anr-user1-checkbox">利用者名を隠す</label>` +
        `                   </div>` +
        `                   <div id="anr-user1-idlink-div" style="display: none;">` +
        `                       <label for="anr-user1-idlink" class="anr-dialog-label"></label>` +
        `                       <a id="anr-user1-idlink" href="" target="_blank"></a>` +
        `                   </div>` +
        `                   <div id="anr-user1-blockstatus-div" style="display: none;">` +
        `                       <label for="anr-user1-blockstatus" class="anr-dialog-label"></label>` +
        `                       <a id="anr-user1-blockstatus" href="" target="_blank" style="color: MediumVioletRed;">ブロックあり</a>` +
        `                   </div>` +
        `               </div>` +
        `               <div id="anr-btn-div">` +
        `                   <button type="button" id="anr-addBtn" class="anr-dialog-button">追加</button>` +
        `               </div>` +
        `           </div>` +
        '           <div id="anr-viplist-div" style="width: 100%; display: none;">' +
        `               <label for="anr-viplist-select" class="anr-dialog-label">VIP</label>` +
        `               <select id="anr-viplist-select">` +
        '                   <optgroup style="display: none;">' + // Adjust font size
        '                       <option selected disabled hidden>コピーする場合は選択してください</option>' +
        //                      getVipList()
        '                   </optgroup>' +
        '               </select>' +
        '           </div>' +
        `           <div id="anr-predefinedreasons-div" class="anr-dialog-needmargin" style="display: none;">` +
        `               <label for="anr-predefinedreasons-select" class="anr-dialog-label">定型文</label>` +
        `               <select id="anr-predefinedreasons-select">` +
        '                   <optgroup style="display: none;">' + // Adjust font size
        `                       <option selected>定型文を使用する場合は選択してください</option>` +
        '                   </optgroup>' +
        `               </select>` +
        `           </div>` +
        `           <div id="anr-reason-div" class="anr-dialog-needmargin">` +
        `               <label for="anr-reason-text" class="anr-dialog-label">理由</label>` +
        `               <textarea id="anr-reason-text" class="anr-dialog-textarea" rows="6"></textarea>` +
        `           </div>` +
        `           <div id="anr-summary-div" class="anr-dialog-needmargin">` +
        `               <input id="anr-summary-checkbox" type="checkbox">` +
        `               <label for="anr-summary-checkbox">要約を指定</label>` +
        `               <textarea id="anr-summary-text" class="anr-dialog-textarea" rows="3" style="display: none;"></textarea>` +
        `           </div>` +
        `           <div id="anr-checkbox-div" class="anr-dialog-needmargin">` +
        `               <input checked id="anr-blockstatus-checkbox" type="checkbox">` +
        `               <label for="anr-blockstatus-checkbox">報告前にブロック状態をチェック</label>` +
        `               <br>` +
        `               <input checked id="anr-duplicatereport-checkbox" type="checkbox">` +
        `               <label for="anr-duplicatereport-checkbox">報告前に重複報告をチェック</label>` +
        `               <br>` +
        `               <input checked id="anr-watchlist-checkbox" type="checkbox">` +
        `               <label for="anr-watchlist-checkbox">報告対象者をウォッチリストに追加</label>` +
        `           </div>` +
        `       </form>` +
        `   </div>` +
        `</div>`;

        // Add the frame div to the page
        $('body').append(modalHtml);

        // Show dialog
        $('#anr-modal-dialog').dialog({
            'resizable': false,
            'height': 'auto',
            'width': 'auto',
            'modal': true,
            'open': initializeAnrDialog,
            'buttons': [{
                'text': 'プレビュー',
                'click': preview
            }, {
                'text': '報告',
                'click': report
            }]
        });

    }

    // Function to initialze the modal dialog
    function initializeAnrDialog(){

        userDiv = $('#anr-user1-div').prop('innerHTML'); // A div of the same structure is appended when the 'add' button is hit

        getSectionsS(); // Get sections on WP:AN/S
        dialogCSS(); // Initialize the design of the dialog
        getVipList(); // Show VIP list
        getPredefinedReasons(); // Show the select box for predefined reasons

        // Add to wathchlist?
        if (anrConfig.addToWatchlist === false) {
            $('#anr-watchlist-checkbox').prop('checked', false);
        };

        // Get the name of the user to report if it can be retrieved from the page
        var username = mw.config.get('wgRelevantUserName'); // Note: This does not pick up IP ranges

        // Workaround to pick up IP ranges
        if (!username && mw.config.get('wgCanonicalSpecialPageName') === 'Contributions') {
            const relUsername = $('#firstHeading').text().replace('の投稿記録', '');
            if (mw.util.isIPAddress(relUsername, true)) username = relUsername;
        }

        // Exit function if the current user is on his/her own page or username has remained undefined or null
        if (!username || username === mw.config.get('wgUserName')) return;

        // Initialize the username input and type dropdown
        const inputID = '#anr-user1-input', selectID = '#anr-user1-select', checkboxDivID = '#anr-user1-checkbox-div';

        $(inputID).val(username); // Fill the input with the username
        $(selectID).prop('disabled', false); // enable dropdown

        if (mw.util.isIPAddress(username, true)) { // if IP

            $(selectID).children('.anr-opt-UNL').prop('hidden', true);
            $(selectID).children('.anr-opt-User2').prop('hidden', true);
            $(selectID).children('.anr-opt-IP2').prop({'hidden': false, 'selected': true});
            $(selectID).children('.anr-opt-logid').prop('hidden', true);
            $(selectID).children('.anr-opt-diff').prop('hidden', true);
            $(selectID).children('.anr-opt-none').prop('hidden', false);
            $(checkboxDivID).css('display', 'none'); // hide 'hide username' checkbox
            toggleBlockStatusLink(inputID, false, false);

        } else { // if user

            $(selectID).children('.anr-opt-UNL').prop({'hidden': false, 'selected': true});
            $(selectID).children('.anr-opt-User2').prop('hidden', false);
            $(selectID).children('.anr-opt-IP2').prop('hidden', true);
            $(selectID).children('.anr-opt-logid').prop('hidden', true);
            $(selectID).children('.anr-opt-diff').prop('hidden', true);
            $(selectID).children('.anr-opt-none').prop('hidden', false);
            $(checkboxDivID).css('display', 'block'); // show 'hide username' checkbox
            toggleBlockStatusLink(inputID, false, false);
        }

    }

    // Function to get sections on WP:AN/S from the API
    async function getSectionsS() {
        const $label = $('#anr-target-options-label'); // Label of '報告先'
        $label.append(toggleLoadingSpinner('add')); // Show a loading spinner while trying to get sections on WP:AN/S

        const parse = await parsePage(ANS, 'sections');
        if (parse) {

            // Get VIP's names
            const sectionInfo = parse.sections;
            const excludeList = [
                '系列が立てられていないもの',
                '著作権侵害・犯罪予告',
                '名誉毀損・なりすまし・個人情報',
                '妨害編集・いたずら',
                'その他',
                'A. 最優先',
                '暫定A',
                '休止中A',
                'B. 優先度高',
                '暫定B',
                '休止中B',
                'C. 優先度中',
                '暫定C',
                '休止中C',
                'D. 優先度低',
                '暫定D',
                '休止中D',
                'N. 未分類',
                'サブページなし',
                '休止中N'
            ];
            const sectionList = [];
            for (let i = 0; i < sectionInfo.length; i++) {
                if (!isInArray(sectionInfo[i].line, excludeList) && sectionInfo[i].index.indexOf('T') === -1) {
                    sectionList.push(`<option>${sectionInfo[i].line}</option>`);
                }
            }

            $('#anr-section-s-lta').append(sectionList.join(''));

        } else {
            alert('WP:AN/Sのセクションリストを取得できませんでした。ダイアログを開き直すと改善する場合があります。');
        }
        toggleLoadingSpinner('remove');

    }

    /**
     * Function to add/remove/move a loading spinner
     * @param {string} action 'add', 'remove', or 'move'
     */
    function toggleLoadingSpinner(action) {
        const img = '<img class="anr-loading-spinner" src="https://upload.wikimedia.org/wikipedia/commons/4/42/Loading.gif" ' +
                    'style="vertical-align: middle; max-height: 100%; border: 0;">';
        switch(action) {
            case 'add':
                return img;
            case 'remove':
                $('.anr-loading-spinner').remove();
                return '';
            case 'move':
                $('.anr-loading-spinner').remove();
                return img;
        }
    }

    /**
     * Function to get a pages's information
     * @param {string} pagename
     * @param {string} prop wikitext, sections, wikitext|sections
     * @param {number} sectionNum optional
     * @returns {Promise} res.parse ({{sections: []}, wikitext: ''}) (undefined if query failed)
     */
    function parsePage(pagename, prop, sectionNum) {
        return new Promise(function(resolve) {
            var params = {
                'action': 'parse',
                'page': pagename,
                'prop': prop,
                'formatversion': 2
            }
            if (sectionNum) params = Object.assign(params, {'section': sectionNum}); // Concatenate params
            new mw.Api().get(params).then(function(res){
                resolve(res && res.parse ? res.parse : undefined);
            });
        });
    }

    // Function to change the CSS of the dialog
    function dialogCSS() {
        $('.ui-dialog-content, .ui-dialog-buttonpane, .ui-corner-all, .ui-draggable, .ui-resizable').css('background', anrConfig.backgroundColor);
        $('.ui-button').css({
            'color': 'black',
            'background-color': 'white'
        });
        $('.ui-dialog-titlebar, .ui-dialog-titlebar-close').attr('style', `background: ${anrConfig.headerColor} !important;`);
        $('.ui-dialog').css('font-size', fontSize);
    }

    // WP:VIP list (for copy to clipboard)
    async function getVipList() {
        
        const parse = await parsePage(VIP, 'sections');
        if (parse) {

            // Get VIP's names
            const sectionInfo = parse.sections;
            const excludeList = [
                '記述について',
                '急を要する二段階',
                '配列',
                'ブロック等の手段',
                'このページに利用者名を加える',
                '注意と選択',
                '警告の方法',
                '未登録(匿名・IP)ユーザーの場合',
                '登録済み(ログイン)ユーザーの場合',
                '警告中',
                '関連項目'
            ];
            const vipList = [];
            for (let i = 0; i < sectionInfo.length; i++) {
                if (!isInArray(sectionInfo[i].line, excludeList) && sectionInfo[i].level == 3) {
                    vipList.push(`<option>${sectionInfo[i].line}</option>`);
                }
            }

            if (vipList.length === 0) {
                return mw.log.error('VIP list: There\'s no VIP to fetch.');
            } else {
                $('#anr-viplist-select')
                    .css('width', $('#anr-target-options').innerWidth())
                    .select2()
                    .children('optgroup').append(vipList.join(''));
                $('#anr-viplist-div').css('display', 'block');
                centerDialog();
            }

        } else {
            return mw.log.error('VIP list: The API returned an unresolvable object.');
        }

    }

    // Function to show the select div for predefined report reasons if they're predefined
    function getPredefinedReasons() {
        const pdReasons = anrConfig.predefinedReasons;
        if (typeof pdReasons !== 'undefined' && !$.isEmptyObject(pdReasons)) { // If the user has fixed reasons prepared

            const $reasons = $('#anr-predefinedreasons-select');
            $reasons.css('width', $('#anr-target-options').innerWidth()).select2();
            
            for (let key in pdReasons) {
                $reasons.children('optgroup').append(`<option>${pdReasons[key]}</option>`);
            }
            $('#anr-predefinedreasons-div').css('display', 'block');
            centerDialog();

        }
    }

    function centerDialog() {
        var $dialog;
        if ($('#anr-preview-dialog').length !== 0) {
            $dialog = $('#anr-preview-dialog');
        } else if ($('#anr-drpreview-dialog').length !== 0) {
            $dialog = $('#anr-drpreview-dialog');
        } else {
            $dialog = $('#anr-modal-dialog');
        }
        $dialog.dialog({'position': {my: 'center', at: 'center', of: window}});
    }

    // Function to check information typed into the form
    function editPrep() {

        // Get all input values and UserAN types, and check for duplicates
        const users = [], types = [], duplicates = [];
        $('#anr-user-div :text').each(function(){ // Loop through all inputs

            const inputID = '#' + $(this).attr('id');
            const type = $(inputID.replace('input', 'select')).children('option').filter(':selected').text(); // UserAN type
            const inputVal = $(this).val().trimANR(); // Username
            if (!inputVal) return;

            var username, logid;
            if (type === 'logid' && (username = getKeyByValue(Logids, logid = inputVal))) { // if t=logid and the logid can be converted to a username

                // If either of the username or the logid is already in the array 'users' and if they have yet to be listed as duplicates
                if ((isInArray(username, users) || isInArray(logid, users)) && !isInArray(username, duplicates) && !isInArray(logid, duplicates)) {
                    duplicates.push(username, logid); // List both the username and the logid as duplicates
                }
                
            } else { // If t!=logid or t=logid but a username can't be obtained

                // If the username is already in the array 'users' (and if it hasn't been listed as a duplicate)
                if (isInArray(username = inputVal, users) && !isInArray(username, duplicates)) {
                    duplicates.push(username); // List the username as a duplicate
                }

            }

            users.push(inputVal); // Push the username into the array
            types.push(type); // Push the UserAN type into the array

        });

        // Get the name of the section to edit
        var pageToEdit =  $('#anr-target-options').children('option').filter(':selected').text();
        var sectionToEdit = '選択してください', reportToANS = false;

        if (pageToEdit === ANI) { // If WP:AN/I is selected as the target page to edit

            sectionToEdit = $('#anr-section-i-select').children('option').filter(':selected').text();

            // Update the target section for cases in which the date has changed since the date-dependent section was chosen
            if (sectionToEdit.match(/^\d{4}年\d{1,2}月\d{1,2}日 - \d{1,2}日新規報告$/)) {
                const sectionIDate = getSectionI(false);
                sectionToEdit = sectionIDate;
                $('#anr-section-i-options-date').text(sectionIDate);
            }

        } else if (pageToEdit === ANS) { // If WP:AN/S is selected as the target page to edit

            reportToANS = true;
            const sectionANS = $('#anr-section-s-select').find('option').filter(':selected').text();
            switch(sectionANS) {
                case 'Iccic系 (Iccic)':
                    pageToEdit = Iccic;
                    sectionToEdit = '新規依頼';
                    break;
                case 'いせちか系 (ISECHIKA)':
                    pageToEdit = ISECHIKA;
                    sectionToEdit = '新規依頼';
                    break;
                case '影武者系(KAGE)':
                    pageToEdit = KAGE;
                    sectionToEdit = '新規依頼';
                    break;
                case '清島達郎系 (清島、KIYOSHIMA)':
                    pageToEdit = KIYOSHIMA;
                    sectionToEdit = '新規依頼';
                    break;
                case '真珠王子系(SHINJU)':
                    pageToEdit = SHINJU;
                    sectionToEdit = '新規依頼';
                    break;
                default:
                    sectionToEdit = sectionANS;
            }

        } else if (pageToEdit === AN3RR) { // If WP:AN/3RR is selected as the target page to edit

            sectionToEdit = '3RR';

        }
        
        // The reason of the report
        var fixedReason = $('#anr-predefinedreasons-select').find('option').filter(':selected').text();
        fixedReason = fixedReason === '定型文を使用する場合は選択してください' ? '': fixedReason;
        var reason = fixedReason + $('#anr-reason-text').val().trimANR();

        // Check if necessary fields are filled
        if (pageToEdit === '選択してください' || sectionToEdit === '選択してください' || reason === '' || users.length === 0) {
            alert('必須項目が入力・選択されていません');
            return;
        }

        // Duplicate warning
        if (duplicates.length !== 0) { // If the inputs have duplicates in them
            const confirmMsg =
            '以下の利用者について、重複入力がある可能性があります。\n\n' + duplicates.join(', ') + '\n\n' +
            '続行する場合は OK を、フォームに戻る場合は Cancel を押してください';

            if (confirm(confirmMsg) === false) return;
        }

        // If the reason doesn't contain a signature, add one
        if (reason.substring(reason.length - 4) !== '~~~~') {
            reason += '--~~~~';
        }

        // Get edit summary
        const summaryText = $('#anr-summary-text').val().trimANR(), editSummarySection = '/*' + sectionToEdit + '*/';
        var editSummary, summaryCustomized;
        if (summaryText) {
            editSummary = editSummarySection + summaryText + scriptAd;
            summaryCustomized = true;
        } else {
            editSummary = editSummarySection + genEditSummary().replace(' - ', '') + scriptAd;
        }
        
        // Warn if a username is hidden but shown in the summary
        if (summaryCustomized) {
            const hiddenUsernames = [];
            for (let i = 0; i < types.length; i++) {
                let type = types[i], inputVal = users[i], username;
                if (type === 'logid' && (username = getKeyByValue(Logids, inputVal)) && editSummary.indexOf(username) !== -1) hiddenUsernames.push(username); 
            }
            if (hiddenUsernames.length !== 0) {
                const warnText = '警告\n以下の利用者名は、フォーム上では隠されていますが、編集要約内では隠されていません。\n・' + hiddenUsernames.join('\n・') +
                                '\nこのまま続行する場合は OK を、中止する場合は Cancel を押してください。';
                if (confirm(warnText) === false) return;
            }
        }

        // Get text to add to the page
        var reportText = '';
        const UserAN = '{{UserAN|t=TYPE|USER}}';
        if (users.length < 2) { // If user to report is just one
            reportText = '\* ' + UserAN.replaceAllANR('TYPE', types[0], 'USER', users[0]) + ' - ' + reason;
        } else { // If two or more
            for (let i = 0; i < users.length; i++) {
                reportText += '\* ' + UserAN.replaceAllANR('TYPE', types[i], 'USER', users[i]) + '\n';
            }
            reportText += ': ' + reason;
        }

        // Return values
        return {
            'users': users,
            'types': types,
            'pageToEdit': debugMode.editTarget ? TEST : pageToEdit,
            'sectionToEdit': sectionToEdit,
            'wikiPagename': debugMode.editTarget ? TEST + '#' + sectionToEdit : pageToEdit + '#' + sectionToEdit,
            'reportToANS': reportToANS,
            'editSummary': debugMode.editSummary ? 'Test edit via mediawiki API' + scriptAd : editSummary,
            'reportText': reportText
        }
    }

    // Function for the 'preview' button of the dialog
    function preview() {

        // Check if the necessary fields are filled and get edit information
        const ep = editPrep();
        if (!ep) return;

        // Preview dialog contour
        const ANSMisc = `<a href="${mw.util.getUrl('WP:AN/S#OTH')}" target="_blank">WP:AN/S#その他</a>`;
        const previewDiv =
        '<div id="anr-preview-dialog" title="AN Reporter Preview" style="max-height: 80vh;">' +
        '   <div id="anr-preview-header" style="padding: 0.5em;">' +
        '       <p id="anr-preview-loading">' +
        `           プレビューを読み込み中${toggleLoadingSpinner('add')}` +
        '       </p>' +
        '       <p id="anr-preview-warning" style="display: none;">' +
        '           注意1: このプレビュー上のリンクは全て新しいタブで開かれます' +
        '              <br>' +
        `           注意2: 報告先が ${ANSMisc} の場合、このプレビューには表示されませんが「他X月X日」のヘッダーは必要に応じて自動挿入されます` +
        '       </p>' +
        '   </div>' +
        '   <div id="anr-preview-body" style="display: none; font-size: 1.1em; padding-top: 1em; border-top: 1px solid silver;">' +
        '       <div id="anr-preview-text" style="border: 1px solid silver; padding: 0.2em 0.5em; background: white;">' +
        //          previewHtml
        '       </div>' +
        '       <div id="anr-preview-summary" style="margin-top: 0.8em; border: 1px solid silver; padding: 0.2em 0.5em; background: white;">' +
        //          summaryHtml
        '       </div>' +
        '   </div>' +
        '</div>';

        // Show preview dialog
        $('body').append(previewDiv);
        $('#anr-preview-dialog').dialog({
            'height': 'auto',
            'width': $('#content').width() * 0.8,
            'modal': true,
            'open': async function(){

                // Initialize the design of the dialog
                dialogCSS();

                // Convert text on the dialog to html
                const parsed = await convertWikitextToHtmlFormat(ep.reportText, ep.editSummary);
                if (parsed) {

                    const previewHtml = parsed.htmltext;
                    const summaryHtml = parsed.htmlsummary.replaceAllANR('API', ep.pageToEdit);
                    $('#anr-preview-text').append(previewHtml);
                    $('#anr-preview-summary').append(summaryHtml);
                    $('.autocomment a').css('color', 'gray'); // Change color of section spec in summary
                    $('#anr-preview-dialog a').attr('target', '_blank'); // Open all links on a new tab
                    $('#anr-preview-body').css('display', 'block');
                    $('#anr-preview-loading').remove();
                    $('#anr-preview-warning').css('display', 'inline');
                    centerDialog();

                } else {
                    $('#anr-preview-loading').text('プレビューの読み込みに失敗しました').css('color', 'MediumVioletRed');
                    centerDialog();
                    setTimeout(function(){
                        $('#anr-preview-dialog').dialog('close');
                    }, 5000);
                }

            },
            'buttons': [{
                'text': '閉じる',
                'click': function(){
                    $(this).dialog('close');
                }
            }]
        });

    }

    /**
     * Function to convert wikitext to its HTML format
     * @param {string} wikitext The wikitext to convert
     * @param {string} wikisummary The summary to convert
     * @returns {Promise<{htmltext: string, htmlsummary: string}>}
     */
    function convertWikitextToHtmlFormat(wikitext, wikisummary) {
        return new Promise(function(resolve) {
            new mw.Api().post({
                'action': 'parse',
                'text': wikitext,
                'summary': wikisummary,
                'contentmodel': 'wikitext',
                'prop': 'text',
                'disableeditsection': true,
                'formatversion': 2
            }).then(function(res) {
                resolve({
                    'htmltext': res.parse.text,
                    'htmlsummary': res.parse.parsedsummary
                });
            }).catch(function(code, err) {
                console.log(err.error.info);
                resolve();
            });
        });
    }

    // Function for the 'report' button of the dialog
    function report() {

        // Check if the necessary fields are filled and get edit information
        const ep = editPrep();
        if (!ep) return;
        
        // Change dialog content
        $('#anr-modal-dialog')
            .dialog({
                'width': $(this).innerWidth(), // Absolute width
                'buttons': [] // Hide buttons
            })
            .append('<div class="anr-editing">') // Append div to show edit status
            .find('form').css('display', 'none'); // Hide dialog content

        reportUsers(ep);
        addUsersToWatchlist(ep);

    }

    // Function to execute report
    async function reportUsers(ep) {

        // Check the block status of the reportees if the checkbox is checked
        var blocked = [];
        if ($('#anr-blockstatus-checkbox').is(':checked')) {
            blocked = await preeditBlockStatusQuery(ep); // Query who's blocked
        }

        if (blocked.length !== 0) { // If any of the reportees is blocked

            // Update dialog buttons
            $('#anr-modal-dialog').dialog({
                'buttons': [{
                    'text': '続行',
                    'click': function(){
                        $(this).dialog({'buttons': [] });
                        reportUsers2(ep);
                    }
                }, {
                    'text': '戻る',
                    'click': function(){
                        $(this).find('form').css('display', 'block');
                        $('.anr-editing').remove();
                        $(this).dialog({
                            'width': 'auto',
                            'buttons': [{
                                'text': 'プレビュー',
                                'click': preview
                            }, {
                                'text': '報告',
                                'click': report
                            }]
                        });
                    }
                }, {
                    'text': '中止',
                    'click': function(){
                        $(this).dialog('close');
                    }
                }] 
            });

        } else { // If no one is blocked
            reportUsers2(ep);
        }
    }

    async function reportUsers2(ep) {

        // Check duplicate reports if the checkbox is checked
        if ($('#anr-duplicatereport-checkbox').is(':checked')) {
            var dr = await preeditDuplicateReportQuery(ep);
        }
        if (typeof dr === 'undefined') var dr = {};

        switch(dr.wikitext) {
            case null: // Error occurred
                return;
            case undefined: // The checkbox is unchecked or no duplicate report found
                reportUsers3(ep);
                return;
            default: // Possible duplicate reports present

                // Update dialog buttons
                $('#anr-modal-dialog').dialog({
                    'buttons': [{
                        'text': '確認',
                        'click': function() {
                            previewDuplicateReports(dr.wikitext, dr.dupUsernames);
                        }
                    }, {
                        'text': '続行',
                        'click': function(){
                            $(this).dialog({'buttons': [] });
                            reportUsers3(ep);
                        }
                    }, {
                        'text': '戻る',
                        'click': function(){
                            $(this).find('form').css('display', 'block');
                            $('.anr-editing').remove();
                            $(this).dialog({
                                'width': 'auto',
                                'buttons': [{
                                    'text': 'プレビュー',
                                    'click': preview
                                }, {
                                    'text': '報告',
                                    'click': report
                                }]
                            });
                        }
                    }, {
                        'text': '中止',
                        'click': function(){
                            $(this).dialog('close');
                        }
                    }] 
                });

        }

    }

    /**
     * Function to check block status before edit
     * @returns {Array} [] if no one is blocked, [user1, user2...] if someone is blocked
     */
    async function preeditBlockStatusQuery(ep) {

        // Can't check block status if the input values are only of t=diff or t=none
        var proceed;
        for (let i = 0; i < ep.types.length; i++) {
            if (ep.types[i] !== 'diff' && ep.types[i] !== 'none') {
                proceed = true;
                break;
            }
        }
        if (!proceed) {
            $('.anr-editing').append('<p>ブロックチェックはスキップされました</p>');
            return [];
        }

        // Update message on the dialog
        var msg = `<p>報告対象者のブロック情報を取得しています${toggleLoadingSpinner('add')}</p>`;
        $('.anr-editing').append(msg);

        // Extract users and IPs from the array
        const usersForBlockCheck = [];
        for (let i = 0; i < ep.users.length; i++) {
            const inputVal = ep.users[i];
            switch(ep.types[i]) {
                case 'UNL':
                case 'User2':
                case 'IP2':
                    if (!isInArray(inputVal, usersForBlockCheck)) usersForBlockCheck.push(inputVal);
                    break;
                case 'logid':
                    let username;
                    if ((username = getKeyByValue(Logids, inputVal)) !== undefined) { // If the logid can be converted to a username
                        if (!isInArray(username, usersForBlockCheck)) usersForBlockCheck.push(username);
                    }
                    break;
                default: // Do nothing if t=diff or t=none (impossible to check block status)
            }
        }

        // Check if any of the users is blocked
        const blocked = await getBlocked(usersForBlockCheck);

        // If any of the users is blocked
        if (blocked.length !== 0) {

            // Update message on the dialog
            msg =
                toggleLoadingSpinner('remove') +
                `<p style="color: MediumVioletRed">ブロック済みの利用者を検出しました</p>`;
            $('.anr-editing').append(msg);
            
            // Update block status links on the dialog
            $('#anr-user-div :text').each(function() { // Loop through all inputs
                const inputID = '#' + $(this).attr('id');
                const inputVal = $(inputID).val().trimANR();
                const $bsLinkDiv = $(inputID.replace('input', 'blockstatus-div'));
                const $bsLink = $(inputID.replace('input', 'blockstatus'));

                $bsLinkDiv.css('display', 'none'); // Temporarily hide the div
                if (isInArray(inputVal, blocked)) {
                    $bsLink.attr('href', mw.util.getUrl('特別:投稿記録/' + inputVal));
                    $bsLinkDiv.css('display', 'block');
                }
            });

        } else {

            // Update message on the dialog
            msg =
                toggleLoadingSpinner('remove') +
                `<p style="color: MediumSeaGreen">ブロック済みの利用者は検出されませんでした</p>`;
            $('.anr-editing').append(msg);

        }
        return blocked;

    }

    /**
     * Function to check duplicate reports
     * @returns {Promise<{wikitext: string, dupUsernames: Array}>} wikitext === null if error occurs, undefined if no duplicate report is found,
     * SECTIONTEXT to fetch preview from if there're potential duplicate reports. If SECTIONTEXT is returned, dupUsernames ===
     * [username1, username2...], without logids that can be converted to usernames.
     */
    async function preeditDuplicateReportQuery(ep) {

        // Update message on the dialog
        var msg = `<p>重複報告情報を取得しています${toggleLoadingSpinner('add')}</p>`;
        $('.anr-editing').append(msg);

        // Get sections and the whole wikitext of the page to which to report
        const parsed = await parsePage(ep.pageToEdit, 'wikitext|sections');
        const sections = parsed.sections, wikitext = parsed.wikitext;
        const sectiontitles = [];
        const sectionheaders = ['']; // Array of equal-enclosed section headers (e.g. == SECTION ==) (Note: the top section has no header, thus arr[0] = '')

        // Get section titles and their corresponding equal-enclosed wikitext
        for (let i = 0; i < sections.length; i++) {
            const section = sections[i];
            if (section.index.indexOf('T') === -1) { // If the section isn't a transcluded one
                sectiontitles.push(section.line); // Get the title of the section
                if (section.level == 2) { // Get equal-enclosed section headers
                    sectionheaders.push('== ' + section.line + ' ==');
                } else if (section.level == 3) {
                    sectionheaders.push('=== ' + section.line + ' ===');
                } else if (section.level == 4) {
                    sectionheaders.push('==== ' + section.line + ' ====');
                } else if (section.level == 5) {
                    sectionheaders.push('===== ' + section.line + ' =====');
                }
            }
        }

        // The sections in which to search for duplicate reports    
        const tarSectionsI = [
            getSectionI(true),
            getSectionI(false),
            '不適切な利用者名',
            '公開アカウント',
            '公開プロキシ・ゾンビマシン・ボット・不特定多数',
            '犯罪行為またはその疑いのある投稿'
        ];
        const tarSectionsS = [
            '著作権侵害・犯罪予告',
            '名誉毀損・なりすまし・個人情報',
            '妨害編集・いたずら',
            'その他',
            ep.sectionToEdit
        ];
        const tarSections3RR = ['3RR'];
        const tarSectionsSubpagedLTA = ['新規依頼'];

        var tarSections;
        switch(ep.pageToEdit) {
            case ANI:
                tarSections = tarSectionsI;
                break;
            case ANS:
                tarSections = tarSectionsS;
                break;
            case AN3RR:
                tarSections = tarSections3RR;
                break;
            case Iccic:
            case ISECHIKA:
            case KAGE:
            case KIYOSHIMA:
            case SHINJU:
                tarSections = tarSectionsSubpagedLTA;
                break;
            case TEST: // For debugging
                eval(`tarSections = ${debugMode.drPreviewSections}`);
                break;
            default: // Error: Target pagename not defined
                msg = 
                    toggleLoadingSpinner('remove') +
                    '<p style="color: MediumVioletRed">致命的なエラーが発生しました</p><br>' +
                    `<p>${developerLink}に、<u>${ep.wikiPagename}</u>への報告においてこのエラーが発生したことの報告をお願いします。</p>` +
                    manualEdit(ep);
                $('.anr-editing').append(msg);
                editDone(ep, true);  
                return {'wikitext': null};
        }

        // Error handler for when pageToEdit doesn't have sections that it's supposed to have
        if (!arrayIsInArray(tarSections, sectiontitles)) {
            sectionNotFound(ep);
            return {'wikitext': null};
        }

        // Separate the content of the parsed page into the content of each section
        var sectionsPiped = sectiontitles.join('|').escapeRegExpANR();
        var regExp = new RegExp(`={2,5}\\s*(?:${sectionsPiped})\\s*={2,5}`, 'g');
        var sectionContent = wikitext.split(regExp); // Array of the content of each section, without section headers
        for (let i = 0; i < sectionContent.length; i++) { // Re-add to sectionContent the '== TITLE ==' header that's been removed by the split() above
            sectionContent[i] = sectionheaders[i] + sectionContent[i];
        }

        // Remove the contents of irrelevant sections from the array 'sectionContent'
        sectionsPiped = tarSections.join('|').escapeRegExpANR();
        regExp = new RegExp(`={2,5}\\s*(?:${sectionsPiped})\\s*={2,5}`, 'g');
        for (let i = sectionContent.length -1; i >= 0; i--) {
            if (sectionContent[i].search(regExp) === -1) sectionContent.splice(i, 1);
        }

        // Get usernames for duplicate report check
        const usersDR = ep.users; // Input values without duplicates: usersDR will be used for duplicate report check
        for (let i = 0; i < ep.types.length; i++) { // Loop through all the input values
            let type = ep.types[i], username, logid, ip;
            switch(type) {
                case 'UNL': // Registered users need duplicate report check also for their logids
                case 'User2':
                    if (logid = Logids[username = usersDR[i]]) { // If the object knows the required logid, just push it into the array
                        usersDR.push(logid);
                    } else { // If not, get the logid from the API and push it into the array (if the response isn't undefined)
                        if (logid = await getLogid(username)) {
                            Logids[username] = logid; // Save the logid into the object
                            usersDR.push(logid);
                        }
                    }
                    break;
                case 'IP2': // IPv6s need to be case-insensitive
                    if (mw.util.isIPv6Address(ip = usersDR[i], true) && ip.match(/[A-Z]/i)) { // If the IP is an IPv6 and if it contains alphabets
                        if (ip.match(/[A-Z]/)) { // If the IPv6 is in uppercase, push its lowercase ver, if in lowercase, push the uppercase ver
                            usersDR.push(ip.toLowerCase());
                        } else {
                            usersDR.push(ip.toUpperCase());
                        }
                    }
                    break;
                case 'logid': // The corresponding username needs to be checked
                    if (username = getKeyByValue(Logids, logid = usersDR[i])) usersDR.push(username);
                    break;
                default: // t=diff or t=none: no need to do anything because the relevant input value is already in the array
            }
        }

        // Extract UserAN templates and find duplicate reports
        const dupTemplates = [], dupUsernames = [];
        for (let i = sectionContent.length -1; i >= 0; i--) { // Loop through all section contents

            const templates = findTemplates(sectionContent[i], 'useran'); // Extract UserAN templates as an array
            let dupUsername, duplicateFound;

            if (templates.length !== 0) { // If the section content contains at least one UserAN
                for (let j = templates.length -1; j >= 0; j--) { // Loop through all the occurences of UserAN
                    if (dupUsername = stringContainsElementInArray(templates[j], usersDR)) { // If there's a duplciate report
                        if (!isInArray(templates[j], dupTemplates)) dupTemplates.push(templates[j]); // List the UserAN as a duplicate
                        if (!isInArray(dupUsername, dupUsernames)) dupUsernames.push(dupUsername); // List the duplicate username
                        duplicateFound = true;
                    }
                }
            }
            if (!duplicateFound) sectionContent.splice(i, 1); // Remove the section text from the array if it doesn't involve duplicate reports 

        }

        // Return text and update dialog
        if (sectionContent.length === 0) { // If there's no duplicate report

            msg = `<p style="color: MediumSeaGreen">重複報告は検出されませんでした${toggleLoadingSpinner('remove')}</p>`;
            $('.anr-editing').append(msg); // Update message on the dialog
            return; // Return undefined

        } else { // If there're duplicate reports

            msg = `<p style="color: MediumVioletRed">重複報告の可能性があります${toggleLoadingSpinner('remove')}</p>`;
            $('.anr-editing').append(msg);

            // Highlight all the duplciate UserAN occurences
            sectionContent = sectionContent.join(''); // Merge the separate sections
            for (let i = 0; i < dupTemplates.length; i++) {
                sectionContent = sectionContent.replaceAllANR(dupTemplates[i], `<span style="background-color: ${anrConfig.headerColor}">${dupTemplates[i]}</span>`);
            }

            // Return wikitext to fetch preview from
            return {
                'wikitext': sectionContent,
                'dupUsernames': dupUsernames
            };

        }

    }

    // Function to show error message if sections that must be there are not found
    function sectionNotFound(ep) {
        const msg =
            toggleLoadingSpinner('remove') +
            '<p style="color: MediumVioletRed">取得に失敗しました</p>' +
            '<p>指定されたセクションが見つかりませんでした</p>' +
            '<br>' +
            '<p>考えられる原因:</p>' + 
            `<p>1. 編集先のページの節構成が変更された</p>` +
            `<p>2. 通信に失敗した</p>` +
            `<p>3. スクリプトのバグ</p>` +
            manualEdit(ep);
        $('.anr-editing').append(msg);
        editDone(ep, true);
    }

    // Function to generate the html for the manual edit helper tab
    function manualEdit(ep) {
        const meHtml =
            '<br>' +
            '<p>手動編集用:</p>' +
            `<textarea disabled class="anr-dialog-textarea" rows="4">${ep.reportText}</textarea>` +
            '<br>' +
            '<p>要約:</p>' + 
            `<textarea disabled class="anr-dialog-textarea" rows="2">${ep.editSummary.replace(scriptAd, '')}</textarea>`;
        return meHtml;
    }

    /**
     * Action for when edit is done (in any way)
     * @param {Object} ep 
     * @param {boolean} editFailed 
     */
    function editDone(ep, editFailed) {

        const btns = [], $dialog = $('#anr-modal-dialog');

        // Button to jump to the report page
        if (editFailed || mw.config.get('wgPageName') !== ep.pageToEdit) { // Show the button if the edit failed or if the user is NOT on the page
            const destBtn = {
                'text': '報告先',
                'click': function(){
                    window.open(mw.util.getUrl(ep.wikiPagename), '_blank');
                }
            };
            btns.push(destBtn);
        }

        // Button to close the dialog (always shown)
        const closeBtn = {
            'text': '閉じる',
            'click': function(){
                $dialog.dialog('close');
                var curPage = mw.config.get('wgPageName');
                if (curPage === ANI ||
                    curPage === ANS ||
                    curPage === AN3RR ||
                    curPage === Iccic ||
                    curPage === ISECHIKA ||
                    curPage === KAGE ||
                    curPage === KIYOSHIMA ||
                    curPage === SHINJU ||
                    curPage === TEST)
                {
                    location.reload(true);
                }
            }
        };
        btns.push(closeBtn);

        // Show the button(s) on the dialog
        $dialog.dialog({'buttons': btns});
        if (editFailed) centerDialog();

    }

    /**
     * Function to preview duplicate reports on a new dialog
     * @param {string} wikitext wikitext for preview (inherited from preeditDuplicateReportQuery)
     * @param {Array} dupUsernames usernames found to be duplicate reports (inherited from preeditDuplicateReportQuery)
     */
    function previewDuplicateReports(wikitext, dupUsernames) {

        // Duplicate usernames to show on the dialog (logids are to be shown in parentheses)
        const usernames = [];
        for (let i = 0; i < dupUsernames.length; i++) {
            let username, logid;
            if (username = getKeyByValue(Logids, logid = dupUsernames[i])) { // if the dupUsername is a logid and that can be converted to a username
                usernames.push(`${username} (${logid})`);
            } else if (logid = Logids[username = dupUsernames[i]]) { // if the dupUsername is a username and that can be converted to a logid
                usernames.push(`${username} (${logid})`);
            } else {
                usernames.push(username); // if else, just push the username into the array
            }
        }

        // Create dialog
        const duplicateReportPreviewDiv =
            '<div id="anr-drpreview-dialog" title="AN Reporter Duplicate Report Preview" style="max-height: 80vh;">' +
            '   <div id="anr-drpreview-header" style="padding: 0.5em;">' +
            '       <p id="anr-drpreview-loading">' +
            `           読み込み中${toggleLoadingSpinner('add')}` +
            '       </p>' +
            '       <p id="anr-drpreview-userlist" style="display: none; font-size: larger">' +
            '           <span style="font-weight: bold">重複報告の可能性のある値:</span>' +
            '           <br>' +
                        usernames.join(', ') +
            '       </p>' +
            '   </div>' +
            '   <div id="anr-drpreview-body" style="display: none; font-size: 1.1em; padding: 0.5em; border: 1px solid silver; background-color: white;">' +
            //      Added when the dialog is opened
            '   </div>' +
            '</div>';
        $('body').append(duplicateReportPreviewDiv);

        // Show preview dialog
        $('#anr-drpreview-dialog').dialog({
            'height': 'auto',
            'width': $('#content').width() * 0.8,
            'modal': true,
            'open': async function(){

                // Initialize the design of the dialog
                dialogCSS();

                // Convert the wikitext to an html form
                const wikitextInHtml = await convertWikitextToHtmlFormat(wikitext.trim(), '');
                if (wikitextInHtml) {
                    $('#anr-drpreview-body').append(wikitextInHtml.htmltext);
                    $('#anr-drpreview-dialog a').attr('target', '_blank'); // Open all links on a new tab
                    $('#anr-drpreview-body').css('display', 'block');
                    $('#anr-drpreview-loading').remove();
                    $('#anr-drpreview-userlist').css('display', 'inline');
                    centerDialog();
                } else {
                    $('#anr-drpreview-loading').text('読み込みに失敗しました').css('color', 'MediumVioletRed');
                    centerDialog();
                    setTimeout(function(){
                        $('#anr-drpreview-dialog').dialog('close');
                    }, 10000);
                }

            },
            'buttons': [{
                'text': '閉じる',
                'click': function(){
                    $(this).dialog('close');
                }
            }]
        });

    }

    async function reportUsers3(ep) {

        const ts = await getTimestamps(ep);
        if (!ts) return;
        const baseTS = ts.baseTS, curTS = ts.curTS;

        const sectionNum = await getSectionNumber(ep);
        if (!sectionNum) return;

        const reportText = await getReportText(ep, sectionNum);
        if (!reportText) return;

        edit(ep, sectionNum, reportText, baseTS, curTS);

    }

    // Function to get the latest revision of the administrator's noticeboard
    function getTimestamps(ep) {
        return new Promise(function(resolve) {

            var msg = `<p>最新版を取得しています${toggleLoadingSpinner('add')}</p>`;
            $('.anr-editing').append(msg);

            new mw.Api().get({
                'action': 'query',
                'titles': ep.pageToEdit,
                'prop': 'revisions',
                'curtimestamp': true,
                'formatversion': 2
            }).then(function(res){

                var resPages;
                if (res && res.query && (resPages = res.query.pages)) { // If the latest revision is successfully retrieved
                    if (!resPages[0].missing) { // .missing is true if the page doesn't exist, otherwise undefined

                        // Get the timestamps of the latest revision and the API query
                        const baseTS = resPages[0].revisions[0].timestamp; // The TS of the latest revision
                        const curTS = res.curtimestamp; // The TS of the API query

                        // Update message on the dialog
                        msg =
                            '<p style="color: MediumSeaGreen">取得に成功しました</p>' +
                            `<p>セクション番号を取得しています${toggleLoadingSpinner('move')}</p>`;
                        $('.anr-editing').append(msg);

                        // Return the timestamps as an object
                        resolve({
                            'baseTS': baseTS,
                            'curTS': curTS
                        });

                    } else { // If the page doesn't exist

                        msg = 
                            toggleLoadingSpinner('remove') +
                            '<p style="color: MediumVioletRed">エラー: 報告先のページが存在しません</p>' + 
                            manualEdit(ep);
                        $('.anr-editing').append(msg);
                        editDone(ep, true);
                        resolve();

                    }

                } else { // If revision retrieval fails
                    queryFailed(ep);
                    resolve();
                }

            });

        });
    }

    function addUsersToWatchlist(ep) {
        if (!$('#anr-watchlist-checkbox').is(':checked')) return console.log('ウォッチリストへの追加設定はオフになっています。');
        return new Promise(function(resolve) {

            // Get pagenames to watch
            const pagenames = [];
            for (let i = 0; i < ep.types.length; i++) {
                const type = ep.types[i], user = ep.users[i];
                if (type === 'User2' || type === 'UNL' || type === 'IP2') {
                    if (!isInArray('利用者:' + user, pagenames)) pagenames.push('利用者:' + user);
                } else if (type === 'logid') {
                    let username;
                    if (username = getKeyByValue(Logids, user) && !isInArray('利用者:' + username, pagenames)) pagenames.push('利用者:' + username);
                }
            }

            // Add the pages to watchlist 
            new mw.Api().get({
                'action': 'query',
                'meta': 'tokens',
                'type': 'watch'
            }).then(function(res){

                const token = res.query.tokens.watchtoken;
                if (!token) resolve(mw.log.error('ウォッチトークンの取得に失敗しました'));

                new mw.Api().post({
                    'action': 'watch',
                    'titles': pagenames.join('|'),
                    'token': token,
                    'formatversion': 2
                }).then(function(res) {
                    resolve(console.log('以下のページをウォッチリストに追加しました:\n' + pagenames.join(', ')));
                }).catch(function(code, err) {
                    resolve(mw.log.error('ウォッチリストへの追加に失敗しました:\n' + err.error.info));
                });

            });

        });
    }

    // Function to show message when edit attempt is done
    function queryFailed(ep) {
        const msg = 
            toggleLoadingSpinner('remove') +
            '<p style="color: MediumVioletRed">取得に失敗しました</p>' +
            manualEdit(ep);
        $('.anr-editing').append(msg);
        editDone(ep, true);
    }

    // Function to get the section number from the section title 
    async function getSectionNumber(ep) {

        const parse = await parsePage(ep.pageToEdit, 'sections');
        var resSect, sectionNum;
        if (parse && (resSect = parse.sections)) { // If the section list is successfully retrieved

            // Get the titles of all sections and their section numbers
            for (let i = 0; i < resSect.length; i++) {
                if (resSect[i].line === ep.sectionToEdit) {
                    sectionNum = resSect[i].index;
                    break;
                }
            }

            // Return a section number if the section is found, undefined if not
            if (!sectionNum) {
                sectionNotFound(ep);
                return;
            } else {
                const msg =
                    '<p style="color: MediumSeaGreen">取得に成功しました</p>' +
                    `<p>最新版のテキストを取得しています${toggleLoadingSpinner('move')}</p>`;
                $('.anr-editing').append(msg);
                return sectionNum;
            }

        } else { // If the section list retrieval fails
            queryFailed(ep);
            return;
        }

    }

    // Function to get the text to replace with the current text in the section
    async function getReportText(ep, sectionNum) {

        const parse = await parsePage(ep.pageToEdit, 'wikitext', sectionNum);
        if (parse) {

            // Update message
            var msg =
                '<p style="color: MediumSeaGreen">取得に成功しました</p>' +
                `<p>報告を試みています${toggleLoadingSpinner('move')}</p>`;
            $('.anr-editing').append(msg);

            // Get the whole text to append
            const wikitext = parse.wikitext;
            var reportText;
            if (ep.reportToANS) { // If the target is WP:AN/S

                // Add div if the target section is 'その他' but lacks div for the current date
                const miscHeader = `{{bgcolor|#eee|{{Visible anchor|他${today()}}}|div}}`;
                if (ep.sectionToEdit === 'その他' && wikitext.indexOf(miscHeader) === -1) ep.reportText = '; ' + miscHeader + '\n\n' + ep.reportText;

                // Get the report text to submit
                let sockInfo = findTemplates(wikitext, 'sockinfo'); // Array
                if (sockInfo.length === 1) { // One section on WP:AN/S should have one SockInfo
                    sockInfo = sockInfo[0];
                    const sockInfoNoClosure = sockInfo.substring(0, sockInfo.length - 2).trimANR();
                    reportText = wikitext.replace(sockInfo, sockInfoNoClosure + '\n\n' + ep.reportText + '\n\n}}');
                    return reportText;
                } else { // There's a problem with SockInfo
                    msg = // Show error and quit the procedure
                        toggleLoadingSpinner('remove') +
                        '<p style="color: MediumVioletRed">報告に失敗しました</p><br>' +
                        `<p>{{SockInfo}}がない、または複数個あるか、テンプレートの「報告」引数の値が不正です</p>` +
                        manualEdit(ep);
                    $('.anr-editing').append(msg);
                    editDone(ep, true);
                    return;
                }

            } else { // If the target is WP:AN/I or WP:AN/3RR
                reportText = wikitext.trimANR() + '\n\n' + ep.reportText;
                return reportText;
            }

        } else { // If wikitext retrieval fails
            queryFailed(ep);
            return;
        }

    }

    // Function to edit the page
    function edit(ep, sectionNum, reportText, baseTS, curTS) {

        new mw.Api().post({
            'action': 'edit',
            'title': ep.pageToEdit,
            'section': sectionNum,
            'text': reportText,
            'summary': ep.editSummary,
            'basetimestamp': baseTS,
            'starttimestamp': curTS,
            'token': debugMode.causeIntentionalError ? '': mw.user.tokens.get('csrfToken'),
            'format': 'json'
        }).then(function(res) {

            toggleLoadingSpinner('remove');
            $('.anr-editing').append(`<p style="color: MediumSeaGreen">報告が完了しました</p>`);
            editDone(ep, false);

        }).catch(function(code, err) {
            var msg;
            if (err && err.error) {

                // Show the details of the error
                msg =
                    toggleLoadingSpinner('remove') +
                    '<p style="color: MediumVioletRed">報告に失敗しました</p><br>' +
                    '<p>詳細:</p>' + 
                    `<p>${err.error.info}</p>` +
                    manualEdit(ep);
                $('.anr-editing').append(msg);
                editDone(ep, true);

            } else { // If unknown error occurred

                msg =
                    toggleLoadingSpinner('remove') +
                    '<p style="color: MediumVioletRed">不明なエラーが発生しました</p>' + 
                    manualEdit(ep);
                $('.anr-editing').append(msg);
                editDone(ep, true);

            }
        });

    }

    /** 
    * Function to extract templates from wikitext
    * @param {string} text The text in which to search for templates
    * @param {string} templateName [Optional] Specify the template name (case-insensitive)
    * @returns {Array} An array of the extracted templates
    */
    function findTemplates(text, templateName) {

        // Split the text with '{{', the head delimiter of templates
        const tempInnerContent = text.split('{{'); // Note: tempInnerContent[0] is always an empty string or a string that has nothing to do with templates
        const templates = []; // The array of extracted templates to return

        // Extract templates from the text
        if (tempInnerContent.length === 0) { // If the text has no tempalte in it

            return templates; // Return an empty array

        } else { // If the text has some templates in it

            const nest = []; // Stores the element number of tempInnerContent if the element involves nested templates
            for (let i = 1; i < tempInnerContent.length; i++) { // Loop through all elements in tempInnerContent (except tempInnerContent[0])

                let tempTailCnt = (tempInnerContent[i].match(/\}\}/g) || []).length; // The number of '}}' in the split segment
                let temp = ''; // Temporary escape hatch for templates

                if (tempTailCnt === 0) { // The split segment not having any '}}' means that it nests another template

                    nest.push(i); // Push the element number into the array

                } else if (tempTailCnt === 1) { // The split segment itself is the whole inner content of one template

                    temp = '{{' + tempInnerContent[i].split('}}')[0] + '}}';
                    if (!isInArray(temp, templates)) templates.push(temp);

                } else if (tempTailCnt > 1) { // The split segment is part of more than one template (e.g. TL2|...}}...}} )

                    for (let j = 0; j < tempTailCnt; j++) { // Loop through all the nests

                        if (j === 0) { // The innermost template

                            temp = '{{' + tempInnerContent[i].split('}}')[j] + '}}'; // Same as when tempTailCnt === 1
                            if (!isInArray(temp, templates)) templates.push(temp);

                        } else { // Nesting templates

                            const elNum = nest[nest.length -1]; // The start of the nesting template
                            nest.pop();
                            const nestedTempInnerContent = tempInnerContent[i].split('}}');

                            temp = '{{' + tempInnerContent.slice(elNum, i).join('{{') + '{{' + nestedTempInnerContent.slice(0, j + 1).join('}}') + '}}';
                            if (!isInArray(temp, templates)) templates.push(temp);

                        }

                    }

                }

            }

            // Check if the optional parameter is specified
            if (templateName && templates.length !== 0) {
                const templateRegExp = new RegExp(templateName, 'i');
                for (let i = templates.length -1; i >= 0; i--) {
                    // Remove the template from the array if it's not an instance of the specified template
                    if (templates[i].split('|')[0].search(templateRegExp) === -1) templates.splice(i, 1);
                }
            }

            return templates;
        }
    }

    // Function to generate edit summary automatically
    function genEditSummary() {

        const links = [];
        $('#anr-user-div :text').each(function() { // Loop through all inputs
            const inputID = '#' + $(this).attr('id');
            const type = $(inputID.replace('input', 'select')).children('option').filter(':selected').text(); // UserAN type specified in the dropdown
            const reportee = $(this).val().trimANR(); // Username

            let link;
            if (reportee !== '') { // Skip if the input value is a null string
                switch(type) { // Get appropriate links depending on the UserAN type
                    case 'UNL':
                    case 'User2':
                    case 'IP2':
                        link = `[[特別:投稿記録/${reportee}|${reportee}]]`;
                        break;
                    case 'logid':
                        link = `[[特別:転送/logid/${reportee}|Logid/${reportee}]]`;
                        break;
                    case 'diff':
                        link = `[[特別:差分/${reportee}|差分/${reportee}]]の投稿者`;
                        break;
                    default:
                        link = reportee;
                }
                if (!isInArray(link, links)) links.push(link); // Push the link into the array
            }
        });

        // Get edit summary
        var summary = '';
        switch(true) {
            case links.length === 0:
                break;
            case links.length === 1:
                summary = '+' + links[0] + ' - ';
                break;
            case links.length > 1 && 5 > links.length:
                summary = '+' + links.join(', ') + ' - ';
                break;
            default: 
                summary = '+' + links.length + ' - ';
        }
        return summary;

    }

    /**
     * Function to get the current date and the section name on WP:AN/I to which to report users
     * @param {boolean} last if true, returns the name of the last section
     * @returns {string} section name
     */
    function getSectionI(last){

        const d = new Date();
        if (last) {
            let subtract = 5;
            if (d.getDate() === 1 || d.getDate() === 2) {
                subtract = 3;
            } else if (d.getDate() === 31) {
                subtract = 6;
            }
            d.setDate(d.getDate() - subtract);
        }

        var sectionName;
        switch(true) {
            case (1 <= d.getDate() && d.getDate() <= 5):
                sectionName = `${d.getFullYear()}年${d.getMonth() + 1}月1日 - 5日新規報告`;
                break;
            case (6 <= d.getDate() && d.getDate() <= 10):
                sectionName = `${d.getFullYear()}年${d.getMonth() + 1}月6日 - 10日新規報告`;
                break;
            case (11 <= d.getDate() && d.getDate() <= 15):
                sectionName = `${d.getFullYear()}年${d.getMonth() + 1}月11日 - 15日新規報告`;
                break;
            case (16 <= d.getDate() && d.getDate() <= 20):
                sectionName = `${d.getFullYear()}年${d.getMonth() + 1}月16日 - 20日新規報告`;
                break;
            case (21 <= d.getDate() && d.getDate() <= 25):
                sectionName = `${d.getFullYear()}年${d.getMonth() + 1}月21日 - 25日新規報告`;
                break;
            case (26 <= d.getDate() && d.getDate() <= lastDay(d.getFullYear(), d.getMonth())):
                sectionName = `${d.getFullYear()}年${d.getMonth() + 1}月26日 - ${lastDay(d.getFullYear(), d.getMonth())}日新規報告`;
                break;
            default:
        }
        return sectionName;

    }

    // Function to check if a user exists locally
    function userExists(username) {
        return new Promise(function(resolve) {
            new mw.Api().get({
                'action': 'query',
                'list': 'users',
                'ususers': username,
                'formatversion': 2
            }).then(function(res){
                resolve(res.query.users[0].userid !== undefined); // True if the user exists and false if not
            });
        });
    }

    // Function to manipulate dropdown options for UserAN types (also maniputes show/hide of checkbox)
    var updateTypeDropdownTimeout;
    function updateTypeDropdown(inputID) {

        const tarVal = $(inputID).val().trimANR(); // The value typed into the input
        const selectID = inputID.replace('input', 'select'); // #anr-userX-select
        const checkboxDivID = inputID.replace('input', 'checkbox-div'); // #anr-userX-checkbox-div
        const checkboxID = inputID.replace('input', 'checkbox'); // #anr-userX-checkbox
        const idlinkDivID = inputID.replace('input', 'idlink-div'); // #anr-userX-idlink-div

        clearTimeout(updateTypeDropdownTimeout); // Run the async function only once when there's been no input change for 0.35 seconds
        updateTypeDropdownTimeout = setTimeout(async function(){

            if (tarVal === '') { // if the field is blanked

                $(selectID).prop('disabled', true).children('.anr-opt-none').prop('selected', true); // Disable dropdown and select 'none'
                $(checkboxDivID).css('display', 'none'); // Hide 'hide username' checkbox
                $(checkboxID).prop('checked', false); // Uncheck the checkbox
                $(idlinkDivID).css('display', 'none'); // Hide logid/diff link
                toggleBlockStatusLink(inputID, true, false);

            } else { // if the field is filled

                if (mw.util.isIPAddress(tarVal, true)) { // if IP

                    $(selectID).prop('disabled', false); // enable dropdown (repeated to prevent a strange lag)
                    $(selectID).children('.anr-opt-UNL').prop('hidden', true);
                    $(selectID).children('.anr-opt-User2').prop('hidden', true);
                    $(selectID).children('.anr-opt-IP2').prop({'hidden': false, 'selected': true});
                    $(selectID).children('.anr-opt-logid').prop('hidden', true);
                    $(selectID).children('.anr-opt-diff').prop('hidden', true);
                    $(selectID).children('.anr-opt-none').prop('hidden', false);
                    $(checkboxDivID).css('display', 'none'); // hide 'hide username' checkbox
                    $(checkboxID).prop('checked', false); // uncheck the checkbox
                    $(idlinkDivID).css('display', 'none');
                    toggleBlockStatusLink(inputID, false, false);

                } else if (await userExists(tarVal)) { // if user

                    $(selectID).prop('disabled', false); // enable dropdown (repeated to prevent a strange lag)
                    $(selectID).children('.anr-opt-UNL').prop({'hidden': false, 'selected': true});
                    $(selectID).children('.anr-opt-User2').prop('hidden', false);
                    $(selectID).children('.anr-opt-IP2').prop('hidden', true);
                    $(selectID).children('.anr-opt-logid').prop('hidden', true);
                    $(selectID).children('.anr-opt-diff').prop('hidden', true);
                    $(selectID).children('.anr-opt-none').prop('hidden', false);
                    $(checkboxDivID).css('display', 'block'); // show 'hide username' checkbox
                    $(checkboxID).prop('checked', false); // uncheck the checkbox
                    $(idlinkDivID).css('display', 'none');
                    toggleBlockStatusLink(inputID, false, false);

                } else { // if something else (like random numbers or strings)

                    $(selectID).prop('disabled', false); // enable dropdown (repeated to prevent a strange lag)
                    $(selectID).children('.anr-opt-UNL').prop('hidden', true);
                    $(selectID).children('.anr-opt-User2').prop('hidden', true);
                    $(selectID).children('.anr-opt-IP2').prop('hidden', true);
                    $(selectID).children('.anr-opt-logid').prop('hidden', false);
                    $(selectID).children('.anr-opt-diff').prop('hidden', false);
                    $(selectID).children('.anr-opt-none').prop({'hidden': false, 'selected': true});
                    $(checkboxDivID).css('display', 'none'); // hide 'hide username' checkbox
                    $(idlinkDivID).css('display', 'none');
                    toggleBlockStatusLink(inputID, true, false);

                }

            }

        }, 350);
    }

    /** 
     * Function to show/hide 'This user has blocks' links
     * @param {string} inputID the ID of the input
     * @param {boolean} forceHide if true, just hide the block status link (for when t=diff and t=none; block check impossible)
     * @param {boolean} convertLogid if true, try to convert a logid to a username (for when t=logid; username is needed for block check)
     */
    function toggleBlockStatusLink(inputID, forceHide, convertLogid) {

        centerDialog();

        const inputVal = $(inputID).val().trimANR(); // The value in the input
        const $bsLinkDiv = $(inputID.replace('input', 'blockstatus-div')); // #anr-userX-blockstatus-div
        const $bsLink = $(inputID.replace('input', 'blockstatus')); // #anr-userX-blockstatus

        var username, logid;
        if (forceHide && convertLogid) { // t=logid
            // Check if the logid can be converted to a username and if it can, proceed to block check, and if it can't, just hide the block status link
            if (!(username = getKeyByValue(Logids, logid = inputVal))) {
                $bsLinkDiv.css('display', 'none');
                centerDialog();
                return;
            }
        } else if (forceHide) { // t=diff or t=none
            $bsLinkDiv.css('display', 'none'); // Hide the link div 
            centerDialog();
            return;
        } else { // t=UNL, t=User2, or t=IP2
            username = inputVal;
        }

        // Check the block status of the user, and if blocked, update the bsLink, or else, hide the link
        getBlocked([username]).then(function(blocked) {
            if (blocked.length !== 0) { // If the user is blocked
                $bsLink.attr('href', mw.util.getUrl('特別:投稿記録/' + username)); // Update the link
                $bsLinkDiv.css('display', 'block'); // Show the link div
            } else {
                $bsLinkDiv.css('display', 'none'); // Hide the link div
            }
            centerDialog();
        });

    }

    // Function to get an array of blocked users & IPs from an array of random users & IPs
    async function getBlocked(namesArr) {

        const users = [], ips = [];
        var blocked = [];

        // Sort names to users and IPs
        for (let i = 0; i < namesArr.length; i++) {
            if (mw.util.isIPAddress(namesArr[i], true)) { // Push IPs into the array
                ips.push(namesArr[i]);
            } else { // Push users into the array
                users.push(namesArr[i]);
            }
        }

        // Check who's (b)locked
        if (users.length !== 0 && ips.length !== 0) { // If namesArr contains both users and IPs

            // Check local block status
            blocked = blocked.concat(
                await getBlockedUsers(users),
                await getBlockedIps(ips)
            );

            // Remove users/IPs that are already in the array 'blocked' (to make the code faster)
            for (let i = users.length -1; i >= 0; i--) {
                if (isInArray(users[i], blocked)) {
                    users.splice(i ,1);
                }
            }
            for (let i = ips.length -1; i >= 0; i--) {
                if (isInArray(ips[i], blocked)) {
                    ips.splice(i ,1);
                }
            }

            // Check global (b)lock status
            blocked = blocked.concat(
                await getGloballyLockedUsers(users),
                await getGloballyBlockedIps(ips)
            );

        } else if (users.length !== 0) { // If namesArr only contains users

            // Check local block status
            blocked = blocked.concat(await getBlockedUsers(users));

            // Remove users that are already in the array 'blocked' (to make the code faster)
            for (let i = users.length -1; i >= 0; i--) {
                if (isInArray(users[i], blocked)) {
                    users.splice(i ,1);
                }
            }

            // Check global lock status
            blocked = blocked.concat(await getGloballyLockedUsers(users));

        } else if (ips.length !== 0) { // If namesArr only contains IPs

            // Check local block status
            blocked = blocked.concat(await getBlockedIps(ips));

            // Remove IPs that are already in the array 'blocked' (to make the code faster)
            for (let i = ips.length -1; i >= 0; i--) {
                if (isInArray(ips[i], blocked)) {
                    ips.splice(i ,1);
                }
            }

            // Check global block status
            blocked = blocked.concat(await getGloballyBlockedIps(ips));

        } else {
            // Do nothing
        }

        return blocked;

    }

    /**
     * Function to get an array of blocked users from an array of random users
     * @param {Array} usersArr 
     * @returns {Promise<Array>}
     */
    function getBlockedUsers(usersArr) { // Note: this function needs to be modified if there're cases in which the reportees are more than 50
        return new Promise(function(resolve) {
            if (usersArr.length === 0) resolve([]);
            new mw.Api().post({
                'action': 'query',
                'list': 'blocks',
                'bklimit': usersArr.length,
                'bkusers': usersArr.join('|'),
                'bkprop': 'user',
                'formatversion': 2
            }).then(function(res){
                const resBlk = res.query.blocks, blockedUsers = [];
                if (resBlk.length === 0) resolve(blockedUsers); // None of the users is blocked; return an empty array
                for (let i = 0; i < resBlk.length; i++) {
                    blockedUsers.push(resBlk[i].user); // Push blocked users into the array
                }
                resolve(blockedUsers); // Return e.g. [user1, user2...]
            });
        });
    }

    /**
     * Function to get an array of locked users from an array of random users
     * @param {Array} usersArr 
     * @returns {Promise<Array>}
     */
    async function getGloballyLockedUsers(usersArr) {
        if (usersArr.length === 0) return [];
        const lockedUsers = [];
        for (let i = 0; i < usersArr.length; i++) {
            const locked = await userIsLocked(usersArr[i]);
            if (locked) lockedUsers.push(usersArr[i]);
        }
        return lockedUsers;
    }

    /**
     * Function to check if a user is globally locked
     * @param {string} user 
     * @returns {Promise<boolean>}
     */
    function userIsLocked(user) {
        return new Promise(function(resolve) {
            new mw.Api().get({
                action: 'query',
                list: 'globalallusers',
                agulimit: 1,
                agufrom: user,
                aguto: user,
                aguprop: 'lockinfo'
            }).then(function(res) {
                const resLck = res.query.globalallusers;
                if (resLck.length === 0) resolve(false); // The global user doesn't exist
                resolve(resLck[0].locked !== undefined); // resLck[0].locked === '' if locked, otherwise undefined
            });
        });
    }

    /**
     * Function to get an array of locally blocked IPs from an array of random IPs
     * @param {Array} ipsArr
     * @returns {Promise<Array>}
     */
    async function getBlockedIps(ipsArr) {
        if (ipsArr.length === 0) return [];
        const blockedIps = [];
        for (let i = 0; i < ipsArr.length; i++) {
            const blocked = await ipIsBlocked(ipsArr[i]);
            if (blocked) blockedIps.push(ipsArr[i]);
        }
        return blockedIps;
    }

    /**
     * Function to check if a given IP is locally blocked
     * @param {String} ip 
     * @returns {Promise<boolean>}
     */
    function ipIsBlocked(ip) {
        return new Promise(function(resolve) {
            new mw.Api().get({
                'action': 'query',
                'list': 'blocks',
                'bklimit': 1,
                'bkip': ip,
                'bkprop': 'user',
                'formatversion': 2
            }).then(function(res){
                resolve(res.query.blocks.length !== 0);
            });
        });
    }

    /**
     * Function to get an array of globally blocked IPs from an array of random IPs
     * @param {Array} ipsArr
     * @returns {Promise<Array>}
     */
    async function getGloballyBlockedIps(ipsArr) {
        if (ipsArr.length === 0) return [];
        const blockedIps = [];
        for (let i = 0; i < ipsArr.length; i++) {
            const blocked = await ipIsGloballyBlocked(ipsArr[i]);
            if (blocked) blockedIps.push(ipsArr[i]);
        }
        return blockedIps;
    }

    /**
     * Function to check if a given IP is globally blocked
     * @param {String} ip 
     * @returns {Promise<boolean>}
     */
    function ipIsGloballyBlocked(ip) {
        return new Promise(function(resolve) {
            new mw.Api().get({
                'action': 'query',
                'list': 'globalblocks',
                'bgip': ip,
                'bglimit': 1,
                'bgprop': 'address'
            }).then(function(res){
                resolve(res.query.globalblocks.length !== 0);
            });
        });
    }

    // Function to get account creation logid
    function getLogid(username) {
        return new Promise(function(resolve) {
            new mw.Api().get({
                'action': 'query',
                'list': 'logevents',
                'leuser': username,
                'ledir': 'newer',
                'lelimit': 1,
                'formatversion': 2
            }).then(function(res){
                if (res.query.logevents.length === 0) resolve(); // No logevent exists if the API returns an empty array
                resolve(res.query.logevents[0].logid);
            });
        });
    }


    // ******************** EVENT HANDLERS ********************

    // Copy a VIP name when the selection is changed
    $(document).off('change', '#anr-viplist-select').on('change', '#anr-viplist-select', function() {
        const vipSelectVal = $('#anr-viplist-select').find('option').filter(':selected').text().trim();
        copyToClipboard('[[WP:VIP#' + vipSelectVal + ']]');
    });

    // Reset dialog when closed
    $(document)
        .off('dialogclose', '#anr-modal-dialog, #anr-preview-dialog, #anr-drpreview-dialog')
        .on('dialogclose', '#anr-modal-dialog, #anr-preview-dialog, #anr-drpreview-dialog',
    function() {
        $(this).remove();
        userCnt = 1;
    });

    // Dynamically change the content of the section dropdown depending on the value selected in '報告先'
    $(document).off('change', '#anr-target-options').on('change', '#anr-target-options', function(){
        const selectedTar = $(this).children('option').filter(':selected').text();
        $('.anr-section-options-initial').prop('selected', true); // Reset the dropdown value
        switch(selectedTar) {
            case ANI:
                $('#anr-section-i-div').css('display', 'block');
                $('#anr-section-s-div').css('display', 'none');
                $('#anr-section-i-options-date').text(getSectionI(false));
                $('#anr-section-i-select').css({'width': $(this).innerWidth()});
                $('#anr-target-pagelink-div').css('display', 'block');
                $('#anr-target-pagelink').attr('href', mw.util.getUrl(ANI));
                break;
            case ANS:
                $('#anr-section-i-div').css('display', 'none');
                $('#anr-section-s-div').css('display', 'block');
                $('#anr-section-s-select').select2({'width': $(this).innerWidth()});
                $('#anr-target-pagelink-div').css('display', 'block');
                $('#anr-target-pagelink').attr('href', mw.util.getUrl(ANS));
                break;
            case AN3RR:
                $('#anr-section-i-div').css('display', 'none');
                $('#anr-section-s-div').css('display', 'none');
                $('#anr-target-pagelink-div').css('display', 'block');
                $('#anr-target-pagelink').attr('href', mw.util.getUrl(AN3RR));
                break;
        }
        centerDialog();
    });

    // Add section name to the '報告先' link when section is specified
    $(document)
        .off('change', '#anr-section-i-select, #anr-section-s-select')
        .on('change', '#anr-section-i-select, #anr-section-s-select',
    function(){
        var tarSection = '', tarPage = '';
        if ($(this).attr('id') === 'anr-section-i-select') {
            tarPage = ANI;
        } else if ($(this).attr('id') === 'anr-section-s-select') {
            tarPage = ANS;
        }
        if ($(this).find('option').filter(':selected').text() !== '選択してください') {
            tarSection = '#' + $(this).find('option').filter(':selected').text();
            $('#anr-target-pagelink').attr('href', mw.util.getUrl(tarPage + tarSection));
        }
    });

    // When the selection is changed in the type dropdown
    $(document).off('change','#anr-user-div select').on('change','#anr-user-div select', function(e){

        const selectID = '#' + e.target.id; // #anr-userX-select
        const valSelected = $(selectID).children('option').filter(':selected').text(); // Selected type
        const inputID = selectID.replace('select', 'input'); // #anr-userX-input
        const valInput = $(inputID).val().trimANR(); // The input value
        const checkboxDivID = selectID.replace('select', 'checkbox-div'); // #anr-userX-checkbox-div
        const checkboxID = selectID.replace('select', 'checkbox'); // #anr-userX-checkbox
        const idlinkDivID = selectID.replace('select', 'idlink-div'); // #anr-userX-idlink-div
        const idlinkID = selectID.replace('select', 'idlink'); // #anr-userX-idlink

        switch(valSelected) {
            case 'UNL':
            case 'User2':
                $(checkboxDivID).css('display', 'block');
                toggleBlockStatusLink(inputID, false, false);
                break;
            case 'IP2':
                toggleBlockStatusLink(inputID, false, false);
                break;
            case 'logid':
                $(checkboxDivID).css('display', 'block');
                $(checkboxID).prop('checked', true);
                $(idlinkID).attr('href', mw.util.getUrl('Special:redirect/logid/' + valInput)).text('特別:転送/logid/' + valInput);
                toggleBlockStatusLink(inputID, true, true);
                break;
            case 'diff':
                $(checkboxDivID).css('display', 'none');
                $(idlinkDivID).css('display', 'block');
                $(idlinkID).attr('href', mw.util.getUrl('Special:diff/' + valInput)).text('特別:差分/' + valInput);
                toggleBlockStatusLink(inputID, true, false);
                break;
            default:
                $(checkboxDivID).css('display', 'none');
                $(idlinkDivID).css('display', 'none');
                toggleBlockStatusLink(inputID, true, false);
        }

    });

    // When username is typed in, change dropdown options for UserAN types
    $(document).off('input', '#anr-user-div :text').on('input', '#anr-user-div :text', function(e){
        const inputID = '#' + e.target.id; // #anr-userX-input
        updateTypeDropdown(inputID);
    });

    // When 'hide username' is clicked, get logid, change dropdown options, show href and so on
    $(document).off('change', '#anr-user-div :checkbox').on('change', '#anr-user-div :checkbox', function(e){

        const checkboxID = '#' + e.target.id; // #anr-userX-checkbox
        const selectID = checkboxID.replace('checkbox', 'select'); // #anr-userX-select
        const inputID = checkboxID.replace('checkbox', 'input'); // #anr-userX-input
        const inputVal = $(inputID).val().trimANR();
        const idlinkID = checkboxID.replace('checkbox', 'idlink'); // #anr-userX-idlink
        const idlinkDivID = checkboxID.replace('checkbox', 'idlink-div'); // #anr-userX-idlink-div

        // Function to update type dropdown for logid
        const updateTypeDropdownLogid = function(logid) {
            $(selectID).children('.anr-opt-UNL').prop('hidden', true);
            $(selectID).children('.anr-opt-User2').prop('hidden', true);
            $(selectID).children('.anr-opt-IP2').prop('hidden', true);
            $(selectID).children('.anr-opt-logid').prop('hidden', false).prop('selected', true);
            $(selectID).children('.anr-opt-diff').prop('hidden', false);
            $(selectID).children('.anr-opt-none').prop('hidden', false);
            $(idlinkDivID).css('display', 'block');
            $(idlinkID).attr('href', mw.util.getUrl('Special:redirect/logid/' + logid)).text('特別:転送/logid/' + logid);
            toggleBlockStatusLink(inputID, true, true);
        }

        var logid, username;
        if ($(checkboxID).is(':checked')) { // If the checkbox is checked (the input value is a username and this needs to be converted to a logid)

            if (logid = Logids[username = inputVal]) { // If the object knows the logid for the user
                $(inputID).val(logid); // Replace the username with the logid in the object
                updateTypeDropdownLogid(logid);
            } else {
                (async function() {
                    logid = await getLogid(username = inputVal); // Get logid from the API
                    if (!logid) { // If undefined is returned, reject the checking of the checkbox
                        alert('エラー\n\n取得可能なlogidが存在しません。Logidを手動で入力するか、type=diff または none を使用してください');
                        $(checkboxID).prop('checked', false); 
                    } else { // If a valid logid is returned
                        $(inputID).val(logid); // Set the logid to the input
                        Logids[username] = logid; // Save the logid in the object
                        updateTypeDropdownLogid(logid);
                    }
                })();
            }

        } else { // if the checkbox is unchecked (the input value is a logid and this needs to be converted to a username)

            if (username = getKeyByValue(Logids, logid = inputVal)) { // Username converted from logid
                $(inputID).val(username); // Replace the logid with the username in the object
                $(selectID).children('.anr-opt-UNL').prop('hidden', false).prop('selected', true);
                $(selectID).children('.anr-opt-User2').prop('hidden', false);
                $(selectID).children('.anr-opt-IP2').prop('hidden', true);
                $(selectID).children('.anr-opt-logid').prop('hidden', true);
                $(selectID).children('.anr-opt-diff').prop('hidden', true);
                $(selectID).children('.anr-opt-none').prop('hidden', false);
                $(idlinkDivID).css('display', 'none');
                toggleBlockStatusLink(inputID, false, false);
            } else {
                alert('エラー\n\nLogidにはアカウント作成記録以外のものも含まれるため、logidからユーザー名への変換機能は実装していません。' +
                    'テキストボックス下のリンク先からユーザー名を取得するか、手動入力してください。なお、ユーザー名からlogidへの変換が行われた' +
                    '場合のみ、その逆の変換が可能です');
                $(checkboxID).prop('checked', true);
            }

        }

    });

    // When the 'add' button is hit, add another input layer
    $(document).off('click', '#anr-addBtn').on('click', '#anr-addBtn', function(){
        userCnt++;
        $('#anr-btn-div').before(userDiv.replaceAllANR('1-', userCnt + '-'));
        $(`#anr-user${userCnt}-div`).css('margin-top', '0.2em');
        centerDialog();
    });

    // When buttons are moused on and off
    $(document).off('mouseover mouseleave', '#anr-modal-dialog form button').on({
        'mouseover': function(e) {e.target.style.borderColor = '#999999';},
        'mouseleave': function(e) {e.target.style.borderColor = '#d3d3d3';}
    }, '#anr-modal-dialog form button');

    // When the summary checkbox is (un)checked
    $(document).off('change', '#anr-summary-checkbox').on('change', '#anr-summary-checkbox', function(){
        const $textarea = $('#anr-summary-text');
        if ($(this).is(':checked')) { // Box is checked
            $textarea.css('display','inline-block').val(genEditSummary()); // Show textarea with an edit summary
        } else { // Box is unchecked
            $textarea.css('display','none').val('');
        }
        centerDialog();
    });


    // ******************** AUXILIARY FUNCTIONS ********************

    /**
     * Function to check if an element is in an array
     * @param {string} el
     * @param {Array} arr
     * @returns {boolean}
     */
    function isInArray (el, arr) {
        return arr.indexOf(el) !== -1;
    }

    /**
     * Function to check if elements of an array are all contained in another array
     * @param {Array} arr1
     * @param {Array} arr2
     * @returns {boolean}
     */
    function arrayIsInArray(arr1, arr2) {
        for (let i = 0; i < arr1.length; i++) {
            if (!isInArray(arr1[i], arr2)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Function to check if a string contains a substring in an element of an array
     * @param {string} str
     * @param {Array} arr 
     * @returns {*} the first element matched in the array (if there's no match, returns undefined)
     */
    function stringContainsElementInArray(str, arr) {
        for (let i = 0; i < arr.length; i++) {
            if (str.indexOf(arr[i]) !== -1) {
                return arr[i];
            }
        }
    }

    /**
     * Function to get the key of a value in an object
     * @param {Object} object 
     * @param {*} value 
     * @returns {*} key
     */
    function getKeyByValue(object, value) {
        for (let key in object) {
            if (object[key] == value) return key;
        }
    }

    // Function to copy a string to the clipboard
    function copyToClipboard(str) {
        const $temp = $('<input>');
        $('body').append($temp); // Create a temporarily hidden text field
        $temp.val(str).select(); // Copy the text string into the field and select the text
        document.execCommand('copy'); // Copy it to the clipboard
        $temp.remove(); // Remove the text field
    }

    // Function to get today's date
    function today() {
        const d = new Date();
        return d.getMonth() + 1 + '月' + d.getDate() + '日';
    }

    // Function to get the last day of the month
    function lastDay(y, m){
        return new Date(y, m + 1, 0).getDate();
    }

    /**
     * String method to get rid of the U+200E space, in addition to the function of $.trim()
     * @returns {string}
     */
    String.prototype.trimANR = function() {
        return this.replace(/\u200e/g, '').trim();
    };

    /**
     * String method (alternative) to replace all occurences of a string with another
     * (takes a replacer and a replacee as arguments)
     * @returns {string}
     */
    String.prototype.replaceAllANR = function() {
        if (arguments.length %2 !== 0) {
            return new Error('SyntaxError: replaceAllANR takes an even number of arguments.');
        } else {
            let replaced = '';
            for (let i = 0; i < arguments.length; i = i + 2) {
                if (i === 0) {
                    replaced = this.split(arguments[i]).join(arguments[i + 1]);
                } else {
                    replaced = replaced.split(arguments[i]).join(arguments[i + 1]);
                }
            }
            return replaced;
        }
    };

    String.prototype.escapeRegExpANR = function() { // Just a note: ^$.*+?()[]{}|
        return this.replaceAllANR('(', '\\(', ')', '\\)', '.', '\\.');
    }

})();
//</nowiki>