コンテンツにスキップ

英文维基 | 中文维基 | 日文维基 | 草榴社区

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

削除された内容 追加された内容
RevisionFinder v1.0.0
(相違点なし)

2022年10月17日 (月) 16:59時点における版

/*****************************************************************\
    Name: Revision Finder
    Author: Dragoniez
    Version: 1.0.0
    Documentation: [[User:Dragoniez/scripts/Revision Finder]]
\*****************************************************************/
//<nowiki>

(function(mw, $) { // コンテナ関数

// ************************************** 初期化 **************************************

// 履歴ページのみでスクリプトを使用
if (mw.config.get('wgAction') !== 'history') return;

// 変数定義
var pagetitle = mw.config.get('wgPageName');
var userGroups = mw.config.get('wgUserGroups');
var canRevDel = userGroups.some(function(el) {
    return ['sysop', 'eliminator', 'suppress'].indexOf(el) !== -1;
});
var api, revisions, contentObtained, revidsForSearch;

// 依存モジュールの読み込み
$.when(
    mw.loader.using(['mediawiki.api', 'mediawiki.util', 'jquery.ui']),
    $.ready
).then(function() { // 完了後

    // ページに変更履歴がない場合は終了
    if ($('.mw-contributions-list').children('li').length === 0) return;

    // ページの全履歴をAPIから取得
    api = new mw.Api();
    getRevisions().then(function(revlist) {

        // 取得した配列をグローバル変数に代入し整理
        revisions = revlist;
        cleanupRevisionsArr();

        // 各版の日時リンクにrevid属性とtimestamp属性を追加
        $('.mw-contributions-list').children('li').each(function() {
            var revid = $(this).attr('data-mw-revid');
            var revObj = revisions.filter(function(obj) { return obj.revid == revid; })[0];
            var timestamp = revObj.timestamp;
            var timestamp_y4 = revObj.timestamp_y4;
            var timestamp_yja = revObj.timestamp_yja;
            $(this).find('a.mw-changeslist-date').attr({
                'data-rf-revid': revid,
                'data-rf-timestamp': timestamp,
                'data-rf-timestamp_y4': timestamp_y4,
                'data-rf-timestamp_yja': timestamp_yja
            });
        });

        // ダイアログとポートレットリンクを生成
        createDialog();
        $(mw.util.addPortletLink(
            'p-cactions',
            '#',
            'Revision Finder',
            'ca-rf',
            '複数版のリンク生成' + (canRevDel ? 'および版指定削除' : ''),
            null,
            '#ca-move'
        )).click(function(e) {
            e.preventDefault();
            $('#rf-dialog-main').dialog('open');
        });
        
        // スクリプトの準備ができたことを可視化するために、一瞬リンクを点滅させる
        var $datelinks = $('.mw-contributions-list').children('li').find('a.mw-changeslist-date');
        var color = $datelinks.eq(0).css('color');
        $datelinks.css('color', 'orange').animate({color: color}, 1000);

    });

});

// ************************************** 主要関数 **************************************

/**
 * 取得した変更履歴を格納した配列を整理する関数
 */
function cleanupRevisionsArr() {
    revisions.forEach(function(obj) {
        obj.timestamp = obj.timestamp.replace(/Z$/, ''); // タイムスタンプ語尾のZを除去
        obj.timestamp_y4 = convertTs(obj.timestamp, 'y4'); // フォーマットを変更したタイムスタンプをプロパティとして登録
        obj.timestamp_yja = convertTs(obj.timestamp, 'yja');
    });
}

/**
 * "yyyy-mm-ddThh:mm:ss" のタイムスタンプを別フォーマットへ変換する関数
 * @param {string} tsUTC JSON timestamp 
 * @param {string} format y4 or yja
 * @returns {string} y4: yyyy年mm月dd日 (曜) hh:mm, yja: 令和y年mm月dd日 (曜) hh:mm
 */
function convertTs(tsUTC, format) {

    var weekdaysJA = ['日', '月', '火', '水', '木', '金', '土'];

    var d = new Date(tsUTC);
    var y;
    switch (format) {
        case 'yja':
            y = d.toLocaleDateString('ja-JP-u-ca-japanese', {era: 'long'}).match(/^[^\d]{2}(\d{1,2}|元)/)[0]; // 「令和4/10/2」⇒「令和4」
            break;
        case 'y4':
            y = d.getFullYear();
    }
    var hr = (hr = d.getHours()).toString().length === 1 ? '0' + hr : hr;
    var min = (min = d.getMinutes()).toString().length === 1 ? '0' + min : min;

    return y + '年' + (d.getMonth() + 1) + '月' + d.getDate() + '日 (' + weekdaysJA[d.getDay()] + ') ' + hr + ':' + min; 

}

/**
 * APIから全ての版情報を取得する関数
 * @param {boolean} [getContent] trueの場合は版本文を取得
 * @returns {jQuery.Promise<Array<{revid: number, timestamp: string, user: string, content: string|undefined}>>}
 */
function getRevisions(getContent) {
    var def = new $.Deferred();
    
    var revlist = [];
    var query = function(rvcontinue) { // 版数が多い場合同じ処理のループが必要な為APIリクエストを独立関数化
        var deferred = new $.Deferred();

        api.get({
            action: 'query',
            titles: pagetitle,
            prop: 'revisions',
            rvprop: 'ids|timestamp|user' + (getContent ? '|content' : ''),
            rvlimit: 'max', // 本文を取得しない場合は500、する場合は50が上限
            rvdir: 'newer',
            rvcontinue: rvcontinue,
            formatversion: '2'
        }).then(function(res) {

            var resPg, resCont;
            if (!res || !res.query || !(resPg = res.query.pages)) return deferred.resolve();

            var resRev = resPg[0].revisions;
            revlist = revlist.concat(resRev);

            if (res && res.continue && (resCont = res.continue.rvcontinue)) {
                query(resCont).then(function() {
                    deferred.resolve();
                });
            } else {
                deferred.resolve();
            }
    
        }).catch(function(code, err) {
            console.log(err);
            deferred.resolve();
        });

        return deferred.promise();
    };
    
    query().then(function() {
        def.resolve(revlist);
    });

    return def.promise();
}

/**
 * メインダイアログを生成する関数
 */
function createDialog() {

    // styleタグを生成
    $('head').append(
        '<style>' +
            '.rf-selected {' +
                'background: #FEC493;' +
            '}' +
            '.rf-matched {' +
                'background: pink;' +
            '}' +
            '.rf-label {' +
                'display: inline-block;' +
                'width: 8ch;' +
            '}' +
            '.rf-labelinfieldset {' +
                'display: inline-block;' +
                'width: calc(8ch - 1em);' +
            '}' +
            '#rf-dialog-main fieldset {' +
                'margin: 0;' +
            '}' +
            '#rf-dialog-main legend {' +
                'font-weight: bold;' +
            '}' +
            '#rf-dialog-main input[type="checkbox"],' +
            '#rf-dialog-search input[type="checkbox"] {' +
                'margin-right: 0.5em;' +
            '}' +
            '#rf-timestamp div:not(#rf-timestamp-buttons) {' +
                'margin-bottom: 0.3em;' +
            '}' +
            '#rf-revdelsettings-target label {' +
                'margin-right: 0.5em;' +
            '}' +
            '#rf-revdelsettings > div:not(#rf-revdelsettings-reason-div) {' +
                'margin-bottom: 0.3em;' +
            '}' +
            '#rf-revdelsettings-reason-div div {' +
                'margin-bottom: 0.3em;' +
            '}' +
            '.rf-dialog-main .ui-dialog-titlebar-close,' +
            '.rf-dialog-links .ui-dialog-titlebar-close,' +
            '.rf-dialog-search .ui-dialog-titlebar-close {' +
                'visibility: hidden;' +
            '}' +
            '#rf-dialog-search > div {' +
                'margin-bottom: 0.3em;' +
            '}' +
            '#rf-dialog-main input,' +
            '#rf-dialog-main textarea,' +
            '#rf-dialog-links input,' +
            '#rf-dialog-links textarea,' +
            '#rf-dialog-search input,' +
            '#rf-dialog-search textarea {' +
                'box-sizing: border-box;' +
            '}' +
        '</style>'
    );

    // DOM上にダイアログを作成
    $('body').append(
        '<div id="rf-dialog-main" title="Revision Finder" style="max-height: 80vh; padding-top: 1.5em;">' +
            '<div id="rf-info">' +
                '<label for="rf-info-pagetitle" class="rf-label" style="font-weight: bold;">ページ名</label>' +
                '<span id="rf-info-pagetitle">' + pagetitle + '</span><br/>' +
                '<label for="rf-info-rvcount" class="rf-label" style="font-weight: bold;">総版数</label>' +
                '<span id="rf-info-rvcount">' + revisions.length + '</span><br/>' +
                '<label for="rf-info-displaycount" class="rf-label" style="font-weight: bold;">表示版数</label>' +
                '<span id="rf-info-displaycount">' + $('.mw-contributions-list').children('li').length + '</span>' +
            '</div>' +
            '<fieldset id="rf-target">' +
                '<legend>対象版</legend>' +
                '<div id="rf-target-temp">' + // ダイアログの横幅を絶対値にするためのみに使用
                    '<input type="checkbox"></input>' +
                    '<label>2000-01-01T00:00:00 ~ 2000-01-01T00:00:00 (連続1000版)</label>' +
                '</div>' +
                '<div id="rf-targetrevisions-list" style="max-height: 20vh; overflow: auto;">' +
                '</div>' +
                '<div id="rf-target-remove" style="display: none; margin-top: 0.5em;">' +
                    '<input id="rf-target-remove-do" type="button" value="除去"></input>' +
                    '<input id="rf-target-remove-checkall" type="button" style="margin-left: 0.5em;" value="全選択"></input>' +
                    '<input id="rf-target-remove-uncheckall" type="button" style="margin-left: 0.5em;" value="全選択解除"></input>' +
                '</div>' +
            '</fieldset>' +
            '<fieldset id="rf-timestamp">' +
                '<legend>タイムスタンプ</legend>' +
                '<div>' +
                    '<label for="rf-timestamp-start" class="rf-labelinfieldset">始点</label>' +
                    '<input id="rf-timestamp-start"></input>' +
                '</div>' +
                '<div>' +
                    '<label for="rf-timestamp-end" class="rf-labelinfieldset">終点</label>' +
                    '<input id="rf-timestamp-end"></input>' +
                '</div>' +
                '<div id="rf-timestamp-buttons" style="margin-top: 0.5em;">' +
                    '<input id="rf-timestamp-add" type="button" value="追加"></input>' +
                    '<input id="rf-timestamp-clear" type="button" style="margin-left: 0.5em;" value="クリア"></input>' +
                '</div>' +
            '</fieldset>' +
            '<fieldset id="rf-revdelsettings" style="display: none;">' +
                '<legend>版指定削除の設定</legend>' +
                '<div>' +
                    '<label for="rf-revdelsettings-mode" class="rf-labelinfieldset">区分</label>' +
                    '<select id="rf-revdelsettings-mode">' +
                        '<option>削除</option>' +
                        '<option>復帰</option>' +
                    '</select>' +
                '</div>' +
                '<div>' +
                    '<label for="rf-revdelsettings-target" class="rf-labelinfieldset">対象</label>' +
                    '<div id="rf-revdelsettings-target" style="display: inline-block;">' +
                        '<input id="rf-revdelsettings-content" type="checkbox"></input>' +
                        '<label for="rf-revdelsettings-content">本文</label>' +
                        '<input id="rf-revdelsettings-user" type="checkbox"></input>' +
                        '<label for="rf-revdelsettings-user">利用者名</label>' +
                        '<input id="rf-revdelsettings-comment" type="checkbox"></input>' +
                        '<label for="rf-revdelsettings-comment">要約</label>' +
                        '<div style="display: none;">' +
                            '<input id="rf-revdelsettings-oversight" type="checkbox"></input>' +
                            '<label for="rf-revdelsettings-oversight">オーバーサイト</label>' +
                        '</div>' +
                    '</div>' +
                '</div>' +
                '<div id="rf-revdelsettings-reason-div">' +
                    '<div>' +
                        '<label for="rf-revdelsettings-reason1" class="rf-labelinfieldset">理由1</label>' +
                        '<select id="rf-revdelsettings-reason1" class="rf-revdelsettings-reason">' +
                            '<optgroup label="指定なし">' +
                                '<option value="">なし</option>' +
                            '</optgroup>' +
                        '</select>' +
                    '</div>' +
                    '<div>' +
                        '<label for="rf-revdelsettings-reason2" class="rf-labelinfieldset">理由2</label>' +
                        '<select id="rf-revdelsettings-reason2" class="rf-revdelsettings-reason">' +
                            '<optgroup label="指定なし">' +
                                '<option value="">なし</option>' +
                            '</optgroup>' +
                        '</select>' +
                    '</div>' +
                    '<div>' +
                        '<label for="rf-revdelsettings-reasonC" class="rf-labelinfieldset"></label>' +
                        '<input id="rf-revdelsettings-reasonC" placeholder="非定型理由 (自由記述)">' +
                    '</div>' +
                '</div>' +
            '</fieldset>' +
        '</div>'
    );

    // ダイアログ用ボタンの定義
    var $dialog = $('#rf-dialog-main');
    var btns = [];
    if (canRevDel) { // 版指定削除権限がある場合
        getDeleteReasonDropdown(); // 削除理由インターフェースを取得
        btns.push(
            {
                text: '版指定削除',
                click: doRevDel
            }
        );
        $('#rf-revdelsettings').css('display', 'block');
        if (userGroups.indexOf('suppress') !== -1) {
            $('#rf-revdelsettings-oversight').parent('div').css('display', 'inline-block');
        }
    }
    btns = btns.concat([
        {
            text: 'リンク生成',
            click: createLinks
        },
        {
            text: '文字列検索',
            click: keywordSearch
        },
        {
            text: '閉じる',
            click: function() {
                $dialog.dialog('close');
            }
        }
    ]);

    // 作成したDOM要素をダイアログ化
    var firstDialogOpen = true;
    $dialog.dialog({
        dialogClass: 'rf-dialog-main',
        autoOpen: false,
        modal: false,
        width: 'auto',
        resizable: false,
        position: {
            my: 'center top',
            at: 'center top+2%'
        },
        buttons: btns,
        open: function() { // 初めてダイアログを開いた際のみ使用
            if (firstDialogOpen) {
                firstDialogOpen = false;

                // 版指定削除の手動入力用テキストボックスを理由ドロップダウンと同じ横幅に変更
                if (canRevDel) {
                    $('#rf-revdelsettings-reasonC').css('width', $('.rf-revdelsettings-reason').eq(0).outerWidth());
                }

                // 横幅設定用のdivがある場合はダイアログの横幅をそのdivに合わせた絶対値に変更し除去
                if ($('#rf-target-temp').length !== 0) {
                    var dWidth = $dialog.innerWidth();
                    $dialog.dialog('option', 'width', dWidth);
                    $('#rf-target-temp').remove();
                }
            }
        }
    });

    // 編集履歴の日時リンクがクリックされた際、ダイアログにその日時をコピーする
    $('.mw-contributions-list').children('li').find('a.mw-changeslist-date').click(function(e) {

        // ShiftキーとCtrlキーの両方が押されている場合のみイベントをトリガー
        if (!(e.shiftKey && e.ctrlKey)) return;
        e.preventDefault();
        if ($('#rf-dialog-links').length !== 0) return; // 別ダイアログが開いている場合は無効化
        if ($('#rf-dialog-search').length !== 0) return;
        if (!$dialog.dialog('isOpen')) $dialog.dialog('open'); // ダイアログが開いていない場合は開く

        // ダイアログに入力済みのタイムスタンプを取得し、両方のテキストボックスが埋まっていないかを確認
        var start = $('#rf-timestamp-start').val().replace(/\u200e/g, '').trim(),
            end = $('#rf-timestamp-end').val().replace(/\u200e/g, '').trim(),
            $tar = !start ? $('#rf-timestamp-start') : (!end ? $('#rf-timestamp-end') : null);
        if (!$tar) return alert('テキストボックスが埋まっています');

        // 始点が空いていれば始点に、終点が空いていれば終点に日時を入力
        $tar.val($(this).attr('data-rf-timestamp'));

    });

}

/**
 * [[MediaWiki:Revdelete-reason-dropdown]] から版指定削除理由のドロップダウンを取得する関数
 * @returns {jQuery.Promise}
 */
function getDeleteReasonDropdown() {
    var def = new $.Deferred();

    // 該当ページの内容を取得
    var interface = 'MediaWiki:Revdelete-reason-dropdown';
    var url = mw.config.get('wgScript') + '?action=raw&title=' + interface;
    $.get(url)
    .then(function(content) { // 成功

        if (!content) return def.resolve();

        // インターフェースの内容をselect optionに変換
        var deleteReasons = '';
        content.split('\n').forEach(function(item, i) { // ページコンテンツを改行で split しその配列をループ

            if (item.match(/^\*[^*]/)) { // 行が「*」で始まる場合 (=<optgroup>)
                if (i !== 0) deleteReasons += '</optgroup>'; // 1回目のループではない場合 optgroup タグを閉じる
                deleteReasons += '<optgroup label="' + item.replace(/^\*[^\S\r\n]*/, '') + '">'; // 行頭の「*(+スペース)」を除去し optgroup タグ化
            } else { // その他 (行が「**」で始まる場合 (=<option/>))
                deleteReasons += '<option>' + item.replace(/^\*{2}[^\S\r\n]*/, '') + '</option>'; // 行頭の「**(+スペース)」を除去し option タグ化
            }

        });
        deleteReasons += '</optgroup>';

        // select に option を追加
        $('.rf-revdelsettings-reason').append(deleteReasons);

        def.resolve();

    }).catch(function(err) { // 失敗
        var msg404 = err.status && err.status === 404 ? ' ( [[' + interface + ']] は存在しません)' : '';
        if (confirm('Revision Finder\n削除理由の取得に失敗しました' + msg404 + '。ページをリロードしますか?')) location.reload(true);
        def.reject();
    });

    return def.promise();
}

// 「追加」ボタンの制御
$(document).off('click', '#rf-timestamp-add').on('click', '#rf-timestamp-add', function() {

    // 「タイムスタンプ」フィールドの値を取得
    var ts = {
        start: $('#rf-timestamp-start').val().replace(/\u200e/g, '').trim(),
        end: $('#rf-timestamp-end').val().replace(/\u200e/g, '').trim()
    };
    if (!ts.start && !ts.end) return alert('タイムスタンプが入力されていません');

    // タイムスタンプのフォーマットを確認
    var tsRegex = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}|(\d{4}|[^\d]{2}\d{1,2})年\d{1,2}月\d{1,2}日 \(.\) \d{2}:\d{2})$/;
    var tsInvalid = Object.keys(ts).some(function(key) {
        return ts[key] && !ts[key].match(tsRegex);
    });
    if (tsInvalid) return alert('タイムスタンプのフォーマットが不正です');

    // 入力されたタイムスタンプの版があるかを確認
    var rvExist = {
        start: false,
        end: false
    };
    revisions.some(function(obj) { // 余分なループ処理をしないようにsomeを使用 (trueがreturnされた場合ループを終了する)
        var tsArr = [obj.timestamp, obj.timestamp_y4, obj.timestamp_yja]; // いずれかのフォーマットのTSと合致するかを確認
        if (tsArr.indexOf(ts.start) !== -1) rvExist.start = true;
        if (tsArr.indexOf(ts.end) !== -1) rvExist.end = true;
        return rvExist.start && rvExist.end;
    });
    if (ts.start && ts.end) {
        if (!rvExist.start && !rvExist.end) {
            return console.log('指定された始点版と終点版が見つかりませんでした');
        } else if (!rvExist.start) {
            return console.log('指定された始点版が見つかりませんでした');
        } else if (!rvExist.end) {
            return console.log('指定された終点版が見つかりませんでした');
        }
    } else if (ts.start) {
        if (!rvExist.start) return console.log('指定された版が見つかりませんでした');
    } else if (ts.end) {
        if (!rvExist.end) return console.log('指定された版が見つかりませんでした');
    }

    // TSがJSONフォーマットではない場合、同分時の版がないかチェックし、入力されたTSをJSONフォーマットに変換
    var tsRegexNonDefault = /^(\d{4}|[^\d]{2}\d{1,2})年\d{1,2}月\d{1,2}日 \(.\) \d{2}:\d{2}$/;
    var multiple = {
        start: {
            samemin: false,
            count: null
        },
        end: {
            samemin: false,
            count: null
        }
    };
    ['start', 'end'].forEach(function(el, i) {
        if (ts[el] && ts[el].match(tsRegexNonDefault)) {
            var matchedRevisions = revisions.filter(function(obj) { return [obj.timestamp_y4, obj.timestamp_yja].indexOf(ts[el]) !== -1; });
            if (matchedRevisions.length !== 1) {
                multiple[el].samemin = true;
                multiple[el].count = matchedRevisions.length;
            }
            var elNum = i === 0 ? 0 : matchedRevisions.length - 1;
            var defaultTs = matchedRevisions[elNum].timestamp;
            ts[el] = defaultTs;
            $('#rf-timestamp-start').val(ts[el]);
        }
    });

    // 始点TSと終点TSの整理
    if (ts.start && ts.end) {
        var dates = {
            start: new Date(ts.start),
            end: new Date(ts.end)
        };
        if (dates.start > dates.end) { // 始点と終点が逆の場合入れ替え
            var tsTemp = ts.start;
            ts.start = ts.end;
            ts.end = tsTemp;
            var multipleTemp = JSON.parse(JSON.stringify(multiple.start));
            multiple.start = multiple.end;
            multiple.end = multipleTemp;
            $('#rf-timestamp-start').val(ts.start);
            $('#rf-timestamp-end').val(ts.end);
        }
        if (ts.start === ts.end) { // 始点と終点が同じ場合片方を除去
            ts.end = null;
            $('#rf-timestamp-end').val('');
        }
    }

    // 指定された版間の版IDを取得
    var newRevids = [];
    if (ts.start && ts.end) { // 複数版の場合
        var counting = false;
        revisions.some(function(obj) {
            if (obj.timestamp === ts.start) counting = true;
            if (counting) newRevids.push(obj.revid);
            if (obj.timestamp === ts.end) counting = null;
            return counting === null;
        });
    } else { // 単一版の場合
        var tsSingle = ts.start || ts.end;
        newRevids.push(revisions.filter(function(obj) { return obj.timestamp === tsSingle; })[0].revid);
    }

    // 既に追加済みの版のrevidを取得
    var oldRevids = $('.rf-targetrevisions').map(function() { return $(this).attr('data-rf-revidchunk'); }).get();
    oldRevids = oldRevids.map(function(el) { return JSON.parse(el); });
    oldRevids = [].concat.apply([], oldRevids);

    // 重複チェック
    var intersect = oldRevids.some(function(n) {
        return newRevids.indexOf(n) !== -1;
    });
    if (intersect) {
        if (!confirm('追加済みの版と重複する版が指定されています。上書きしますか?')) return;
    }

    // 追加済みの版IDと新規追加の版IDの配列をマージし、版間差分テキストを取得
    newRevids = newRevids.concat(oldRevids).filter(function(n, i, arr) {
        return arr.indexOf(n) === i; // 重複revidの除去
    }).sort(function(a, b) { return a - b; }); // 昇順に並び替え

    // リストを更新
    updateRevisionsList(newRevids);

    // TSフィールドを白紙化
    $('#rf-timestamp-start').val('');
    $('#rf-timestamp-end').val('');

    // 同分台の版を検出した場合警告を表示
    if (multiple.start.samemin || multiple.end.samemin) {
        var msg = ['警告: 以下の指定版について、同分以内に複数の編集を検出しました\n'];
        var point = function(el) { return el === 'start' ? '始点' : '終点'; };
        if (ts.start && ts.end) {
            ['start', 'end'].forEach(function(el) {
                if (multiple[el].samemin) msg.push(point(el) + ': ' + ts[el].replace(/:\d{2}$/, '') + '台 (' + multiple[el].count + '版)');
            });
        } else {
            tsSingle = ts.start ? 'start' : 'end';
            msg.push(point(tsSingle) + ': ' + ts[tsSingle].replace(/:\d{2}$/, '') + '台 (' + multiple[tsSingle].count + '版)');
        }
        alert(msg.join('\n'));
    }

});

