MediaWiki:Gadget-wiadomosci-o-blokadzie.js

Z Wikipedii, wolnej encyklopedii

Uwaga: aby zobaczyć zmiany po opublikowaniu, może zajść potrzeba wyczyszczenia pamięci podręcznej przeglądarki.

  • Firefox / Safari: Przytrzymaj Shift podczas klikania Odśwież bieżącą stronę, lub naciśnij klawisze Ctrl+F5, lub Ctrl+R (⌘-R na komputerze Mac)
  • Google Chrome: Naciśnij Ctrl-Shift-R (⌘-Shift-R na komputerze Mac)
  • Internet Explorer / Edge: Przytrzymaj Ctrl, jednocześnie klikając Odśwież, lub naciśnij klawisze Ctrl+F5
  • Opera: Naciśnij klawisze Ctrl+F5.
//@ts-check
/**
 * A gadget that allows to send a message to the blocked user
 * directly from the Special:Block page. It provides the administrator
 * with some common messages to choose from.
 * 
 * @author [[w:pl:User:Msz2001]]
 * <nowiki>
 */
$(function(){
    // Load data from JSON files (editable by ordinary admins)
    var jsonData = require('./wiadomosci-o-blokadzie.json');
    var MSG = jsonData.messages;
    var blockMessages = jsonData.presets;
    
    /*
    Message parameter documentation:
    bm-section-intro: $1 = blocked user name
    bm-form-title: $1 = blocked user name
    bm-subject-default: $1 = today date
    */

    /**
     * Creates a widget with a list of preset block messages
     * @class
     * @param {string} title The title of the widget
     * @param {PresetMessage[]} messages The list of messages to choose from
     */
    function PresetMessages(title, messages){
        this.title = title;
        this.messages = messages;
        this.listeners = [];

        /** The root DOM element of the messages selector */
        this.$element = $('<div>')
            .addClass('bm-preset-messages')
            .append($('<h3>').text(this.title));

        this.$list = $('<ul>').appendTo(this.$element);
        this.fillList();

        // Fire a hook so that user can add their own messages
        mw.hook('userjs.block-messenger.presets').fire(this);

        /*
        Example hook listener:
        mw.hook('userjs.block-messenger.presets').add((m) => m.addMessage(...));
        */
    }

    /**
     * Sets the messages to display in the widget
     * @param {PresetMessage[]} messages The messages to display in the widget
     */
    PresetMessages.prototype.setMessages = function(messages){
        this.messages = messages;
        this.fillList();
    };

    /**
     * Adds a message or multiple messages to the widget, without removing the existing ones
     * @param {PresetMessage | PresetMessage[]} message A message or messages to be added to the widget
     */
    PresetMessages.prototype.addMessage = function(message){
        if(Array.isArray(message)){
            this.messages.push.apply(this.messages, message);
        }else{
            this.messages.push(message);
        }
        this.fillList();
    };

    /**
     * Fills a list with the provided messages.
     */
    PresetMessages.prototype.fillList = function(){
        var tm = this;
        this.$list.empty();
        this.messages.forEach(function(msg){
            var $li = $('<li>').appendTo(tm.$list);
            var $button = $('<button>').attr('type', 'button').appendTo($li);
    
            var fullText = msg.text;
            $button.attr('title', fullText);
            
            var $title = $('<span>').addClass('bm-title').text(msg.title);
            $button.append($title);
    
            var displayText = fullText;
            if(displayText.length > 150){
                displayText = displayText.substring(0, 150) + '…';
            }
            var $text = $('<span>').addClass('bm-text').text(displayText);
            $button.append($text);
    
            $button.on('click', function() {
                for(var j = 0; j < tm.listeners.length; j++){
                    tm.listeners[j](fullText);
                }
            });
        });
    };

    /**
     * Adds a listener to the widget
     * @param {(text: string) => void} listener A function to be fired when a message is selected
     */
    PresetMessages.prototype.addOnSelect = function(listener){
        this.listeners.push(listener);
    };

    /**
     * Creates a widget with a preview of the message
     * @class
     * @param {string} targetPage The page to which the message will be eventually posted
     * @param {string} previewLabel The label of the preview box
     * @param {string} emptyMessage The message to be displayed when the preview is empty
     */
    function PreviewBox(targetPage, previewLabel, emptyMessage){
        this.targetPage = targetPage;
        this.emptyMessage = emptyMessage;

        this.api = new mw.Api({
            parameters: {
                format: 'json',
                formatversion: 2
            }
        });

        this.$element = $('<div>')
            .addClass('bm-preview-box bm-no-preview')
            .attr('data-label', previewLabel);
        this.update('', '');
    }

    /**
     * Sets the label of the preview box
     * @param {string} label New label
     */
    PreviewBox.prototype.setLabel = function(label){
        this.$element.attr('data-label', label);
    };

    /**
     * Updates the preview with the provided wikitext
     * @param {string} sectionTitle The title of the new section
     * @param {string} wikitext The wikitext to be previewed
     */
    PreviewBox.prototype.update = function(sectionTitle, wikitext){
        if(sectionTitle === this.sectionTitle && wikitext === this.wikitext){
            return;
        }

        this.sectionTitle = sectionTitle;
        this.wikitext = wikitext;

        this.$element.removeClass('bm-no-preview');
        if(sectionTitle == '' || wikitext == ''){
            this.$element.html('<i class="bm-preview-empty">' + this.emptyMessage + '</i>');
            return;
        }

        var apiParams = {
            action: 'parse',
            prop: 'text',
            title: this.targetPage,
            text: wikitext,
            pst: 1,
            section: 'new',
            sectiontitle: sectionTitle,
            disablelimitreport: 1,
            disableeditsection: 1,
            preview: 1,
            sectionpreview: 1
        }

        var pb = this;
        this.api.get(apiParams).done(function(data){
            pb.$element.html(data.parse.text);
        });
    };

    /**
     * Creates a new instance of the block messenger
     * @class
     * @param {string} blockedUserName The name of the blocked user
     */
    function BlockMessenger(blockedUserName) {
        this.blockedUserName = blockedUserName;
        mw.messages.set(MSG);

        this.api = new mw.Api({
            parameters: {
                format: 'json',
                formatversion: 2,
                errorformat: 'html',
                errorsuselocal: true
            }
        });

        //! Initialize the UI below

        /** The root DOM element of the BlockMessenger */
        this.$element = $('<div>')
            .addClass('noprint bm-wrapper');

        // Start the section with block messenger
        var $intro = $('<p>')
            .appendTo(this.$element)
            .html(mw.message('bm-section-intro', this.blockedUserName).parse());

        var $wrapperFormAndPreset = $('<div>')
            .addClass('bm-form-wrapper')
            .appendTo(this.$element);

        var $form = $('<div>')
            .addClass('bm-form')
            .appendTo($wrapperFormAndPreset);

        // Modify the links in the intro: lead to a new tab
        var newTabSuffix = mw.message('bm-newtab-suffix').parse();
        $intro.find('a')
            .attr('target', '_blank')
            .each(function(_, a) { a.title += newTabSuffix; });
        this.makeRedLinks($intro);

        var today = new Date();
        var todayText = today.toLocaleDateString(mw.config.get('wgContentLanguage'));
        var defaultSubject = mw.message('bm-subject-default', todayText).parse();
        var defaultMessage = '';

        // Add the message subject input
        var subjectInput = new OO.ui.TextInputWidget({
            name: 'bm-subject',
            value: defaultSubject
        });
        var subjectField = new OO.ui.FieldLayout(subjectInput, {
            align: 'top',
            label: mw.message('bm-subject-label').parse()
        });
        
        // Add the message input
        var messageInput = new OO.ui.MultilineTextInputWidget({
            rows: 5,
            autosize: true,
            maxRows: 15,
            value: defaultMessage
        });
        var messageField = new OO.ui.FieldLayout(messageInput, {
            align: 'top',
            label: mw.message('bm-message-label').parse(),
            help: mw.message('bm-message-help').parse(),
            helpInline: true
        });

        var fieldset = new OO.ui.FieldsetLayout({
            label: mw.message('bm-form-title', this.blockedUserName).parse()
        });
        fieldset.addItems([
            subjectField,
            messageField
        ]);
        $form.append(fieldset.$element);

        // Create the preview box
        var previewBox = new PreviewBox(
            'User talk:' + this.blockedUserName,
            mw.message('bm-preview-label').parse(),
            mw.message('bm-preview-empty').parse()
        );
        $form.append(previewBox.$element);

        // The "Send" button
        var sendButton = new OO.ui.ButtonWidget({
            label: mw.message('bm-send-button').parse(),
            flags: ['primary', 'progressive']
        });
        var sendField = new OO.ui.FieldLayout(sendButton);
        $form.append(sendField.$element);

        // Status field, to display errors, if any
        var $statusBox = $('<div>')
            .addClass('bm-status')
            .appendTo($form);

        // Add the preset messages panel
        var presetMessages = new PresetMessages(
            mw.message('bm-preset-header').parse(),
            blockMessages
        );
        $wrapperFormAndPreset.append(presetMessages.$element);

        presetMessages.addOnSelect(function(text){
            if(messageInput.isDisabled()) return;
            messageInput.setValue(text);
        });

        // Auto-preview
        var bm = this;
        var displayPreview = OO.ui.throttle(function () {
            var wikitext = bm.transformWikitext(messageInput.getValue());
            previewBox.update(subjectInput.getValue(), wikitext);
        }, 750);

        subjectInput.on('change', displayPreview);
        messageInput.on('change', displayPreview);

        // Prevent the user from closing the window
        this.closeLock = mw.confirmCloseWindow({
            test: function(){
                // Prevent only when the user has typed something
                if(subjectInput.getValue() != defaultSubject) return true;
                if(messageInput.getValue() != defaultMessage) return true;

                // Else, don't prevent
                return false;
            }
        });

        // Send the message
        sendButton.on('click', function(){
            var wikitext = bm.transformWikitext(messageInput.getValue());
            var sectionTitle = subjectInput.getValue();

            var errors = [];
            if(sectionTitle == ''){
                errors.push(mw.message('bm-error-no-subject').parse());
            }
            if(wikitext == ''){
                errors.push(mw.message('bm-error-no-message').parse());
            }

            if(errors.length > 0){
                $statusBox.html(errors.join('<br>'));
                return;
            }

            subjectInput.setDisabled(true);
            messageInput.setDisabled(true);
            sendButton.setDisabled(true);
            messageInput.pushPending();
            $statusBox.html('');

            bm.sendMessage('User talk:' + bm.blockedUserName, sectionTitle, wikitext)
                .done(function(){
                    $statusBox.addClass('bm-success');
                    $statusBox.html(mw.message('bm-success').parse());
                    messageInput.popPending();
                    bm.releaseCloseLock();

                    // Hide unnecessary elements
                    $intro.remove();
                    fieldset.$element.remove();
                    presetMessages.$element.remove();
                    sendField.$element.remove();

                    previewBox.update(sectionTitle, wikitext);
                    previewBox.setLabel(mw.message('bm-preview-label-after').parse());
                }).fail(function(reason){
                    $statusBox.html(reason);
                    subjectInput.setDisabled(false);
                    messageInput.setDisabled(false);
                    sendButton.setDisabled(false);
                    messageInput.popPending();
                });
        });
    }

    /**
     * Transforms the wikitext, adding a missing signature if needed
     * @param {string} wikitext The wikitext typed by the user
     * @returns {string}
     */
    BlockMessenger.prototype.transformWikitext = function(wikitext){
        wikitext = wikitext.trim();

        // Don't process an empty message
        if(wikitext == '') return wikitext;

        // Add a signature if needed
        if(!/~{3,5}/.test(wikitext)){
            wikitext += ' ~~~~';
        }
        return wikitext;
    };

    /**
     * Releases the close lock, allowing the user to close the window
     */
    BlockMessenger.prototype.releaseCloseLock = function(){
        this.closeLock.release();
    };

    /**
     * Sends a message to the user
     * @param {string} page The page to send the message to
     * @param {string} subject The message subject
     * @param {string} content The message content
     * @returns {JQuery.Promise}
     */
    BlockMessenger.prototype.sendMessage = function(page, subject, content){
        return this.api.newSection(page, subject, content, { redirect: true, watchlist: 'nochange' });
    };

    /**
     * Scans the links in a container and makes them red if they point to
     * a page that doesn't exist.
     * @param {JQuery<HTMLElement>} $container Where to look for links
     */
    BlockMessenger.prototype.makeRedLinks = function($container){
        var wikiPages = [];
        var anchors = [];

        /**
         * @param {string} href 
         * @returns {string|null}
         */
        var extractTargetPage = function(href){
            // Ignore external links
            if(href.indexOf('/wiki/') === -1) return null;

            var pageMatch = /\/wiki\/([^?#]+)/.exec(href);
            if(pageMatch === null) return null;
            return pageMatch[1];
        };

        $container.find('a').each(function(_, a){
            var href = a.getAttribute('href');
            if(href === null) return;

            var targetPage = extractTargetPage(href);
            if(targetPage === null) return;
            wikiPages.push(targetPage);
            anchors.push(a);
        });

        if(wikiPages.length === 0) return;
        this.api.get({
            action: 'query',
            prop: 'info',
            titles: wikiPages.join('|'),
            redirects: true
        }).then(function(data){
            var result = data.query;
            var normalizer = {};
            var pageExists = {};

            if(result.normalized !== undefined){
                for(var i = 0; i < result.normalized.length; i++){
                    normalizer[result.normalized[i].from] = result.normalized[i].to;
                }
            }

            if(result.pages !== undefined){
                for(var i = 0; i < result.pages.length; i++){
                    var page = result.pages[i];
                    pageExists[page.title] = (page.missing !== true);
                }
            }

            for(var i = 0; i < anchors.length; i++){
                var anchor = anchors[i];
                var pageTitle = extractTargetPage(anchor.getAttribute('href'));
                if(pageTitle === null) continue;

                if(normalizer[pageTitle] !== undefined){
                    pageTitle = normalizer[pageTitle];
                }

                if(pageExists[pageTitle] === false){
                    anchor.classList.add('new');
                }
            }
        });
        // Do nothing on failure, this is not critical
    }


    /**
     * The gadget entry point. Checks whether we can run here
     * and then invokes the actual gadget code.
     */
    function main(){
        // Ensure we are on the right page
        var specialPage = mw.config.get('wgCanonicalSpecialPageName');
        if(specialPage !== 'Block') return;
    
        // Ensure the user has already been blocked
        // (i.e. we see the confirmation page)
        var userNameInput = document.querySelector('input[name="wpTarget"]');
        if(userNameInput !== null) return;
    
        // Get the blocked user name. Will be null if blocking an IP range
        var blockedUserName = mw.config.get('wgRelevantUserName');
        if(blockedUserName === null) return;

        // Gadget is used so rarely that we shouldn't load dependencies at every page load
        mw.loader.using([
            'mediawiki.api',
            'mediawiki.confirmCloseWindow',
            'mediawiki.jqueryMsg',
            'mediawiki.util',
            'oojs-ui-core'
        ]).then(function(){
                var messenger = new BlockMessenger(blockedUserName);
                $('#mw-content-text').append(messenger.$element);
            });
    }

    main();
});
/**
 * @typedef {{title: string, text: string}} PresetMessage
 */
// </nowiki>