Jump to content

User:Polygnotus/Scripts/UsernameExtractor.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/**
 * UsernameExtractor
 * Opens an OOUI popup where you can paste wikitext copied from talk pages.
 * Extracts unique usernames from [[User:Username]] links and formats them
 * as {{ping|user1|user2|...}} for copying to clipboard.
 *
 * If more than 50 unique usernames are found, they are split into chunks of
 * 50 and each chunk gets its own copy button (the ping template cannot handle
 * more than 50 parameters).
 *
 * Usage: Click "Username Extractor" in the Tools menu (left sidebar).
 */

( function () {
    'use strict';

    var PING_LIMIT = 50;

    mw.loader.using( [
        'oojs-ui-core',
        'oojs-ui-widgets',
        'oojs-ui-windows'
    ] ).done( function () {

        // ---------------------------------------------------------------------
        // Dialog definition
        // ---------------------------------------------------------------------
        function UsernameExtractorDialog( config ) {
            UsernameExtractorDialog.super.call( this, config );
        }
        OO.inheritClass( UsernameExtractorDialog, OO.ui.ProcessDialog );

        UsernameExtractorDialog.static.name = 'usernameExtractorDialog';
        UsernameExtractorDialog.static.title = 'Username extractor';
        UsernameExtractorDialog.static.actions = [
            {
                action: 'extract',
                label: 'Extract',
                flags: [ 'primary', 'progressive' ]
            },
            {
                label: 'Close',
                flags: [ 'safe', 'close' ]
            }
        ];

        UsernameExtractorDialog.prototype.initialize = function () {
            UsernameExtractorDialog.super.prototype.initialize.call( this );

            // Input: wikitext paste area
            this.wikitextInput = new OO.ui.MultilineTextInputWidget( {
                placeholder: 'Paste wikitext from talk page(s) here...',
                rows: 10,
                autofocus: true
            } );
            this.wikitextField = new OO.ui.FieldLayout( this.wikitextInput, {
                label: 'Wikitext',
                align: 'top'
            } );

            // Container for dynamically generated result rows (one per chunk)
            this.$chunksContainer = $( '<div>' );

            // Status message (e.g. "Copied!" or "No usernames found")
            this.$status = $( '<p>' ).css( {
                'min-height': '1.2em',
                'margin-top': '4px',
                'color': '#54595d'
            } );

            this.content = new OO.ui.PanelLayout( {
                padded: true,
                expanded: false
            } );

            this.content.$element.append(
                this.wikitextField.$element,
                this.$chunksContainer,
                this.$status
            );

            this.$body.append( this.content.$element );
        };

        /**
         * Extract unique usernames from [[User:Foo]] and [[User:Foo|bar]] patterns.
         * Matching is case-insensitive for the "User:" prefix.
         */
        UsernameExtractorDialog.prototype.extractUsernames = function ( wikitext ) {
            var regex = /\[\[\s*[Uu]ser\s*:\s*([^\]|]+?)(?:\|[^\]]*?)?\s*\]\]/g,
                seen = {},
                usernames = [],
                match;

            while ( ( match = regex.exec( wikitext ) ) !== null ) {
                var name = match[ 1 ].trim();
                // Normalize: first letter uppercase, rest as-is (standard MediaWiki behaviour)
                var normalized = name.charAt( 0 ).toUpperCase() + name.slice( 1 );
                if ( !Object.prototype.hasOwnProperty.call( seen, normalized ) ) {
                    seen[ normalized ] = true;
                    usernames.push( normalized );
                }
            }
            return usernames;
        };

        /**
         * Split an array into chunks of at most `size` elements.
         */
        function chunkArray( arr, size ) {
            var chunks = [];
            for ( var i = 0; i < arr.length; i += size ) {
                chunks.push( arr.slice( i, i + size ) );
            }
            return chunks;
        }

        /**
         * Build one result row: a read-only textarea + copy button for a single
         * {{ping}} command.
         */
        function buildChunkRow( pingText, index, total, $status ) {
            var label = total > 1 ? 'Result (' + index + ' of ' + total + ')' : 'Result';

            var resultInput = new OO.ui.MultilineTextInputWidget( {
                value: pingText,
                rows: 3,
                readOnly: true
            } );
            var resultField = new OO.ui.FieldLayout( resultInput, {
                label: label,
                align: 'top'
            } );

            var copyButton = new OO.ui.ButtonWidget( {
                label: 'Copy to clipboard',
                icon: 'copy'
            } );
            copyButton.on( 'click', function () {
                navigator.clipboard.writeText( pingText ).then( function () {
                    $status.text( 'Copied ' + label + ' to clipboard!' );
                } ).catch( function () {
                    // Fallback for older browsers
                    var $tmp = $( '<textarea>' ).val( pingText ).appendTo( 'body' );
                    $tmp[ 0 ].select();
                    document.execCommand( 'copy' );
                    $tmp.remove();
                    $status.text( 'Copied ' + label + ' to clipboard!' );
                } );
            } );

            return $( '<div>' ).css( 'margin-bottom', '8px' ).append(
                resultField.$element,
                copyButton.$element
            );
        }

        UsernameExtractorDialog.prototype.getActionProcess = function ( action ) {
            var dialog = this;

            if ( action === 'extract' ) {
                return new OO.ui.Process( function () {
                    var wikitext = dialog.wikitextInput.getValue();
                    var usernames = dialog.extractUsernames( wikitext );

                    // Clear previous results
                    dialog.$chunksContainer.empty();
                    dialog.$status.text( '' );

                    if ( usernames.length === 0 ) {
                        dialog.$status.text( 'No usernames found.' );
                        dialog.updateSize();
                        return;
                    }

                    var chunks = chunkArray( usernames, PING_LIMIT );
                    var total = chunks.length;

                    // Warning banner when a split was necessary
                    if ( total > 1 ) {
                        var $warning = $( '<p>' ).css( {
                            color: '#b32424',
                            'font-weight': 'bold',
                            'margin-bottom': '8px'
                        } ).text(
                            '\u26a0 ' + usernames.length + ' usernames found \u2014 ' +
                            'split into ' + total + ' ping commands ' +
                            '(the {{ping}} template supports at most ' + PING_LIMIT + ' users).'
                        );
                        dialog.$chunksContainer.append( $warning );
                    }

                    chunks.forEach( function ( chunk, i ) {
                        var pingText = '{{ping|' + chunk.join( '|' ) + '}}';
                        var $row = buildChunkRow( pingText, i + 1, total, dialog.$status );
                        dialog.$chunksContainer.append( $row );
                    } );

                    if ( total === 1 ) {
                        dialog.$status.text(
                            usernames.length + ' unique username' +
                            ( usernames.length === 1 ? '' : 's' ) + ' found.'
                        );
                    }

                    dialog.updateSize();
                } );
            }

            return UsernameExtractorDialog.super.prototype.getActionProcess.call( this, action );
        };

        UsernameExtractorDialog.prototype.getBodyHeight = function () {
            return this.content.$element.outerHeight( true );
        };

        // ---------------------------------------------------------------------
        // Register and open via portlet link
        // ---------------------------------------------------------------------
        var windowManager = new OO.ui.WindowManager();
        $( document.body ).append( windowManager.$element );
        windowManager.addWindows( [ new UsernameExtractorDialog() ] );

        mw.util.addPortletLink(
            'p-tb',
            '#',
            'Username extractor',
            't-username-extractor',
            'Extract usernames from wikitext and format as {{ping}}'
        );

        $( '#t-username-extractor a' ).on( 'click', function ( e ) {
            e.preventDefault();
            windowManager.openWindow( 'usernameExtractorDialog' );
        } );

    } );

}() );