/**
 * 対象版フィールドの内容をアップデートする関数
 * @param {Array} revidsArr 
 */
function updateRevisionsList(revidsArr) {

    // 表示するテキストを取得
    var revlist = convertRevids(revidsArr, 'none', 'timestamp');

    // ハイライトを更新
    $('.rf-selected').removeClass('rf-selected');
    $('.mw-contributions-list').children('li').find('a.mw-changeslist-date').filter(function() {
        return revidsArr.indexOf(parseInt($(this).attr('data-rf-revid'))) !== -1;
    }).addClass('rf-selected');

    // フィールドの更新
    $('#rf-targetrevisions-list').empty();
    revlist.revisionsLink.forEach(function(label, i) {
        var id = 'rf-targetrevisions-' + i;
        $('#rf-targetrevisions-list').append(
            $('<div>')
            .append(
                $('<input>')
                .attr({
                    id: id,
                    class: 'rf-targetrevisions',
                    type: 'checkbox',
                    'data-rf-revidchunk': JSON.stringify(revlist.revidsNested[i])
                })
                .css('margin-right', '0.5em')
            )
            .append(
                $('<label>')
                .attr('for', id)
                .text(label)
            )
        );
    });
    toggleRemoveButton();

}

/**
 * revidの配列を連続IDの配列にまとめ、版間のリンクを生成する関数
 * @param {Array} revidsArr
 * @param {string} linktype diff, permalink, none
 * @param {string} texttype timestamp, timestamp|suffix, pagetitle, pagetitle|timestamp, pagetitle|timestamp|suffix
 * @returns {{revisionsLink: Array<string>, revidsNested: Array<Array<number>>}}
 */
