跳转到内容

User:Chatsven/FastEditData.js

维基百科,自由的百科全书
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
// <nowiki>
// 功能:在中文维基页面侧栏加入“修改描述”,修改当前页面关联 Wikidata 项目的中文描述与繁体中文描述。
// 说明:弹窗 CSS 直接沿用原文档的 vpf-detail 弹窗体系,只做必要的输入框与撤销按钮扩展。

(function () {
    'use strict';

    const TOOL_NAME = 'Wikidata描述修改';
    const PORTLET_LINK_ID = 'ca-wikidata-description-editor';
    const STYLE_ID = 'wikidata-description-editor-style';
    const WIKIDATA_API = 'https://www.wikidata.org/w/api.php';

    let wddState = {
        qid: null,
        pageTitle: null,
        previousDescriptions: null,
        currentRevisionId: null,
        createdNewItem: false,
        submitting: false,
        undoing: false
    };

    mw.loader.using(['mediawiki.api', 'mediawiki.util'], function () {
        addStyles();
        addPortletButton();
        window.setTimeout(ensurePortletButton, 400);
        window.setTimeout(ensurePortletButton, 1200);
        window.setTimeout(ensurePortletButton, 2500);
    });

    function addStyles() {
        if ($('#' + STYLE_ID).length) {
            return;
        }

        $('head').append(
            $('<style>')
                .attr('id', STYLE_ID)
                .text(`
                    #vpf-overlay {
                        scrollbar-width: none;
                        -ms-overflow-style: none;
                    }

                    #vpf-overlay::-webkit-scrollbar {
                        display: none;
                        width: 0;
                        height: 0;
                    }

                    @keyframes vpf-window-open {
                        0% {
                            opacity: 0;
                            transform: translate(-50%, -48%) scale(0.96);
                        }

                        100% {
                            opacity: 1;
                            transform: translate(-50%, -50%) scale(1);
                        }
                    }

                    @keyframes vpf-window-close {
                        0% {
                            opacity: 1;
                            transform: translate(-50%, -50%) scale(1);
                        }

                        100% {
                            opacity: 0;
                            transform: translate(-50%, -48%) scale(0.96);
                        }
                    }

                    @keyframes vpf-detail-overlay-fade-in {
                        0% {
                            opacity: 0;
                        }

                        100% {
                            opacity: 1;
                        }
                    }

                    @keyframes vpf-detail-overlay-fade-out {
                        0% {
                            opacity: 1;
                        }

                        100% {
                            opacity: 0;
                        }
                    }

                    @keyframes wdd-undo-fade-in {
                        0% {
                            opacity: 0;
                            transform: translateY(3px);
                        }

                        100% {
                            opacity: 1;
                            transform: translateY(0);
                        }
                    }

                    .vpf-overlay {
                        position: fixed;
                        inset: 0;
                        z-index: 9998;
                        background: rgba(32, 33, 36, 0.28);
                        backdrop-filter: blur(6px);
                        -webkit-backdrop-filter: blur(6px);
                        font-family: -apple-system, BlinkMacSystemFont, "SF Pro", "Segoe UI", sans-serif;
                        font-size: 12px;
                        line-height: 1.35;
                    }
                    .vpf-detail-layer {
                        position: fixed;
                        inset: 0;
                        z-index: 100;
                        display: flex;
                        align-items: center;
                        justify-content: center;
                        box-sizing: border-box;
                        background: transparent;
                        opacity: 0;
                        animation: vpf-detail-overlay-fade-in 0.22s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
                    }

                    .vpf-detail-layer.vpf-detail-closing {
                        pointer-events: none;
                        animation: vpf-detail-overlay-fade-out 0.18s cubic-bezier(0.4, 0, 1, 1) forwards;
                    }

                    .vpf-detail-card {
                        position: absolute;
                        left: 50%;
                        top: 50%;
                        width: 390px;
                        min-height: 330px;
                        height: 330px;
                        transform: translate(-50%, -50%);
                        box-sizing: border-box;
                        border-radius: 26px;
                        background: #FFFFFF;
                        overflow: hidden;
                        box-shadow: 0 8px 40px 0 rgba(0, 0, 0, 0.12);
                        animation: vpf-window-open 0.22s cubic-bezier(0.2, 0.8, 0.2, 1);
                        transform-origin: center center;
                    }

                    .vpf-detail-layer.vpf-detail-closing .vpf-detail-card {
                        animation: vpf-window-close 0.18s cubic-bezier(0.4, 0, 1, 1) forwards;
                    }

                    .vpf-detail-title {
                        position: absolute;
                        left: 120px;
                        top: 20px;
                        right: 31px;
                        color: rgba(0, 0, 0, 0.85);
                        font-family: -apple-system, BlinkMacSystemFont, "SF Pro", "Segoe UI", sans-serif;
                        font-size: 13px;
                        font-style: normal;
                        font-weight: 700;
                        line-height: 16px;
                        overflow: hidden;
                        text-overflow: ellipsis;
                        white-space: nowrap;
                    }

                    .vpf-detail-subtitle {
                        position: absolute;
                        left: 120px;
                        top: 44px;
                        right: 31px;
                        color: rgba(0, 0, 0, 0.85);
                        font-family: -apple-system, BlinkMacSystemFont, "SF Pro", "Segoe UI", sans-serif;
                        font-size: 11px;
                        font-style: normal;
                        font-weight: 400;
                        line-height: 14px;
                    }

                    .vpf-detail-row-label {
                        position: absolute;
                        left: 34px;
                        display: flex;
                        width: 80px;
                        height: 24px;
                        flex-direction: column;
                        justify-content: center;
                        box-sizing: border-box;
                        color: rgba(0, 0, 0, 0.50);
                        text-align: right;
                        font-family: -apple-system, BlinkMacSystemFont, "SF Pro", "Segoe UI", sans-serif;
                        font-size: 13px;
                        font-style: normal;
                        font-weight: 400;
                        line-height: 14px;
                        letter-spacing: -0.26px;
                    }

                    .vpf-detail-row-value {
                        position: absolute;
                        left: 126px;
                        right: 31px;
                        display: -webkit-box;
                        height: 24px;
                        -webkit-box-orient: vertical;
                        -webkit-line-clamp: 1;
                        align-items: center;
                        overflow: hidden;
                        color: rgba(0, 0, 0, 0.85);
                        text-overflow: ellipsis;
                        font-family: -apple-system, BlinkMacSystemFont, "SF Pro", "Segoe UI", sans-serif;
                        font-size: 13px;
                        font-style: normal;
                        font-weight: 510;
                        line-height: 16px;
                        mix-blend-mode: plus-darker;
                    }

                    .vpf-detail-row-value > span {
                        display: flex;
                        height: 24px;
                        align-items: center;
                        overflow: hidden;
                        text-overflow: ellipsis;
                        white-space: nowrap;
                    }

                    .vpf-detail-row-value.vpf-detail-summary {
                        display: block;
                        height: auto;
                        min-height: 24px;
                        line-height: 16px;
                        overflow: visible;
                        text-overflow: clip;
                        -webkit-line-clamp: unset;
                        -webkit-box-orient: initial;
                    }

                    .vpf-detail-row-value.vpf-detail-summary > span {
                        display: block;
                        height: auto;
                        max-height: none;
                        align-items: initial;
                        overflow: visible;
                        white-space: normal;
                        overflow-wrap: anywhere;
                        word-break: break-word;
                        text-overflow: clip;
                    }

                    .vpf-detail-row-1 {
                        top: 96px;
                    }

                    .vpf-detail-row-2 {
                        top: 134px;
                    }

                    .vpf-detail-row-3 {
                        top: 172px;
                    }

                    .vpf-detail-row-4 {
                        top: 210px;
                    }

                    .vpf-detail-row-5 {
                        top: 248px;
                    }

                    .vpf-detail-actions {
                        position: absolute;
                        right: 20px;
                        bottom: 20px;
                        display: flex;
                        gap: 8px;
                    }

                    .vpf-detail-button {
                        display: flex;
                        width: 74px;
                        height: 24px;
                        justify-content: center;
                        align-items: center;
                        box-sizing: border-box;
                        border: 0;
                        border-radius: 8px;
                        cursor: pointer;
                        text-align: center;
                        font-family: -apple-system, BlinkMacSystemFont, "SF Pro", "Segoe UI", sans-serif;
                        font-size: 13px;
                        font-style: normal;
                        font-weight: 510;
                        line-height: 16px;
                        mix-blend-mode: plus-darker;
                    }

                    .vpf-detail-button:focus {
                        outline: none;
                    }

                    .vpf-detail-close {
                        background: rgba(0, 0, 0, 0.06);
                        color: #1A1A1A;
                    }

                    .vpf-detail-add {
                        background: #007AFF;
                        color: #FFFFFF;
                    }

                    .vpf-detail-add:disabled {
                        opacity: 0.45;
                        cursor: not-allowed;
                    }

                    .wdd-detail-input {
                        display: block;
                        width: 100%;
                        height: 24px;
                        box-sizing: border-box;
                        border: 1px solid rgba(0, 0, 0, 0.08);
                        border-radius: 8px;
                        background: #FFFFFF;
                        color: rgba(0, 0, 0, 0.85);
                        font-family: -apple-system, BlinkMacSystemFont, "SF Pro", "Segoe UI", sans-serif;
                        font-size: 13px;
                        font-style: normal;
                        font-weight: 510;
                        line-height: 16px;
                        padding: 0 8px;
                        outline: none;
                        mix-blend-mode: plus-darker;
                        transition:
                            border-color 0.16s ease,
                            box-shadow 0.16s ease,
                            opacity 0.16s ease;
                    }

                    .wdd-detail-input:focus {
                        border-color: rgba(0, 0, 0, 0.08);
                        box-shadow:
                            0 0 0 1px rgba(0, 0, 0, 0.08),
                            0 0 0 3.5px rgba(0, 122, 255, 0.25),
                            0 0 0 7px rgba(0, 122, 255, 0.15);
                    }

                    .wdd-detail-input:disabled {
                        opacity: 0.45;
                        cursor: not-allowed;
                    }

                    .wdd-detail-undo {
                        display: none;
                        background: rgba(255, 0, 0, 0.13);
                        color: #FF0000;
                        opacity: 0;
                    }

                    .wdd-detail-undo.wdd-detail-undo-visible {
                        display: flex;
                        animation: wdd-undo-fade-in 0.22s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
                    }

                    .wdd-hidden {
                        display: none !important;
                    }
                `)
        );
    }

    function addPortletButton() {
        $('#' + PORTLET_LINK_ID).remove();

        const linkNode = mw.util.addPortletLink(
            'p-cactions',
            '#',
            '修改描述',
            PORTLET_LINK_ID,
            '修改当前条目在 Wikidata 上的中文描述'
        );

        if (linkNode) {
            bindPortletButton();
            return true;
        }

        return false;
    }

    function ensurePortletButton() {
        if ($('#' + PORTLET_LINK_ID).length || $('#pt-' + PORTLET_LINK_ID).length) {
            bindPortletButton();
            return;
        }

        if (addPortletButton()) {
            return;
        }

        addPortletButtonToTools();
    }

    function addPortletButtonToTools() {
        const ids = ['p-tb', 'p-page-tools', 'p-associated-pages', 'p-personal'];

        for (let i = 0; i < ids.length; i += 1) {
            if (!document.getElementById(ids[i])) {
                continue;
            }

            const linkNode = mw.util.addPortletLink(
                ids[i],
                '#',
                '修改描述',
                PORTLET_LINK_ID,
                '修改当前条目在 Wikidata 上的中文描述'
            );

            if (linkNode) {
                bindPortletButton();
                return true;
            }
        }

        return false;
    }

    function bindPortletButton() {
        $('#' + PORTLET_LINK_ID + ', #pt-' + PORTLET_LINK_ID)
            .off('click.wdd')
            .on('click.wdd', function (e) {
                e.preventDefault();
                openDescriptionDialog();
            });

        $('#' + PORTLET_LINK_ID + ' a, #pt-' + PORTLET_LINK_ID + ' a')
            .off('click.wdd')
            .on('click.wdd', function (e) {
                e.preventDefault();
                openDescriptionDialog();
            });
    }

    function openDescriptionDialog() {
        if ($('#vpf-overlay').length) {
            return;
        }

        wddState = {
            qid: null,
            pageTitle: mw.config.get('wgTitle') || mw.config.get('wgPageName') || document.title,
            previousDescriptions: null,
            currentRevisionId: null,
            createdNewItem: false,
            submitting: false,
            undoing: false
        };

        renderLoadingDialog();

        fetchCurrentPageEntity()
            .then(function (entityData) {
                wddState.qid = entityData.qid || null;
                wddState.previousDescriptions = entityData.descriptions || {};
                wddState.currentRevisionId = entityData.lastrevid || null;
                wddState.createdNewItem = false;
                renderReadyDialog();
            })
            .fail(function (err) {
                console.error(err);
                mw.notify(getErrorMessage(err), {
                    title: TOOL_NAME,
                    type: 'error'
                });
                closeDescriptionDialog(true);
            });
    }

    function renderBaseDialog() {
        $('#vpf-overlay').remove();

        const $overlay = $('<div>')
            .attr('id', 'vpf-overlay')
            .addClass('vpf-overlay');

        const $layer = $('<div>')
            .attr('id', 'vpf-detail-layer')
            .addClass('vpf-detail-layer');

        const $card = $('<div>')
            .addClass('vpf-detail-card')
            .on('click', function (e) {
                e.stopPropagation();
            });

        $layer.append($card);
        $overlay.append($layer);
        $('body').append($overlay);

        bindDialogKeys();

        return $card;
    }

    function renderLoadingDialog() {
        const $card = renderBaseDialog();

        $card.append(
            $('<div>').addClass('vpf-detail-title').text(wddState.pageTitle || '条目名称'),
            $('<div>').addClass('vpf-detail-subtitle').text('正在读取此条目在 Wikidata 的描述。'),
            $('<div>').addClass('vpf-detail-row-label vpf-detail-row-1').text('先前:'),
            $('<div>')
                .addClass('vpf-detail-row-value vpf-detail-row-1')
                .append($('<span>').text('读取中')),
            $('<div>')
                .addClass('vpf-detail-actions')
                .append(
                    $('<button>')
                        .attr('type', 'button')
                        .addClass('vpf-detail-button vpf-detail-close')
                        .text('取消')
                        .on('click', function (e) {
                            e.preventDefault();
                            closeDescriptionDialog();
                        }),
                    $('<button>')
                        .attr('type', 'button')
                        .addClass('vpf-detail-button vpf-detail-add')
                        .text('添加')
                        .prop('disabled', true)
                )
        );
    }

    function renderReadyDialog() {
        const previousDisplay = getPreviousDescriptionForDisplay(wddState.previousDescriptions);
        const $card = $('#vpf-detail-layer .vpf-detail-card');

        if (!$card.length) {
            return;
        }

        $card.empty();

        const $descriptionInput = $('<input>')
            .attr({
                id: 'wdd-description-input',
                type: 'text',
                autocomplete: 'off',
                spellcheck: 'false'
            })
            .addClass('wdd-detail-input')
            .val(previousDisplay === '无' ? '' : previousDisplay);

        const $summaryInput = $('<input>')
            .attr({
                id: 'wdd-summary-input',
                type: 'text',
                autocomplete: 'off',
                spellcheck: 'false'
            })
            .addClass('wdd-detail-input')
            .val(buildEditSummary(wddState.qid));

        $card.append(
            $('<div>').addClass('vpf-detail-title').text(wddState.pageTitle || '条目名称'),
            $('<div>').addClass('vpf-detail-subtitle').text(wddState.qid ? '你可以在此快速修改条目在 Wikidata 的描述。' : '此页面尚未关联 Wikidata 项目,将新建项目并写入描述。'),

            $('<div>').addClass('vpf-detail-row-label vpf-detail-row-1').text('先前:'),
            $('<div>')
                .addClass('vpf-detail-row-value vpf-detail-row-1')
                .attr('title', previousDisplay)
                .append($('<span>').text(previousDisplay)),

            $('<label>')
                .addClass('vpf-detail-row-label vpf-detail-row-2')
                .attr('for', 'wdd-description-input')
                .text('描述:'),
            $('<div>')
                .addClass('vpf-detail-row-value vpf-detail-row-2')
                .append($descriptionInput),

            $('<label>')
                .addClass('vpf-detail-row-label vpf-detail-row-3')
                .attr('for', 'wdd-summary-input')
                .text('修改日志:'),
            $('<div>')
                .addClass('vpf-detail-row-value vpf-detail-row-3')
                .append($summaryInput),

            $('<div>')
                .addClass('vpf-detail-actions')
                .append(
                    $('<button>')
                        .attr('type', 'button')
                        .addClass('vpf-detail-button wdd-detail-undo')
                        .text('撤销')
                        .on('click', function (e) {
                            e.preventDefault();
                            undoSubmission();
                        }),
                    $('<button>')
                        .attr('type', 'button')
                        .addClass('vpf-detail-button vpf-detail-close')
                        .text('取消')
                        .on('click', function (e) {
                            e.preventDefault();
                            closeDescriptionDialog();
                        }),
                    $('<button>')
                        .attr('type', 'button')
                        .addClass('vpf-detail-button vpf-detail-add')
                        .text('添加')
                        .on('click', function (e) {
                            e.preventDefault();
                            submitDescriptionChange();
                        })
                )
        );

        window.setTimeout(function () {
            $descriptionInput.trigger('focus').select();
        }, 40);
    }

    function closeDescriptionDialog(immediate) {
        const $overlay = $('#vpf-overlay');
        const $layer = $('#vpf-detail-layer');

        if (!$overlay.length) {
            return;
        }

        unbindDialogKeys();

        if (immediate) {
            $overlay.remove();
            return;
        }

        if ($overlay.hasClass('vpf-closing')) {
            return;
        }

        $overlay.addClass('vpf-closing');
        $layer.addClass('vpf-detail-closing');

        window.setTimeout(function () {
            $overlay.remove();
        }, 190);
    }

    function fetchCurrentPageEntity() {
        const localApi = new mw.Api();
        const pageName = mw.config.get('wgPageName');

        if (!pageName) {
            return $.Deferred().reject(new Error('无法识别当前页面。')).promise();
        }

        return localApi.get({
            action: 'query',
            prop: 'pageprops',
            ppprop: 'wikibase_item',
            titles: pageName,
            formatversion: 2,
            format: 'json'
        }).then(function (data) {
            const page = data && data.query && data.query.pages && data.query.pages[0];
            const qid = page && page.pageprops && page.pageprops.wikibase_item;

            if (!qid) {
                return fetchWikidataEntityBySitelink(getCurrentWikiSiteId(), getCurrentPageTitleForWikidata())
                    .then(function (entityData) {
                        return entityData;
                    }, function () {
                        return {
                            qid: null,
                            descriptions: {},
                            lastrevid: null,
                            missingItem: true
                        };
                    });
            }

            return fetchWikidataEntity(qid);
        });
    }

    function fetchWikidataEntity(qid) {
        return mw.loader.using('mediawiki.ForeignApi').then(function () {
            const wikidataApi = new mw.ForeignApi(WIKIDATA_API);

            return wikidataApi.get({
                action: 'wbgetentities',
                ids: qid,
                props: 'descriptions|info',
                languages: 'zh|zh-hans|zh-hant',
                format: 'json'
            });
        }).then(function (data) {
            const entity = data && data.entities && data.entities[qid];

            if (!entity || entity.missing) {
                return $.Deferred().reject(new Error('无法读取 Wikidata 项目:' + qid)).promise();
            }

            return {
                qid: qid,
                descriptions: cloneDescriptions(entity.descriptions || {}),
                lastrevid: entity.lastrevid || null
            };
        });
    }

    function fetchWikidataEntityBySitelink(siteId, pageTitle) {
        return mw.loader.using('mediawiki.ForeignApi').then(function () {
            const wikidataApi = new mw.ForeignApi(WIKIDATA_API);

            return wikidataApi.get({
                action: 'wbgetentities',
                sites: siteId,
                titles: pageTitle,
                props: 'descriptions|info',
                languages: 'zh|zh-hans|zh-hant',
                format: 'json'
            });
        }).then(function (data) {
            const entities = data && data.entities ? data.entities : {};
            const ids = Object.keys(entities).filter(function (id) {
                return id !== '-1' && entities[id] && !entities[id].missing;
            });

            if (!ids.length) {
                return $.Deferred().reject(new Error('当前页面没有关联的 Wikidata 项目。')).promise();
            }

            const qid = ids[0];
            const entity = entities[qid];

            return {
                qid: qid,
                descriptions: cloneDescriptions(entity.descriptions || {}),
                lastrevid: entity.lastrevid || null
            };
        });
    }

    function cloneDescriptions(descriptions) {
        const result = {};

        ['zh', 'zh-hans', 'zh-hant'].forEach(function (lang) {
            if (descriptions[lang] && typeof descriptions[lang].value === 'string') {
                result[lang] = descriptions[lang].value;
            }
        });

        return result;
    }

    function getPreviousDescriptionForDisplay(descriptions) {
        const variant = getUserChineseVariant();
        let value = '';

        if (variant === 'zh-hant') {
            value = descriptions['zh-hant'] || descriptions.zh || descriptions['zh-hans'] || '';
        } else if (variant === 'zh-hans') {
            value = descriptions.zh || descriptions['zh-hans'] || descriptions['zh-hant'] || '';
        } else {
            value = descriptions.zh || descriptions['zh-hans'] || descriptions['zh-hant'] || '';
        }

        return value || '无';
    }

    function getUserChineseVariant() {
        const variant =
            mw.config.get('wgUserVariant') ||
            mw.user.options.get('variant') ||
            mw.config.get('wgUserLanguage') ||
            mw.config.get('wgContentLanguage') ||
            'zh';

        const normalized = String(variant).toLowerCase().replace('_', '-');

        if (/^zh-(hant|tw|hk|mo)$/.test(normalized)) {
            return 'zh-hant';
        }

        if (/^zh-(hans|cn|sg|my)$/.test(normalized)) {
            return 'zh-hans';
        }

        return 'zh';
    }

    function submitDescriptionChange() {
        if (wddState.submitting) {
            return;
        }

        const rawDescription = ($('#wdd-description-input').val() || '').trim();
        const rawSummary = ($('#wdd-summary-input').val() || '').trim();

        if (!rawDescription) {
            mw.notify('请先填写描述。', {
                title: TOOL_NAME,
                type: 'warn'
            });
            $('#wdd-description-input').trigger('focus');
            return;
        }

        wddState.submitting = true;
        $('.vpf-detail-add').prop('disabled', true).text('提交中');
        $('#wdd-description-input, #wdd-summary-input').prop('disabled', true);

        $.when(
            convertChineseVariant(rawDescription, 'zh-hans'),
            convertChineseVariant(rawDescription, 'zh-hant')
        ).then(function (simplifiedText, traditionalText) {
            return saveWikidataDescriptions({
                zh: normalizeConvertedText(simplifiedText),
                'zh-hant': normalizeConvertedText(traditionalText)
            }, rawSummary || buildEditSummary(wddState.qid));
        }).done(function () {
            $('.vpf-detail-add')
                .prop('disabled', true)
                .text('完成');

            $('.vpf-detail-close')
                .addClass('wdd-hidden');

            $('.wdd-detail-undo')
                .addClass('wdd-detail-undo-visible')
                .prop('disabled', false);

            mw.notify(wddState.createdNewItem ? '已新建 Wikidata 项目并写入描述。' : '已更新 Wikidata 描述。', {
                title: TOOL_NAME,
                type: 'success'
            });
        }).fail(function (err) {
            console.error(err);
            mw.notify(getErrorMessage(err), {
                title: TOOL_NAME,
                type: 'error'
            });

            $('.vpf-detail-add').prop('disabled', false).text('添加');
            $('#wdd-description-input, #wdd-summary-input').prop('disabled', false);
        }).always(function () {
            wddState.submitting = false;
        });
    }

    function undoSubmission() {
        if (wddState.undoing || !wddState.previousDescriptions) {
            return;
        }

        wddState.undoing = true;
        $('.wdd-detail-undo').prop('disabled', true).text('撤销中');

        saveWikidataDescriptions({
            zh: wddState.previousDescriptions.zh || '',
            'zh-hant': wddState.previousDescriptions['zh-hant'] || ''
        }, wddState.createdNewItem ? '撤销本次新建项目的中文与繁体中文描述' : '撤销先前的 Wikidata 描述修改,恢复中文与繁体中文描述')
            .done(function () {
                $('#wdd-description-input').val(
                    getPreviousDescriptionForDisplay(wddState.previousDescriptions) === '无'
                        ? ''
                        : getPreviousDescriptionForDisplay(wddState.previousDescriptions)
                );

                $('.vpf-detail-add')
                    .prop('disabled', true)
                    .text('已撤销');

                $('.wdd-detail-undo')
                    .prop('disabled', true)
                    .text('已撤销');

                mw.notify('已撤销本次 Wikidata 描述修改。', {
                    title: TOOL_NAME,
                    type: 'success'
                });
            })
            .fail(function (err) {
                console.error(err);
                mw.notify(getErrorMessage(err), {
                    title: TOOL_NAME,
                    type: 'error'
                });

                $('.wdd-detail-undo').prop('disabled', false).text('撤销');
            })
            .always(function () {
                wddState.undoing = false;
            });
    }

    function saveWikidataDescriptions(descriptions, summary) {
        if (wddState.qid) {
            return updateWikidataEntityTerms(wddState.qid, descriptions, summary, false);
        }

        return createWikidataItemForCurrentPage(summary)
            .then(function (qid) {
                wddState.qid = qid;
                wddState.createdNewItem = true;
                return updateWikidataEntityTerms(qid, descriptions, summary, true);
            });
    }

    function createWikidataItemForCurrentPage(summary) {
        return mw.loader.using('mediawiki.ForeignApi').then(function () {
            const wikidataApi = new mw.ForeignApi(WIKIDATA_API);
            const siteId = getCurrentWikiSiteId();
            const pageTitle = getCurrentPageTitleForWikidata();

            if (!siteId || !pageTitle) {
                return $.Deferred().reject(new Error('无法取得当前页面的站点或标题,不能新建 Wikidata 项目。')).promise();
            }

            return wikidataApi.postWithToken('csrf', {
                action: 'wbsetsitelink',
                new: 'item',
                linksite: siteId,
                linktitle: pageTitle,
                summary: summary || '新建 Wikidata 项目并连接当前页面',
                format: 'json'
            }).then(function (response) {
                const qid = extractQidFromEntityResponse(response);

                if (qid) {
                    if (response && response.entity && response.entity.lastrevid) {
                        wddState.currentRevisionId = response.entity.lastrevid;
                    }

                    return qid;
                }

                return fetchWikidataEntityBySitelink(siteId, pageTitle).then(function (entityData) {
                    wddState.currentRevisionId = entityData.lastrevid || null;
                    return entityData.qid;
                });
            });
        });
    }

    function updateWikidataEntityTerms(qid, descriptions, summary, includeLabel) {
        return mw.loader.using('mediawiki.ForeignApi').then(function () {
            const wikidataApi = new mw.ForeignApi(WIKIDATA_API);
            const data = {
                descriptions: {
                    zh: {
                        language: 'zh',
                        value: descriptions.zh || ''
                    },
                    'zh-hant': {
                        language: 'zh-hant',
                        value: descriptions['zh-hant'] || ''
                    }
                }
            };

            if (includeLabel) {
                data.labels = {
                    zh: {
                        language: 'zh',
                        value: wddState.pageTitle || getCurrentPageTitleForWikidata()
                    }
                };
            }

            const params = {
                action: 'wbeditentity',
                id: qid,
                data: JSON.stringify(data),
                summary: summary,
                bot: false,
                format: 'json'
            };

            if (wddState.currentRevisionId) {
                params.baserevid = wddState.currentRevisionId;
            }

            return wikidataApi.postWithToken('csrf', params);
        }).then(function (response) {
            if (response && response.entity && response.entity.lastrevid) {
                wddState.currentRevisionId = response.entity.lastrevid;
            }

            return descriptions;
        });
    }

    function extractQidFromEntityResponse(response) {
        if (!response) {
            return '';
        }

        if (response.entity && response.entity.id) {
            return response.entity.id;
        }

        if (response.entities) {
            const ids = Object.keys(response.entities).filter(function (id) {
                return /^Q\d+$/.test(id);
            });

            if (ids.length) {
                return ids[0];
            }
        }

        if (response.item && response.item.id) {
            return response.item.id;
        }

        if (response.id && /^Q\d+$/.test(response.id)) {
            return response.id;
        }

        return '';
    }

    function getCurrentWikiSiteId() {
        return mw.config.get('wgDBname') || 'zhwiki';
    }

    function getCurrentPageTitleForWikidata() {
        const pageName = mw.config.get('wgPageName') || '';

        return pageName.replace(/_/g, ' ');
    }

    function convertChineseVariant(text, targetVariant) {
        const localApi = new mw.Api();

        return localApi.get({
            action: 'parse',
            text: String(text),
            contentmodel: 'wikitext',
            prop: 'text',
            variant: targetVariant,
            disablelimitreport: true,
            formatversion: 2,
            format: 'json'
        }).then(function (data) {
            const html = data && data.parse && data.parse.text;

            if (!html) {
                return fallbackChineseVariantConversion(text, targetVariant);
            }

            return extractPlainTextFromHtml(html);
        }, function () {
            return fallbackChineseVariantConversion(text, targetVariant);
        });
    }

    function extractPlainTextFromHtml(html) {
        const div = document.createElement('div');
        div.innerHTML = html;

        return (div.textContent || '').replace(/\s+/g, ' ').trim();
    }

    function normalizeConvertedText(text) {
        return String(text || '').replace(/\s+/g, ' ').trim();
    }

    function fallbackChineseVariantConversion(text, targetVariant) {
        const simplified = {
            '們': '们', '個': '个', '國': '国', '與': '与', '為': '为', '於': '于', '這': '这', '後': '后',
            '會': '会', '時': '时', '來': '来', '對': '对', '體': '体', '關': '关', '開': '开', '發': '发',
            '學': '学', '長': '长', '廣': '广', '東': '东', '門': '门', '風': '风', '電': '电', '車': '车',
            '馬': '马', '島': '岛', '區': '区', '縣': '县', '鄉': '乡', '鎮': '镇', '臺': '台', '灣': '湾',
            '歷': '历', '義': '义', '華': '华', '設': '设', '劃': '划', '稱': '称', '屬': '属', '類': '类',
            '別': '别', '條': '条', '項': '项', '頁': '页', '編': '编', '輯': '辑', '寫': '写', '無': '无',
            '雙': '双', '邊': '边', '織': '织', '構': '构', '員': '员', '團': '团', '線': '线', '證': '证',
            '戰': '战', '將': '将'
        };

        const traditional = {};
        Object.keys(simplified).forEach(function (trad) {
            traditional[simplified[trad]] = trad;
        });

        const table = targetVariant === 'zh-hant' ? traditional : simplified;

        return String(text).replace(/./g, function (char) {
            return table[char] || char;
        });
    }

    function buildEditSummary(qid) {
        if (qid) {
            return '更新 Wikidata 描述:补充或更正用于区分同名项目的简短说明' +
                '(' + qid + ',zh、zh-hant)';
        }

        return '新建 Wikidata 项目并添加中文与繁体中文描述';
    }

    function bindDialogKeys() {
        $(document)
            .off('keydown.wddDialog')
            .on('keydown.wddDialog', function (e) {
                if (!$('#vpf-overlay').length) {
                    return;
                }

                if (e.key === 'Escape' && !wddState.submitting && !wddState.undoing && !$('.vpf-detail-close').hasClass('wdd-hidden')) {
                    e.preventDefault();
                    closeDescriptionDialog();
                }

                if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
                    e.preventDefault();
                    submitDescriptionChange();
                }
            });
    }

    function unbindDialogKeys() {
        $(document).off('keydown.wddDialog');
    }

    function getErrorMessage(err) {
        if (!err) {
            return '操作失败。';
        }

        if (typeof err === 'string') {
            return err;
        }

        if (err.error && err.error.info) {
            return err.error.info;
        }

        if (err.exception) {
            return err.exception;
        }

        if (err.message) {
            return err.message;
        }

        if (Array.isArray(err) && err.length) {
            return getErrorMessage(err[0]);
        }

        return '操作失败,请查看控制台。';
    }
})();
// </nowiki>