LLWiki正在建设中,欢迎加入我们

“MediaWiki:Gadget-inspect.js”的版本间差异

来自LLWiki
跳转到导航 跳转到搜索
第105行: 第105行:
mw.request = mw.request || mw.standardQuery(api);
mw.request = mw.request || mw.standardQuery(api);
$original.find( ':header:has(.mw-editsection)' ).dblclick(function() {
$original.find( ':header:has(.mw-editsection)' ).dblclick(function() {
section = mw.util.getParamValue( $(this).find( '.mw-editsection > a' ).attr('href'), 'section' );
section = mw.util.getParamValue( 'section', $(this).find( '.mw-editsection > a' ).attr('href') );
mw.sections = mw.sections || [];
mw.sections = mw.sections || [];
mw.sections[ section ] = mw.sections[ section ] || mw.timedQuery(api, {action: 'parse', oldid: curRevid,
mw.sections[ section ] = mw.sections[ section ] || mw.timedQuery(api, {action: 'parse', oldid: curRevid,

2021年1月9日 (六) 21:32的版本

//<nowiki>
// 由ResourceLoader直接调用,不可使用ES6语法
/**
 * @Function: 在页面内快速编辑和预览
 * @Author: [[User:Bhsd]]
 */
"use strict";
/*global mw, $, OO, CodeMirror, wgULS, wgUCS*/
const id = mw.config.get( 'wgArticleId' ),
    curRevid = mw.config.get( 'wgCurRevisionId' ),
    page = mw.config.get( 'wgPageName' ), // 这里未转义,后面应用时需要注意
    isCode = mw.config.get( 'wgPageContentModel' ) != 'wikitext', // javascript/css/Scribunto/json
    gadgets = mw.gadgets || {},
    inspect = gadgets.inspect || {},
    rule = inspect.rule,
    charInsert = gadgets.charinsert || {},
    src = charInsert.src || 'special:我的用户页/edittools';
$(function() {
    // 特殊页面或页面未创建、重定向、不是阅读模式、历史版本
    if (id === 0 || mw.config.get('wgIsRedirect') || mw.config.get('wgAction') != 'view' ||
        mw.config.get( 'wgRevisionId' ) < curRevid || !(rule === undefined ? true : rule)) { return; }
    // 由于resizable,left, top, height必须加!important,width不可加!important
    const cssHide = mw.util.addCSS( '#inspector { position:fixed; bottom:0; right:24px; left:unset !important;' +
        'top:unset !important; height:unset !important; width:calc(50% - 7rem - 0.5px); }' ),
        $content = $('#mw-content-text, #mw-imagepage-content').last(),
        // wikitext/Scribunto对应.mw-parser-output,javascript/css对应.mw-code,json对应.mw-json
        $original = $content.children('.mw-json, .mw-code, .mw-parser-output'),
        $outer = $('<div>', {id: 'inspector', class: 'mw-ajax-loader'}).insertBefore( $original ),
        api = new mw.Api(),
        initList = function(pages) {
        flag = pages[0] == id ? 1 : 0;
        list = [pages, flag];
        mw.storage.setObject( 'inspect-category', [pages, 0] );
        btnGo.setHref( '/zh?redirect=no&curid=' + pages[ flag ] ).setDisabled( false );
    };
    var nextid, list = mw.storage.getObject( 'inspect-category' ), flag;
    if (!list) { nextid = id + 1; }
    else {
        flag = (list[0][ list[1] ] == id);
        if (flag) { list[1]++; }
        nextid = list[0][ list[1] ];
    }
    const btnGo = new OO.ui.ButtonWidget({flags: 'progressive', icon: 'next', href: '/zh?redirect=no&curid=' + nextid,
        disabled: !nextid});
    // 这个click事件不能绑到btnGo上,否则会禁用href跳转
    btnGo.$element.click(function() { if (list && flag) { mw.storage.setObject( 'inspect-category', list ); } })
        .contextmenu(function(e) {
        e.preventDefault();
        OO.ui.prompt( wgULS('请输入分类名或名字空间编号:', '請輸入分類名或名字空間編號:') ).then(function(cat) {
            if (cat === null) { return; }
            // 输入空白则清除分类
            if (cat === '') {
                mw.storage.remove( 'inspect-category' );
                btnGo.setHref( '/zh?redirect=no&curid=' + (id + 1) ).setDisabled( false );
                return;
            }
            // 输入数字对应名字空间
            if (!isNaN(cat)) {
                if (!Object.keys( mw.config.get( 'wgFormattedNamespaces' ) ).includes(cat)) {
                    mw.notify(wgULS('错误的名字空间编号!', '錯誤的名字空間編號!'), {type: 'error'});
                    return;
                }
                mw.timedQuery(api, {list: 'allpages', apnamespace: cat, apfilterredir: 'nonredirects', aplimit: 'max'},
                    wgULS('该名字空间的页面列表', '該名字空間的頁面列表')).then(function(ap) {
                    const pages = ap.query.allpages.map(function(ele) { return ele.pageid; })
                        .sort(function(a, b) { return a < b; }); // 由新到旧排列
                    if (pages.length === 0) {
                        mw.notify( wgULS('该名字空间沒有非重定向的页面!', '該名字空間沒有非重定向的頁面!'), {type: 'warn'} );
                        return;
                    }
                    initList(pages);
                }, function() {}); // mw.timedQuery已经通知过错误信息,这里只要一个空函数处理reject,下同
                return;
            }
            if (!/^(category|分[类類]):/i.test( cat )) { cat = 'Category:' + cat; }
            // cmtitle参数不可自动转换,所以需要先获得转换后的正确标题
            mw.timedQuery(api, {titles: cat, converttitles: 1}, wgULS('标准页面名称', '標準頁面名稱')).then(function(r) {
                const target = r.query.pages[0];
                if (target.missing) {
                    mw.notify(wgULS('错误的分类名!', '錯誤的分類名!'), {type: 'error'});
                    return;
                }
                mw.timedQuery(api, {list: 'categorymembers', cmtitle: target.title, cmprop: 'ids', cmlimit: 'max',
                    cmsort: 'timestamp', cmdir: 'older'}, wgULS('分类下的页面列表', '分類下的頁面列表')).then(function(cm) {
                    const pages = cm.query.categorymembers.map(function(ele) { return ele.pageid; });
                    if (pages.length === 0) {
                        mw.notify( wgULS('该分类下无页面!', '該分類下無頁面!'), {type: 'warn'} );
                        return;
                    }
                    initList(pages);
                }, function() {});
            }, function() {});
        });
    });
    // 模板、JavaScript、CSS、Lua、JSON只需要箭头按钮
    if ((mw.config.get( 'wgNamespaceNumber' ) == 10 && !page.endsWith( '/doc' )) || isCode) {
        $outer.removeClass( 'mw-ajax-loader' ).append( $('<div>', {id: 'inspector-btns', html: btnGo.$element}) );
        mw.loader.addStyleTag( '#inspector-btns { width:auto; }' );
        return;
    }
    // 先提交Ajax请求,这里手动设置cache: true
    const getJSON = $.get({ dataType: 'json', cache: true,
        url: '/zh?title=mediawiki:gadget-CodeMirror.json&action=raw&ctype=application/json' });
    // 生成通用的API请求
    mw.request = mw.request || mw.standardQuery(api);
    $original.find( ':header:has(.mw-editsection)' ).dblclick(function() {
        section = mw.util.getParamValue( 'section', $(this).find( '.mw-editsection > a' ).attr('href') );
        mw.sections = mw.sections || [];
        mw.sections[ section ] = mw.sections[ section ] || mw.timedQuery(api, {action: 'parse', oldid: curRevid,
            prop: 'wikitext', section: section}, wgULS('章节Wikitext', '章節Wikitext'));
        mw.sections[ section ].then(function(r) { editor.setValue( r.parse.wikitext ); }, function() {});
    });
    // 标注<p>标签
    const css = mw.util.addCSS( '.empty { border:1px solid rgba(253,220,154,0.5); box-shadow:0 0 0.5em #fddc9a; }' +
        // 解决<ul>等元素文字不换行的问题
        '#mw-content-text .mw-parser-output { display:flow-root; overflow:hidden; word-wrap:break-word; }' );
    css.disabled = true;
    var dialog, actionP, actionD, text, editor, $wrapper, $replace, $charinsert, prefix, ns, section,
        lang = mw.config.get( 'wgUserVariant' );
    // 也可以使用user.options,这里使用其他方法绕过
    const isGadget = function(name) {
        return ['loaded', 'loading', 'ready'].includes( mw.loader.getState('ext.gadget.' + name) );
    },
        isBackup = isGadget('contentBackup'),
        backupObj = isBackup ? Object.fromEntries( mw.storage.getObject( 'LLWiki-contentBackup' ) || [] ) : {},
        backup = isBackup ? function() {
        backupObj[id] = [mw.now(), editor.getValue()];
        mw.storage.setObject( Object.entries(backupObj) );
    } : function() {},
        loadBackup = function() {
        const backupContent = (backupObj[id] || [])[1];
        if (!backupContent) {
            mw.notify(wgULS('当前页面尚未备份!', '當前頁面尚未備份!'), {type: 'warn'});
            return;
        }
        mw.confirm( wgULS('要加载备份吗?', '要加載備份嗎?'), 'progressive' ).then(function(confirm) {
            if (!confirm) { return; }
            editor.setValue( backupContent );
            mw.notify(wgULS('已还原备份!', '已復原備份!'), {type: 'success'});
        });
    },
        pEmpty = function($div) {
        $div.find( 'p:not(:has(script))' ).filter(function() { return !/\S/.test( this.textContent ); })
            .addClass('empty');
    },
        $placeholder = $('<div>', {class: "mw-parser-output"}).css('display', 'none'), // 用于替换时保留原始数据
        $url = $('<a>'), // 桌面版CSS不必需href
        $warning = $('<div>', {html: [wgULS("确定要还原为未编辑的状态吗?", "確認要復原為未編輯的狀態嗎?"), '<br>',
            wgULS("建议您做好编辑内容备份。", "建議您做好編輯內容備份。")]}),
        btns = [new OO.ui.ButtonWidget({label: '提交', flags: ['primary', 'progressive']}).on('click', function() {
        backup();
        // 改变CSS样式表示提交中
        btns[0].setDisabled( true );
        mw.safeEdit(api, curRevid, $.extend({ pageid: id, text: editor.getValue(),
            summary: wgULS( '使用[[help:小工具/页面文本对比查看器|页面/文本对比查看器]]快速编辑',
                '使用[[help:小工具/页面文本对比查看器|頁面/文本對比察看器]]快速編輯'
            ) }, section === undefined ? {section: section} : {}), isBackup).then(function() { location.reload(); },
            function(reason) { btns[0].setDisabled( reason == 'editConflict' ); }); // 编辑冲突必须刷新页面,不应重复提交
    }), new OO.ui.ButtonWidget({label: wgULS('预览', '預覽')}).on('click', function() {
        backup();
        // 改变CSS样式表示预览中
        btns[1].setDisabled( true );
        console.log('API request: 请求预览');
        const now = mw.now();
        api.parse(editor.getValue(), $.extend({title: page, uselang: lang, disablelimitreport: 1,
            disableeditsection: 1}, section === undefined ? {section: section} : {})).then(function(html) {
            console.log('End API request: 已生成预览,用时 ' + (mw.now() - now) + ' ms');
            if ($.contains( $content[0], $original[0] )) { $original.after( $placeholder ).detach(); }
            $content.children( '.mw-parser-output' ).replaceWith( html );
            pEmpty( $content.children( '.mw-parser-output' ) );
            if (mw.resizeLyrics) { mw.resizeLyrics(); }
            mw.hook( 'wikipage.content' ).fire($content);
        }, function(reason) { mw.apiFailure(reason, wgULS('预览', '預覽')); }) // mw.apiFailure不会抛出错误
            .then(function() { btns[1].setDisabled( false ); });
    }), new OO.ui.ButtonWidget({label: wgULS('还原', '復原'), flags: 'destructive', disabled: true})
        .on('click', function() {
        mw.confirm($warning, ['primary', 'destructive']).then(function(confirm) {
            if (!confirm) { return; }
            section = undefined;
            editor.setValue( text );
            $content.children( '.mw-parser-output' ).replaceWith( $original );
            if (mw.resizeLyrics) { mw.resizeLyrics(); }
        });
    }), new OO.ui.ButtonWidget({label: wgULS('显示', '顯示')}).on('click', function() {
        css.disabled = !css.disabled;
        cssHide.disabled = !cssHide.disabled;
        $wrapper.toggle();
        btns[3].setLabel( css.disabled ? wgULS('显示', '顯示') : wgULS('隐藏', '隱藏'));
        btns[2].setDisabled( css.disabled ); // 折叠时无法更新textarea
        if (mw.resizeLyrics) { mw.resizeLyrics(); }
    }), btnGo],
        options = $('<div>', {text: wgULS('查找替换', '查找替換')}).click(function() {
        if ($replace) {
            $replace.show();
            return;
        }
        var replaceBackup;
        const enable = function() { replaceBtn.setDisabled( false ); },
            ptn = new OO.ui.TextInputWidget().on('change', enable),
            val = new OO.ui.MultilineTextInputWidget({autosize: true, maxRows: 3}).on('change', enable),
            regex = new OO.ui.CheckboxInputWidget().on('change', enable),
            modifier = new OO.ui.CheckboxInputWidget().on('change', enable),
            undoBtn = new OO.ui.ButtonWidget({label: wgULS('撤销', '撤銷'), disabled: true, flags: ['destructive']})
            .on('click', function() {
            editor.setValue( replaceBackup );
            enable();
        }),
            replaceBtn = new OO.ui.ButtonWidget({label: wgULS('替换', '替換'), flags: ['progressive']})
            .on('click', function() {
            const pattern = ptn.getValue(),
                regexp = new RegExp(regex.isSelected() ? pattern : mw.util.escapeRegExp(pattern),
                'g' + (modifier.isSelected() ? 'i' : ''));
            replaceBackup = editor.getValue();
            undoBtn.setDisabled( false );
            replaceBtn.setDisabled( true ); // 防止连续点击造成无法撤销
            editor.setValue( replaceBackup.replace(regexp, val.getValue()) );
        }),
            hideBtn = new OO.ui.ButtonWidget({label: wgULS('关闭', '關閉')})
            .on('click', function() {
            $replace.hide();
            enable();
        });
        $replace = $('<div>', {class: 'inspector-field', html: [
            $('<div>', {html: ['查找:', ptn.$element]}),
            $('<div>', {html: [wgULS('替换:', '替換:'), val.$element]}),
            $('<div>', {html: [
                wgULS('正则', '正則'), regex.$element,
                $('<i>', {text: 'i'}), modifier.$element,
                replaceBtn.$element, undoBtn.$element, hideBtn.$element
            ]})
        ]}).appendTo( 'body' ).draggable();
    }).add(isGadget('charinsert') ? $('<div>', {text: '快速插入'}).click(function() {
        if ($charinsert) {
            $charinsert.show();
            return;
        }
        $charinsert = $('<div>', {html: $('<div>', {text: '快速插入工具', id: 'inspector-field-title'}),
            class: 'inspector-field mw-ajax-loader'}).appendTo('body').draggable().contextmenu(function(e) {
            e.preventDefault();
            $charinsert.hide();
        }).on('click', '.mw-charinsert-item', function() {
            const $this = $(this),
                start = $this.data('mw-charinsert-start') || $this.data('start') || '',
                end = $this.data('mw-charinsert-end') || $this.data('end') || '';
            editor.replaceSelection( start + editor.getSelection() + end );
        });
        // 为了充分利用浏览器缓存,这里不使用API;注意url转义,这里不需要简化'/mediawiki/index.php'
        $.get({url: mw.util.getUrl(src, {variant: mw.config.get( 'wgUserLanguage' )}),
            cache: charInsert.cache !== false, dataType: 'text'}).then(function(doc) {
            $charinsert.append( $( doc.match(/<body[\s\S]+<\/body>/)[0] ).find( '.mw-parser-output' ) );
        }, function() {
            mw.notify([
                wgULS('无法加载', '無法加載'),
                $('<a>', {text: src, href: mw.util.getUrl(src)}),
                wgULS('!请检查该页面是否存在。', '!請檢查該頁面是否存在。')
            ], {type: 'error', autoHideSeconds: 'long'});
        }).then(function() { $charinsert.removeClass( 'mw-ajax-loader' ); });
    }) : null),
        $dropdown = $('<div>', {class: "inspector-menu", html: options}),
        $hints = $('<div>', {class: "inspector-menu"}).on('click', '.inspector-hint', function() {
        const cursor = editor.getCursor();
        editor.replaceRange(this.textContent + (ns > 0 ? '}}' : ']]'),
            {line: cursor.line, ch: cursor.ch - prefix.length}, cursor);
        // 随后再次执行updateHints时会自行stop
    }),
        // 判别模板时可能会误处理解析器函数
        regexps = {0: /\[\[\s*:?([^|[\]{}<>]*)$/i, 10: /{{\s*([^|[\]{}<>#]*)$/i,
        828: /{{\s*#invoke:\s*([^|[\]{}<>#:]*)$/i, 274: /{{\s*#widget:\s*([^|[\]{}<>#:]*)$/i},
        $noHint = $('<div>', {class: 'error', text: wgULS('不存在对应页面!', '不存在對應頁面!')}),
        nsPrefixes = {0: '', 10: 'Template:', 828: '模块:', 274: 'Widget:'},
        autocomplete = isGadget('autocomplete') ? function(e) {
        if (![9, 27].includes( e.keyCode )) { return; } // tab和esc
        var $cursor, before;
        const cursorActivity = function() {
            const newCursor = editor.getCursor(),
                newLine = editor.getLine( newCursor.line ),
                newAfter = newLine.slice( newCursor.ch );
            // 如果光标改变过位置
            if (newCursor.line != cursor.line || newAfter != after) {
                stop();
                return;
            }
            before = newLine.slice(0, newCursor.ch);
            updateHints();
        },
            stop = function() {
            $hints.slideUp('fast');
            editor.off('cursorActivity', cursorActivity);
        },
            updateHints = mw.util.debounce(500, function() {
            if (!regexps[ns].test( before )) {
                stop();
                return;
            }
            $cursor = $wrapper.find( '.CodeMirror-cursor' );
            prefix = before.match( regexps[ns] )[1];
            if (prefix === "") { return; }
            mw.timedQuery(api, $.extend({list: 'prefixsearch', pssearch: prefix}, ns > 0 ? {psnamespace: ns} : {}),
                wgULS('前缀相符的页面', '前綴相符的頁面')).then(function(r) {
                $hints.html( r.query.prefixsearch.map(function(ele) {
                    return $('<div>', {class: 'inspector-hint', text: ele.title.slice( nsPrefixes[ns].length )});
                }) ).show().position({my: 'left top', of: $cursor, within: $wrapper, collision: 'fit'});
                if ($hints.children().length === 0) { $hints.append( $noHint ); }
            }, function() { stop(); });
        });
        if (e.keyCode == 27) {
            stop();
            return;
        }
        editor.execCommand( 'delCharBefore' ); // 删掉新增的\t
        const cursor = editor.getCursor(),
            line = editor.getLine( cursor.line ),
            after = line.slice( cursor.ch );
        before = line.slice(0, cursor.ch);
        $.each(regexps, function(k, v) { if (v.test(before)) { ns = k; } });
        if (!ns) { return; }
        mw.notify(wgULS('开启自动补全提示。', '開啟自動補全提示。'), {tag: 'autocomplete'});
        editor.on('cursorActivity', cursorActivity);
        // 实际上updateHints总是延迟500ms执行,因此下一行代码的位置影响不大
        updateHints();
    } : function() {};
    $outer.on('contextmenu', '.cm-mw-template-name', function() {
        const template = '模板:' + $(this).text();
        var label;
        // 这里增加一次确认界面是因为部分浏览器不允许JS直接打开新标签页。
        if (!dialog) {
            actionP = new OO.ui.ActionWidget({label: '是', target: '_blank', flags: 'progressive'});
            actionD = new OO.ui.ActionWidget({label: '否', flags: 'destructive'});
            label = [wgULS('要在新标签页打开', '要在新標籤頁打開'), $url, wgULS('吗?', '嗎?')];
        }
        $url.text( template );
        actionP.setHref( mw.util.getUrl(template) ); // 注意url转义
        dialog = mw.dialog(dialog, [actionP, actionD], label);
        return false; // 这里需要同时执行stopImmediatePropagation()和preventDefault()
    }).on('contextmenu', '.CodeMirror', function(e) {
        e.preventDefault();
        // 使用jquery.ui让菜单总是落在CodeMirror内,此时slideDown效果不生效
        $dropdown.show().position({my: 'left top', of: e, within: $wrapper, collision: 'fit'});
    }).on('keydown', '.CodeMirror', autocomplete ).resizable( {handles: 'w', minWidth: 350} );
    $('body').click(function() { $dropdown.slideUp('fast'); });
    if (isBackup) {
        btns[2].$element.contextmenu(function(e) {
            e.preventDefault();
            loadBackup();
        });
    }
    if (isGadget( 'PreviewWithVariant' ) ) {
        const options = [{label: "大陆简体", data: "zh-cn"}, {label: "香港繁體", data: "zh-hk"},
            {label: "澳門繁體", data: "zh-mo"}, {label: "马来西亚简体", data: "zh-my"},
            {label: "新加坡简体", data: "zh-sg"}, {label: "臺灣繁體", data: "zh-tw"}],
            select = new OO.ui.DropdownInputWidget({classes: ['inspector-variant'], options: options, value: lang})
            .on('change', function() { lang = select.getValue(); }),
            menu = select.$element.find( '.oo-ui-menuSelectWidget' ).click(function() {
            menu.addClass( 'oo-ui-element-hidden' );
        });
        select.$element.prependTo( btns[1].$element );
        btns[1].$element.contextmenu(function(e) {
            e.preventDefault();
            menu.removeClass( 'oo-ui-element-hidden' );
        }).children('a').blur(function() { menu.addClass( 'oo-ui-element-hidden' ); });
    }
    pEmpty( $original );
    // $.when很容易出错,这里保险起见用Promise.all
    Promise.all([getJSON, mw.request]).then(function(data) {
        mw.config.set('extCodeMirrorConfig', data[0]);
        mw.hook( 'codemirror.config' ).fire();
        text = data[1].query.pages[0].revisions[0].content;
        editor = new CodeMirror($outer[0], {value: text, mode: 'text/mediawiki', mwConfig: data[0],
            lineWrapping: true, lineNumbers: true});
        $wrapper = $( editor.getWrapperElement() ).toggle().append( [$dropdown, $hints] );
        $('<div>', {id: 'inspector-btns', html: btns.map(function(ele) { return ele.$element; })}).appendTo( $outer );
        // 处理页面上方的差异
        $('.diff').click(function(e) {
            const row = $( e.target ).closest('tr'),
                isLineno = row.children( '.diff-lineno' ).length ? 1 : 0,
                rowLineno = isLineno ? row : row.prevAll( ':has(.diff-lineno)' ).first(),
                n = parseInt( rowLineno.children().last().text().match(/\d+/) ) + row.index() - rowLineno.index()
                - 2 + isLineno, // 使用parseInt规避可能的程序错误,注意CodeMirror的行号从0开始
                nLine = editor.lastLine();
            if (nLine >= n) {
                editor.scrollIntoView(nLine);
                editor.scrollIntoView(n);
            }
            else { mw.notify( wgULS('当前不存在该行!', '當前不存在該行!'), {type: 'warn'} ); }
        });
    }, function(reason) { mw.apiFailure(reason, 'CodeMirror' + wgULS('设置或页面', '設置或頁面') + 'Wikitext'); })
        .then(function() { $outer.removeClass( 'mw-ajax-loader' ); });
});
//</nowiki>
// [[category:维护工具]] [[category:桌面版小工具]] [[category:需要自确用户权限的小工具]] [[category:作为模块的小工具]]
// {{DEFAULTSORT:inspect.js}}