function convertRevids(revidsArr, linktype, texttype) {

    // 版情報の配列からrevidを抽出 (例: [111, 112, 113, 115, 117, ...])
    var allRevids = revisions.map(function(obj) { return obj.revid; });

    // それぞれのrevidの配列インデックスを取得
    var revidIndex = [];
    revidsArr.forEach(function(revid) {
        revidIndex.push(allRevids.indexOf(revid)); // 特定のrevidが全ての版のうち何番目かを判別
    });
    revidIndex.sort(function(a, b) { return a - b; }); // 昇順に並び替え

    // revidインデックスの配列で連続するインデックスをネストさせる(例: [1,2,3,5] => [[1,2,3], [5]])
    var revidIndexNested = [];
    var tempArr;
    var consective = false;
    revidIndex.forEach(function(n, i, arr) {
        if (!consective) tempArr = [];
        tempArr.push(n);
        if (arr[i + 1] && arr[i + 1] === n + 1) {
            consective = true;
        } else {
            consective = false;
            revidIndexNested.push(tempArr);
        }
    });

    // ネストさせたインデックス配列を版情報オブジェクトに置換
    var revInfoNested = [];
    var revidsNested = [];
    revidIndexNested.forEach(function(arr) {
        var arrOfObj = revisions.slice(arr[0], arr[arr.length - 1] + 1);
        revInfoNested.push(arrOfObj);
        revidsNested.push(arrOfObj.map(function(obj) { return obj.revid; }));
    });

    /**
     * 版情報オブジェクトを文字列に変換する関数
     * @param {{revid: number, timestamp: string}} revInfoObj
     * @returns {string}
     */
    var getDiff = function(revInfoObj) {

        var wrapWithLink = function(str) {
            switch (linktype) {
                case 'diff':
                    return '[[特別:差分/' + revInfoObj.revid + '|' + str + ']]';
                case 'permalink':
                    return '[[特別:固定リンク/' + revInfoObj.revid + '|' + str + ']]';
                default:
                    return str;
            }
        };

        var converted;
        switch (texttype) {
            case 'timestamp':
                converted = revInfoObj.timestamp;
                break;
            case 'timestamp|suffix':
                converted = revInfoObj.timestamp + 'の版';
                break;
            case 'pagetitle':
                converted = pagetitle;
                break;
            case 'pagetitle|timestamp':
                converted = pagetitle + ' (' + revInfoObj.timestamp + ')';
                break;
            case 'pagetitle|timestamp|suffix':
                converted = pagetitle + ' (' + revInfoObj.timestamp + 'の版)';
                break;
            default:
                throw 'ReferenceError: texttype is not defined';
        }

        return wrapWithLink(converted);

    };

    var getRevisionsLink = function(revisionsArr) {
        var rvCnt = revisionsArr.length;
        var difflink;
        if (rvCnt > 1) {
            difflink = getDiff(revisionsArr[0]) + ' ~ ' + getDiff(revisionsArr[revisionsArr.length - 1]);
        } else {
            difflink = getDiff(revisionsArr[0]);
        }
        rvCnt = rvCnt > 1 ? ' (連続' + rvCnt + '版)' : '';
        return difflink + rvCnt;
    };

    var revisionsLink = [];
    revInfoNested.forEach(function(arr) {
        revisionsLink.push(getRevisionsLink(arr));
    });

    return {
        revisionsLink: revisionsLink,
        revidsNested: revidsNested
    };

}

