مدیاویکی:Gadget-asbox.js

نکته: برای دیدن تغییرات، ممکن است نیاز باشد که حافظهٔ نهانی مرورگر خود را پس از انتشار پاک‌سازی کنید. گوگل کروم، فایرفاکس، مایکروسافت اج و سافاری: کلید Shift را نگه دارید و روی دکمهٔ Reload در نوار ابزار مرورگر کلیک کنید. برای آگاهی از جزئیات و نحوهٔ پاک‌سازی حافظهٔ نهانی سایر مرورگرها، صفحهٔ ویکی‌پدیا:میانگیر مرورگرتان را خالی کنید را ببینید.

/**
 * 
 * Originally written by [[:en:User:SD0001]] at [[:en:User:SD0001/StubSorter.js]]
 * این نسخهٔ فارسی تنها برای سازگاری با ویکی‌پدیای فارسی بومی‌سازی شده و کدهای آن منتسب به نویسندهٔ ابزار در ویکی‌پدیای انگلیسی است
 * Ajax-based stub tag manager
 * 
 */

// <nowiki>
// jshint maxerr: 999
$.when(
    $.ready,
    mw.loader.using(['mediawiki.util', 'mediawiki.api', 'mediawiki.Title', 'jquery.chosen'])
).then(function() {

    var API = new mw.Api({
        ajax: {
            headers: {
                'Api-User-Agent': '[[w:en:User:SD0001/StubSorter.js]]'
            }
        }
    });

    var callback = function(e) {
        e.preventDefault();

        // if already present, don't duplicate
        if ($('#stub-sorter-wrapper').length !== 0) {
            return;
        }

        $('#mw-content-text').before(
            $('<div>').attr('id', 'stub-sorter-wrapper').css({
                'max-height': 'max-content',
                'background-color': '#c0ffec',
                'margin-bottom': '10px'
            }).append(
                $('<select>')
                .attr('id', 'stub-sorter-select')
                .attr('multiple', 'true')
                .change(handlePreview),

                $('<div>').attr('id', 'stub-sorter-previewbox').css({
                    'background-color': '#cfd8eb' // '#98b685'
                    // 'border-bottom': 'solid 0.5px #aaaaaa'
                })
            )
        );

        var $select = $('#stub-sorter-select');

        var selectExistingStubTags = function($html) {
            $html.find('.stub .hlist .nv-view a').each(function(_, e) {
                var template = e.title.slice('الگو:'.length);
                $select.append(
                    $('<option>').text(template).val(template).attr('selected', 'true')
                );
            });
        };

        if (mw.config.get('wgCurRevisionId') === mw.config.get('wgRevisionId')) {
            // Viewing the current version of the page, no need for api call to get the page html
            selectExistingStubTags($('.mw-parser-output'));
        } else {
            // In edit/history/diff/oldrevision mode, get the page html by api call
            var pageNameToEncode = mw.config.get('wgPageName');
            API.parse(new mw.Title(encodeURIComponent(pageNameToEncode))).then(function(html) {
                selectExistingStubTags($(html));
                $select.trigger('chosen:updated');
                $select.trigger('click');
                $input.focus();
            });
        }

        $select.chosen({
            search_contains: true,
            placeholder_text_multiple: 'برای افزودن یک برچسب خرد، شروع به نوشتن کنید...',
            width: '100%',

            // somehow beacuse of the hacks below, the no_results_text shows up
            // when the search results are loading, and not when there are no results
            no_results_text: 'بارگذاری نتایج برای'
        });

        var $input = $('#stub_sorter_select_chosen input');

        var menuFrozen = false;
        var searchBy = getPref('searchBy', 'prefix');

        $('#stub_sorter_select_chosen .chosen-choices').after(

            $('<div>').append(

                // Freeze button
                $('<span>').append(
                    $('<a>').text('منو باز بماند ').click(function() {
                        menuFrozen = !menuFrozen;
                        if (menuFrozen) {
                            $(this).text('منو بسته شود ');
                            $(this).parent().css('font-weight', 'bold');
                        } else {
                            $(this).text('منو باز بماند ');
                            $(this).parent().css('font-weight', 'normal');
                        }
                        $input[0].focus();
                        $input.trigger('keyup');
                    }).css({
                        'padding-right': '5px',
                        'padding-left': '100px'
                    })
                ),

                // Search mode select
                $('<select>').append(
                    $('<option>').text('اول پیشوندهای منطبق را فهرست کن').val('prefix'),
                    $('<option>').text('اول موارد منطبق در عنوان‌ها را فهرست کن').val('intitle'),
                    $('<option>').text('استفاده از جستجوی دقیق انطباق نویسه‌ها').val('regex')
                ).change(function(e) {
                    searchBy = e.target.value;
                    $input.trigger('keyup');
                }),

                // help button after the search mode select
                $('<small>').append(
                    ' (',
                    $('<a>', {
                        text: 'راهنما',
                        href: mw.util.getUrl('راهنما:ابزار/خردیاب'),
                        target: '_blank'
                    }),
                    ')'
                )
            ).css({
                'border-bottom': 'solid 0.5px #aaaaaa',
                'border-left': 'solid 0.5px #aaaaaa',
                'border-right': 'solid 0.5px #aaaaaa'
            })

        );

        // Save button
        $('<button>')
            .text('ذخیره').css({
                'float': 'left'
            })
            .attr('id', 'stub-sorter-save')
            .attr('accesskey', 's')
            .click(handleSave)
            .insertAfter($('#stub_sorter_select_chosen .chosen-choices'));

        // hide selected items in dropdown
        mw.util.addCSS(
            '#stub_sorter_select_chosen .chosen-results .result-selected { display: none; }'
        );

        // Focus on the search box as soon as the the sorter menu loads
        // Add placeholder, because chosen's native placeholder doesn't work with a changing menu.
        // Reset the search box width to accomodate the placeholder text
        // Keep resetting whenever the input goes out of focus
        $input
            .focus()
            .attr('placeholder', 'برای افزودن یک برچسب خرد، شروع به نوشتن کنید...')
            .css('width', '200px')
            .blur(function() {
                $(this).css('width', '100%');
            });

        // also reset it when an option is selected by clicking on it
        // or when clicking on the search box after the $input has become narrow (despite our best efforts...)
        $('.chosen-container').click(function() {
            $input.css('width', '100%');
        });

        // Adapted from [[User:Enterprisey/afch-master.js/submissions.js]]'s category selection menu:
        // Offer dynamic suggestions!
        // Since jquery.chosen doesn't natively support dynamic results,
        // we sneakily inject some dynamic suggestions instead.
        // Consider upgrading to select2 or OOUI to avoid these hacks
        $input.keyup(function(e) {
            var searchStr = $input.val();

            // The worst hack. Because Chosen keeps messing with the
            // width of the text box, keep on resetting it to 100%
            $input.css('width', '100%');
            $input.parent().css('width', '100%');

            // Ignore arrow keys and home/end keys to allow users to navigate through the suggestions or through the search query
            // and don't show results when an empty string is provided
            if ((e.which >= 35 && e.which <= 40) ||
                (menuFrozen && e.which !== undefined) ||
                !searchStr) {
                return;
            }

            // true when fake keyup is produced by the Freeze button
            // in this case, api limit has to be raised to 500
            var extended = e.which === undefined;

            $.when(
                searchBy !== 'regex' ? getStubSearchResults('prefix', searchStr, extended) : undefined,
                searchBy !== 'regex' ? getStubSearchResults('intitle', searchStr, extended) : undefined,
                searchBy === 'regex' ? getStubSearchResults('regex', searchStr, extended) : undefined
            ).then(function(stubsPrefix, stubsIntitle, stubsRegex) {

                var stubs;
                switch (searchBy) {
                    case 'prefix':
                        stubs = uniqElements(stubsPrefix, stubsIntitle);
                        break;
                    case 'intitle':
                        stubs = uniqElements(stubsIntitle, stubsPrefix);
                        break;
                    case 'regex':
                        stubs = stubsRegex;
                        break;
                }

                // Reset the text box width again
                $input.css('width', '100%');
                $input.parent().css('width', '100%');

                // If the input has changed since we started searching,
                // don't show outdated results
                if ($input.val() !== searchStr) {
                    return;
                }

                // Clear existing suggestions
                $select.children().not(':selected').remove();

                // Now, add the new suggestions
                stubs.forEach(function(stub) {

                    // do not add if already selected
                    if ($select.val().indexOf(stub) !== -1) {
                        return;
                    }
                    $select.append(
                        $('<option>').text(stub).val(stub)
                    );
                });

                // We've changed the <select>, now tell Chosen to
                // rebuild the visible list
                $select.trigger('liszt:updated');
                $select.trigger('chosen:updated');
                $input.val(searchStr);
                $input.css('width', '100%');
                $input.parent().css('width', '100%');

            }).catch(function(e) {
                if ($input.val() !== searchStr) {
                    return;
                }
                $select.children().not(':selected').remove();
                $select.append(
                    $('<option>')
                    .text('خطا در دریافت نتایج: ' + e)
                    .attr('disabled', 'true')
                );
                $select.trigger('liszt:updated');
                $select.trigger('chosen:updated');
                $input.val(searchStr);
                $input.css('width', '100%');
                $input.parent().css('width', '100%');
            });

        });

    };

    var getStubSearchResults = function(searchType, searchStr, extended) {
        var query = {
            'action': 'query',
            'list': 'search',
            'srsearch': 'incategory:"الگو:مقاله‌های_خرد" ',
            'srnamespace': '10',
            'srlimit': extended ? '500' : '100',
            'srqiprofile': 'classic',
            'srprop': '',
            'srsort': 'relevance'
        };
        switch (searchType) {
            case 'prefix':
                query.srsearch += 'prefix:"الگو:' + searchStr + '"';
                break;
            case 'intitle':
                var searchStrWords = searchStr.split(' ').filter(function(e) {
                    return !/^\s*$/.test(e);
                });
                query.srsearch += 'intitle:"' + searchStrWords.join('" intitle:"') + '"';
                break;
            case 'regex':
                query.srsearch += 'intitle:/' + mw.util.escapeRegExp(searchStr) + '/i';
                break;
        }

        return API.get(query).then(function(response) {
            if (response && response.query && response.query.search) {
                return response.query.search.map(function(e) {
                    return e.title.slice(5);
                });
            } else {
                return $.Deferred().reject(JSON.stringify(response));
            }
        }, function(e) {
            return $.Deferred().reject(JSON.stringify(e));
        });
    };

    var handlePreview = function() {

        // Show preview
        var $this = $(this);
        var selectedTags = $this.val();
        if (selectedTags.length) {
            var tagsWikitext = '{{' + selectedTags.join('}}\n{{') + '}}';

            API.parse(tagsWikitext).then(function(parsedhtmldiv) {

                // Do nothing if tag selection has changed since we
                // sent the parse API call, comparing lengths is enough
                if (selectedTags.length !== $this.val().length) {
                    return;
                }
                $('#stub-sorter-previewbox').html(parsedhtmldiv);
            });
        } else {
            $('#stub-sorter-previewbox').empty();
        }
        // $input.css('width', '100%');  // doesn't work
    };


    var handleSave = function submit() {
        $('#stub-sorter-error').remove();
        var $status = $('<div>').text('واکشی صفحه...')
            .attr('id', 'stub-sorter-status')
            .css({
                'float': 'left'
            });
        $(this).replaceWith($status);
        API.edit(mw.config.get('wgPageName'), function(revision) {
            $status.text('ذخیرهٔ صفحه...');
            var pageText = revision.content;

            var tagsBefore = (pageText.match(/\{\{[^{]*(?:خرد|ناقص)(?:\|.*?)?\}\}/g) || []).map(function(e) {
                // capitalise first char after {{
                return e[0] + e[1] + e[2].toUpperCase() + e.slice(3);
            });
            var tagsAfter = $('#stub-sorter-select').val().map(function(e) {
                return '{{' + e + '}}';
            });

            // Automatically remove {{Stub}} if accidentally left behind
            if (tagsAfter.length > 1) {
                var idx = tagsAfter.indexOf('{{خرد}}');
                if (idx !== -1) {
                    tagsAfter.splice(idx, 1);
                }
            }

            // remove all stub tags
            pageText = pageText.replace(/\{\{[^{]*(?:خرد|ناقص)(?:\|.*?)?\}\}\s*/g, '').trim();

            // add selected stub tags
            pageText += '\n\n' + tagsAfter.join('\n'); // per [[MOS:LAYOUT]]

            // For producing edit summary
            var summary = '';

            var tagsAdded = tagsAfter.filter(function(e) {
                return tagsBefore.indexOf(e) === -1;
            });
            var tagsRemoved = tagsBefore.filter(function(e) {
                return tagsAfter.indexOf(e) === -1;
            });

            tagsRemoved.forEach(function(e) {
                summary += '–' + e + '، ';
            });
            tagsAdded.forEach(function(e) {
                summary += '+' + e + '، ';
            });
            summary = summary.slice(0, -2); // remove the final ', '

            return {
                text: pageText,
                summary: summary + ' با کمک [[راهنما:ابزار/خردیاب|خردیاب]]',
                nocreate: 1,
                minor: getPref('minor', true),
                watchlist: getPref('watchlist', 'nochange')
            };
        }).then(function() {
            $status.text('انجام شد. بارگذاری مجدد صفحه...');
            setTimeout(function() {
                window.location.href = mw.util.getUrl(mw.config.get('wgPageName'));
            }, 500);
        }).fail(function(e) {
            $status.text('ذخیرهٔ ناموفق. لطفاً دوباره تلاش کنید.')
                .attr('id', 'stub-sorter-error')
                .css({
                    'color': 'red',
                    'font-weight': 'bold',
                    'padding-left': '5px'
                });
            console.error(e); // eslint-disable-line no-console
            setTimeout(function() {
                $status.before($('#stub-sorter-save'));
                $('#stub-sorter-save').click(handleSave);
            }, 500);
        });
    };

    // utility function to get unique elements from 2 arrays
    var uniqElements = function(arr1, arr2) {
        var obj = {};
        var i;
        for (i = 0; i < arr1.length; i++) {
            obj[arr1[i]] = 0;
        }
        for (i = 0; i < arr2.length; i++) {
            obj[arr2[i]] = 0;
        }
        return Object.keys(obj);
    };

    // function to obtain a preference option from common.js
    var getPref = function(name, defaultVal) {
        if (window['stubSorter_' + name] === undefined) {
            return defaultVal;
        } else {
            return window['stubSorter_' + name];
        }
    };

    /**
     ********************* SET UP *********************
     */

    // auto start the script when navigating to an article from CAT:STUBS
    if (mw.config.get('wgPageName') === 'رده:مقاله‌های خرد') {
        $('#mw-pages li a').each(function(_, e) {
            e.href += '?startstubsorter=y';
        });
    }
    // show only on existing articles, and my sandbox (for testing)
    if (mw.config.get('wgNamespaceNumber') === 0 && mw.config.get('wgCurRevisionId') !== 0 ) {
        mw.util.addPortletLink(getPref('portlet', 'p-cactions'), '#', 'خرد',
            'ca-stub', 'افزودن یا حذف الگوهای خرد').addEventListener('click', callback);
    }
    if (mw.util.getParamValue('startstubsorter')) {
        setTimeout(function() {
            $('#ca-stub').click();
        }, 1000);
    }
});

// </nowiki>