// 「除去」ボタンの制御
$(document).off('click', '#rf-target-remove-do').on('click', '#rf-target-remove-do', function() {

    // チェックされた対象版を取得
    var $checked = $('.rf-targetrevisions').filter(':checked');
    if ($checked.length === 0) return alert('除去対象が指定されていません');

    // 対象版とハイライトを除去
    $checked.each(function() {
        var revids = JSON.parse($(this).attr('data-rf-revidchunk'));
        $('.rf-selected').filter(function() {
            return revids.indexOf(parseInt($(this).attr('data-rf-revid'))) !== -1;
        }).removeClass('rf-selected');
        $(this).parent('div').remove();
    });
    toggleRemoveButton();

});

// 「全選択」ボタンの制御
$(document).off('click', '#rf-target-remove-checkall').on('click', '#rf-target-remove-checkall', function() {
    $('.rf-targetrevisions').prop('checked', true);
});

// 「全選択解除」ボタンの制御
$(document).off('click', '#rf-target-remove-uncheckall').on('click', '#rf-target-remove-uncheckall', function() {
    $('.rf-targetrevisions').prop('checked', false);
});

/**
 * 対象版フィールド内に要素があれば「除去」ボタンを表示し、なければ非表示にする関数
 */
function toggleRemoveButton() {
    var display = $('.rf-targetrevisions').length === 0 ? 'none' : 'block';
    $('#rf-target-remove').css('display', display);
}

// 「クリア」ボタンの制御
$(document).off('click', '#rf-timestamp-clear').on('click', '#rf-timestamp-clear', function() {
    $('#rf-timestamp-start, #rf-timestamp-end').val('');
});

/**
 * ダイアログの「リンク生成」ボタンの制御用関数
 */
function createLinks() {

    // 対象版のチェックボックスを取得
    var $target = $('.rf-targetrevisions');
    if ($target.length === 0) return alert('対象版が指定されていません');

    // 新しいダイアログ用のHTML要素をDOM上に作成
    $('body').append('<div id="rf-dialog-links" title="Revision Finder" style="max-height: 80vh;"/>');

    // 要素のダイアログ化
    var $linkDialog = $('#rf-dialog-links');
    var $mainDialog = $('#rf-dialog-main');
    $linkDialog.dialog({
        dialogClass: 'rf-dialog-links',
        modal: false,
        width: 'auto',
        resizable: false,
        position: {
            my: 'center top',
            at: 'center top+2%'
        },
        buttons: [
            {
                text: '戻る',
                click: function() {
                    $(this).dialog('destroy').remove();
                    $mainDialog.dialog('open');
                }
            },
            {
                text: '閉じる',
                click: function() {
                    $(this).dialog('destroy').remove();
                }
            }
        ],
        open: function() { // ダイアログを開いた際の初期設定
            var dWidth = $mainDialog.innerWidth();
            $linkDialog.dialog('option', 'width', dWidth); // メインダイアログと同じ横幅を設定
            $mainDialog.dialog('close'); // メインダイアログを隠す
            createDiffTextarea('#rf-dialog-links'); // リンク表示用のtextareaを作成
        }
    });

}

/**
 * リンク表示用のtextareaを作成する関数
 * @param {string} elementId textareaをappendする対象のelement ID (#の接頭辞必須)
 */
function createDiffTextarea(elementId) {

    $(elementId).append(
        '<textarea id="rf-revlist" style="width: 100%; margin: 0.5em 0;" rows="10" disabled></textarea>' +
        '<table id="rf-revlist-options">' +
            '<tr>' +
                '<td>' +
                    '<label for="rf-revlist-indent" class="rf-label">字下げ</label>' +
                    '<select id="rf-revlist-indent">' +
                        '<option>1</option>' +
                        '<option>2</option>' +
                        '<option>3</option>' +
                        '<option>4</option>' +
                        '<option>5</option>' +
                    '</select>' +
                '</td>' +
                '<td>' +
                    '<label for="rf-revlist-linktype" class="rf-label">リンク</label>' +
                    '<select id="rf-revlist-linktype">' +
                        '<option value="none">なし</option>' +
                        '<option value="diff" selected>差分</option>' +
                        '<option value="permalink">固定リンク</option>' +
                    '</select>' +
                '</td>' +
            '</tr>' +
            '<tr>' +
                '<td>' +
                    '<label for="rf-revlist-texttype" class="rf-label">テキスト</label>' +
                    '<select id="rf-revlist-texttype">' +
                        '<option value="timestamp">時刻</option>' +
                        '<option value="timestamp|suffix" selected>時刻の版</option>' +
                        '<option value="pagetitle">ページ名</option>' +
                        '<option value="pagetitle|timestamp">ページ名 (時刻)</option>' +
                        '<option value="pagetitle|timestamp|suffix">ページ名 (時刻の版)</option>' +
                    '</select>' +
                    '<span style="display: inline-block; width: 1em;"/>' +
                '</td>' +
                '<td>' +
                    '<label for="rf-revlist-total" class="rf-label">合計版数</label>' +
                    '<select id="rf-revlist-total">' +
                        '<option>あり</option>' +
                        '<option>なし</option>' +
                    '</select>' +
                '</td>' +
            '</tr>' +
        '</table>' +
        '<input id="rf-revlist-copy" type="button" value="コピー"></input>'
    );

    setRevisionsLink(); // 作成したtextareaに値を設定

}

/**
 * リンク表示用textareaに値を設定する関数
 */
function setRevisionsLink() {

    // リンク作成元のrevidを取得
    var revidsArr;
    var $searchDialog = $('#rf-dialog-search');
    if ($searchDialog.length !== 0 && $searchDialog.dialog('isOpen')) { // 検索ダイアログが開いている場合は検索結果のrevidを取得
        revidsArr = revidsForSearch;
    } else { // それ以外の場合はメインダイアログの対象版フィールドからrevidを取得
        revidsArr = $('.rf-targetrevisions').map(function() { return $(this).attr('data-rf-revidchunk'); }).get();
        revidsArr = revidsArr.map(function(el) { return JSON.parse(el); });
        revidsArr = [].concat.apply([], revidsArr);
    }

    // リンクの表示設定を取得
    var indenttype = $('#rf-revlist-indent').children('option').filter(':selected').val();
    var linktype = $('#rf-revlist-linktype').children('option').filter(':selected').val();
    var texttype = $('#rf-revlist-texttype').children('option').filter(':selected').val();
    var showtotal = $('#rf-revlist-total').children('option').filter(':selected').val() === 'あり';

    // インデントの生成
    var indent = '';
    for (var i = 1; i <= parseInt(indenttype); i++) indent += '*';
    indent += ' ';

    // revidと設定から表示する値を取得し、textareaに設定
    var txt = indent + convertRevids(revidsArr, linktype, texttype).revisionsLink.join('\n' + indent);
    if (showtotal) txt += '\n' + indent + '計' + revidsArr.length + '版';
    $('#rf-revlist').val(txt);

}

// リンク設定用のドロップダウンの値が変化した際、textareaの値を更新
$(document).off('change', '#rf-revlist-indent').on('change', '#rf-revlist-indent', setRevisionsLink);
$(document).off('change', '#rf-revlist-linktype').on('change', '#rf-revlist-linktype', setRevisionsLink);
$(document).off('change', '#rf-revlist-texttype').on('change', '#rf-revlist-texttype', setRevisionsLink);
$(document).off('change', '#rf-revlist-total').on('change', '#rf-revlist-total', setRevisionsLink);
$(document).off('click', '#rf-revlist-copy').on('click', '#rf-revlist-copy', function() {
    var $revlist = $('#rf-revlist');
    $revlist.prop('disabled', false).select();
    document.execCommand('copy');
    $revlist.prop('disabled', true);
});

/**
 * ダイアログの「文字列検索」ボタンの制御用関数
 */
function keywordSearch() {

    // 初めて開く時に各版の本文の取得が必要なため、時間が掛かりそうであればメッセージを表示
    if (!contentObtained && revisions.length > 100) {
        if (!confirm('全ての版の本文を取得します。版数に応じて時間が掛かる場合がありますが、実行しますか?')) return;
    }

    // 新しいダイアログ用のHTML要素をDOM上に作成
    $('body').append('<div id="rf-dialog-search" title="Revision Finder" style="max-height: 80vh; padding-top: 1.5em;"/>');

    // 要素のダイアログ化
    var $selected = $('.rf-selected');
    var $searchDialog = $('#rf-dialog-search');
    var $mainDialog = $('#rf-dialog-main');
    $searchDialog.dialog({
        dialogClass: 'rf-dialog-search',
        modal: false,
        width: 'auto',
        resizable: false,
        position: {
            my: 'center top',
            at: 'center top+2%'
        },
        open: function() {

            // 横幅設定
            var dWidth = $mainDialog.innerWidth();
            $searchDialog.dialog('option', 'width', dWidth);
            $mainDialog.dialog('close');

            // 初めてダイアログを開く場合は各版の本文を取得
            if (!contentObtained) {
                contentObtained = true;

                $searchDialog.append(
                    '<p>' +
                        '各版の本文を取得しています' +
                        '<img src="//upload.wikimedia.org/wikipedia/commons/4/42/Loading.gif" style="vertical-align: middle; height: 1em; border: 0;">' +
                    '</p>'
                );

                getRevisions(true).then(function(revlist) { // 取得し終わったら

                    // 取得した配列を整理
                    revisions = revlist;
                    cleanupRevisionsArr();

                    // ダイアログのインターフェースを作成
                    $searchDialog.empty();
                    createSearchDialogContent();

                    // 削除済みの版がある場合は警告
                    var texthidden = revisions.some(function(obj) {
                        return typeof obj.content === 'undefined';
                    });
                    if (texthidden) alert('注意: 本文を参照できない版があります');
                    
                });

            } else {
                createSearchDialogContent();
            }

        }
    });

    /**
     * 検索ダイアログのインターフェースを作成する関数
     */
    function createSearchDialogContent() {
        $searchDialog
        .dialog({
            buttons: [
                {
                    text: '戻る',
                    click: function() {
                        $(this).dialog('destroy').remove();
                        $mainDialog.dialog('open');
                        $selected.addClass('rf-selected');
                        $('.rf-matched').removeClass('rf-matched');
                    }
                },
                {
                    text: '閉じる',
                    click: function() {
                        $(this).dialog('destroy').remove();
                        $selected.addClass('rf-selected');
                        $('.rf-matched').removeClass('rf-matched');
                    }
                }
            ]
        })
        .append(
            '<div>' +
                '<input id="rf-search-input" style="width: 100%;" placeholder="検索する文字列"></input>' +
            '</div>' +
            '<div>' +
                '<input id="rf-search-rmwhitespace" type="checkbox" checked></input>' +
                '<label for="rf-search-rmwhitespace">本文内のスペースとアンダーバーを無視</label>' +
            '</div>' +
            '<div>' +
                '<input id="rf-search-rmlinebreaks" type="checkbox" checked></input>' +
                '<label for="rf-search-rmlinebreaks">本文内の改行を無視</label>' +
            '</div>' +
            '<div>' +
                '<input id="rf-search-caseinsensitive" type="checkbox"></input>' +
                '<label for="rf-search-caseinsensitive">大文字小文字を区別しない</label>' +
            '</div>' +
            '<div>' +
                '<input id="rf-search-regex" type="checkbox"></input>' +
                '<label for="rf-search-regex">正規表現モード</label>' +
            '</div>' +
            '<div id="rf-search-regex-flag-div" style="display: none;">' +
                '<label for="rf-search-regex-flag" class="rf-label">フラグ</label>' +
                '<input id="rf-search-regex-flag"></input>' +
            '</div>' +
            '<div>' +
                '<input id="rf-search-button" type="button" value="検索"></input>' +
            '</div>' +
            '<div id="rf-search-result" style="margin-top: 0.5em;"></div>'
        );
    }

    // 「検索」ボタンが押されたら検索を実行
    $(document).off('click', '#rf-search-button').on('click', '#rf-search-button', function() {

        // 検索対象の文字列を取得
        var searchStr = $('#rf-search-input').val().replace(/\u200e/g, '');
        if (!searchStr) return alert('検索する文字列を入力してください');

        // 正規表現モードの場合、使用可能な正規表現かチェック
        var regexMode = $('#rf-search-regex').is(':checked');
        if (regexMode) {
            try {
                var flag = $('#rf-search-regex-flag').val();
                var regex;
                if (flag) {
                    regex = new RegExp(searchStr, flag);
                } else {
                    regex = new RegExp(searchStr);
                }
            }
            catch (err) {
                return alert(err);
            }
        }

        // 検索ボタンを無効化、検索結果を初期化、ハイライトを全て除去
        $(this).prop('disabled', true);
        $('#rf-search-result').empty();
        $('.rf-selected').removeClass('rf-selected');
        $('.rf-matched').removeClass('rf-matched');

        // 設定を取得
        var rmwhitespace = $('#rf-search-rmwhitespace').is(':checked');
        var rmlinebreaks = $('#rf-search-rmlinebreaks').is(':checked');
        var caseinsensitive;
        if (!regexMode) caseinsensitive = $('#rf-search-caseinsensitive').is(':checked');
        var iRegex = new RegExp(mw.util.escapeRegExp(searchStr), 'i');
        var rm;
        if (rmwhitespace && rmlinebreaks) {
            rm = /[_\s]/g;
        } else if (rmwhitespace) {
            rm = /([^\S\n\r]|_)/g;
        } else if (rmlinebreaks) {
            rm = /\n/g;
        }

        // 全ての版の本文を検索
        revidsForSearch = [];
        revisions.filter(function(obj) { return obj.content; }).forEach(function(obj) {
            var content = JSON.parse(JSON.stringify(obj.content));
            if (rm) content = content.replace(rm, '');
            if (regexMode) {
                if (content.match(regex)) revidsForSearch.push(obj.revid);
            } else {
                if (caseinsensitive) {
                    if (content.match(iRegex)) revidsForSearch.push(obj.revid);
                } else {
                    if (content.indexOf(searchStr) !== -1) revidsForSearch.push(obj.revid);
                }
            }
        });

        // 結果を表示
        if (revidsForSearch.length === 0) {
            alert('該当する版は見つかりませんでした');
        } else {
            createDiffTextarea('#rf-search-result');
            $('#rf-search-result').append(
                '<input id="rf-revlist-update" type="button" style="margin-left: 0.5em;" value="対象版を更新"></input>'
            );
            $('.mw-contributions-list').children('li').find('a.mw-changeslist-date').filter(function() {
                return revidsForSearch.indexOf(parseInt($(this).attr('data-rf-revid'))) !== -1;
            }).addClass('rf-matched');
            window.requestAnimationFrame(function() {
                setTimeout(function() {
                    alert(revidsForSearch.length + '版がヒットしました');
                });
            });
        }
        $(this).prop('disabled', false); // 検索ボタンを有効化

    });

    // 正規表現モードのチェックボックスの制御
    $(document).off('change', '#rf-search-regex').on('change', '#rf-search-regex', function() {
        var checked = $(this).is(':checked');
        var display = checked ? 'block' : 'none';
        $('#rf-search-caseinsensitive').prop('disabled', checked); // 「大文字小文字を区別しない」を有効化・無効化
        $('#rf-search-regex-flag-div').css('display', display); // regexのフラグ指定inputを表示・非表示
        if (!checked) $('#rf-search-regex-flag').val(''); // チェックが外されたらフラグを白紙化
    });

    // 「対象版を更新」が押されたら、メインダイアログの対象版を更新
    $(document).off('click', '#rf-revlist-update').on('click', '#rf-revlist-update', function() {
        $('.rf-matched').removeClass('rf-matched');
        updateRevisionsList(revidsForSearch);
        $selected = $('.rf-selected');
        alert('更新しました');
    });
   
}

/**
 * ダイアログの「版指定削除」ボタンの制御用関数
 */
function doRevDel() {

    // 設定を取得
    var dp = deletePrep();
    if (!dp) return;

    // ボタンとダイアログの内容を非表示に
    var $dialog = $('#rf-dialog-main');
    var dWidth = $dialog.innerWidth();
    $dialog.dialog({
        buttons: [],
        width: dWidth
    });
    $dialog.children().hide();

    // 処理メッセージを表示
    var totalCnt = dp.revids.length;
    $dialog.append(
        '<div id="rf-progress-div">' +
            '<p id="rf-progress">' +
                '対象版 (<span id="rf-progress-resolved">0</span>/' + totalCnt + ')<br/>' +
                '<span id="rf-progress-message">' +
                    '処理中' +
                    '<img src="//upload.wikimedia.org/wikipedia/commons/4/42/Loading.gif" style="vertical-align: middle; height: 1em; border: 0;">' +
                '</span>' +
            '</p>' +
        '</div>'
    );

    // 一回のAPIリクエストで指定版の全てを処理できない場合の対策
    var apilimit = /sysop/.test(userGroups) ? 500 : 50; // 管理者の場合一度に処理できる版数は500、それ以外 (削除者) は50
    var revlist = [];
    var revidsCopy = JSON.parse(JSON.stringify(dp.revids));
    while (revidsCopy.length) {
        revlist.push(revidsCopy.splice(0, apilimit)); // 配列を配列の配列 ([ [1, ...50], [51, ...100], ...]) に変換
    }

    /**
     * 版指定削除を実行する非同期関数
     * @param {Array} ids 
     * @returns {jQuery.Promise}
     */
    var revdel = function(ids) {
        var def = new $.Deferred();

        var params = {
            action: 'revisiondelete',
            type: 'revision',
            target: pagetitle,
            ids: ids.join('|'),
            suppress: dp.suppress,
            reason: dp.reason,
            format: 'json'
        };
        params[dp.showhide] = dp.target;

        api.postWithToken('csrf', params)
        .then(function(res) {
            var resItems;
            if (res && res.revisiondelete && (resItems = res.revisiondelete.items)) { // 成功
                var successCnt = resItems.filter(function(obj) { return !obj.errors; }).length; // 削除・復帰に成功した版数を取得
                successCnt = parseInt($('#rf-progress-resolved').text()) + successCnt; // 削除・復帰に成功した版数の合計を取得
                $('#rf-progress-resolved').text(successCnt); // 進捗の版数を更新
            }
            def.resolve();
        }).catch(function(code, err) {
            console.log(err);
            def.resolve();
        });

        return def.promise();
    };

    // 版指定削除を実行
    var result = []; // 非同期処理を格納する配列
    revlist.forEach(function(arr) {
        result.push(revdel(arr));
    });

    // // 全ての非同期処理が終了した際の処理
    $.when.apply($, result).then(function() {

        // 処理に失敗した版がある場合はその版数を取得
        var failedCnt = totalCnt - parseInt($('#rf-progress-resolved').text()),
            failedMsg = failedCnt === 0 ? '' : ' (' + totalCnt + '版中' + failedCnt + '版の処理に失敗しました)';

        // 進捗を更新
        $('#rf-progress-message').prop('innerHTML',
            '<span style="color: MediumSeaGree;">処理が完了しました</span>' + failedMsg
        );

        // 処理した版のリンク表示用textareaを生成
        createDiffTextarea('#rf-progress-div');

        // ダイアログボタンを再設定
        $dialog.dialog({
            buttons: [
                {
                    text: '閉じる',
                    click: function() {
                        $(this).dialog('close');
                    }
                }
            ]
        });

    });
    
}

/**
 * 「版指定削除」ボタンを押した際に必要な情報が入力されているかをチェックし設定を取得する関数
 * @returns {{revids: Array<number>, showhide: string, target: string, suppress: string, reason: string}}
 */
function deletePrep() {

    // 削除対象のrevidを取得
    if ($('.rf-targetrevisions').length === 0) return alert('版が指定されていません');
    var revidsNested = $('.rf-targetrevisions').map(function() { return $(this).attr('data-rf-revidchunk'); }).get();
    revidsNested = revidsNested.map(function(el) { return JSON.parse(el); });
    var revids = [].concat.apply([], revidsNested).sort(function(a, b) { return a - b; });

    // 版内容の何を削除・復帰の対象とするかの設定を取得
    var revdelTarget = [];
    var tars = ['content', 'user', 'comment'];
    var idPrefix = '#rf-revdelsettings-';
    tars.forEach(function(el) {
        if ($(idPrefix + el).is(':checked')) revdelTarget.push(el);
    });
    if (revdelTarget.length === 0) return alert('削除・復帰の対象とする版内容が指定されていません');

    // 削除理由を取得
    var reason = '';
    $('.rf-revdelsettings-reason').each(function() {
        var v = $(this).val();
        if (v) reason += v + ': ';
    });
    reason += $('#rf-revdelsettings-reasonC').val().replace(/\u200e/g, '').trim();
    reason = reason.replace(/: $/, '');
    if (!reason) {
        if (!confirm('理由が入力されていません。このまま実行しますか?')) return;
    }

    // 最終確認
    if (!confirm(revids.length + '版を削除・復帰します。よろしいですか?')) return;

    // オブジェクトをreturn
    return {
        revids: revids,
        target: revdelTarget.join('|'),  // 例: 'content|user|comment'
        showhide: $('#rf-revdelsettings-mode').children('option').filter(':selected').val() === '削除' ? 'hide' : 'show',
        suppress: $('#rf-revdelsettings-oversight').is(':checked') ? 'yes' : 'nochange', // オーバーサイトするか否か
        reason: reason
    };

}

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

})(mediaWiki, jQuery);
//</nowiki>