MediaWiki:Gadget-CustomSidebar.js

From Test Wiki
Revision as of 12:55, 2 May 2024 by DodoMan (talk | contribs) (1 revision imported)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/*
* CustomSidebar
*
* ...
*
* {{Projet:JavaScript/Script|CustomSidebar}}
*/
/* <nowiki> */

// Note for cleanup after debugging - To delete the saved sidebar:
// new mw.Api().saveOption( 'userjs-customsidebar', null );

/* globals mw, OO, $ */

mw.loader.using( [ 'mediawiki.util', 'oojs-ui.styles.icons-interactions', 'oojs-ui-widgets' ], function () {
    'use strict';

    // Site-related parameters
    const PREFID = 'userjs-customsidebar',
          PORTLETID = 'p-cs';

    const MAGIC_WORDS = {
        day: 'jour',
        week: 'semaine',
        month: 'mois',
        monthname: 'nom du mois',
        year: 'année',
        page: 'page',
        title: 'titre',
        diff: 'diff',
        user: 'pseudo'
    };

    const MONTH_NAMES = [
        'janvier',
        'février',
        'mars',
        'avril',
        'mai',
        'juin',
        'juillet',
        'août',
        'septembre',
        'octobre',
        'novembre',
        'décembre'
    ];

    const deltaDayRegex = new RegExp( '{ *' + MAGIC_WORDS.day + '([\+\-][0-9]+) *}' ),
        deltaWeekRegex = new RegExp( '{ *' + MAGIC_WORDS.week + '([\+\-][0-9]+) *}' ),
        deltaMonthRegex = new RegExp( '{ *(?:' + MAGIC_WORDS.month + '|' + MAGIC_WORDS.monthname + ')([\+\-][0-9]+) *}' ),
        deltaYearRegex = new RegExp( '{ *' + MAGIC_WORDS.year + '([\+\-][0-9]+) *}' ),
        dayRegex = new RegExp( '{ *' + MAGIC_WORDS.day + '([\+\-][0-9]+)* *}', 'g' ),
        weekRegex = new RegExp( '{ *' + MAGIC_WORDS.week + '([\+\-][0-9]+)* *}', 'g' ),
        monthRegex = new RegExp( '{ *' + MAGIC_WORDS.month + '([\+\-][0-9]+)* *}', 'g' ),
        monthnameRegex = new RegExp( '{ *' + MAGIC_WORDS.monthname + '([\+\-][0-9]+)* *}', 'g' ),
        yearRegex = new RegExp( '{ *' + MAGIC_WORDS.year + '([\+\-][0-9]+)* *}', 'g' ),
        pageRegex = new RegExp( '{ *' + MAGIC_WORDS.page + ' *}', 'g' ),
        titleRegex = new RegExp( '{ *' + MAGIC_WORDS.title + ' *}', 'g' ),
        diffRegex = new RegExp( '{ *' + MAGIC_WORDS.diff + ' *}', 'g' ),
        userRegex = new RegExp( '{ *' + MAGIC_WORDS.user + ' *}', 'g' );

    // global vars
    var $links,
        links = [];

    var isBootstrapped = false;
    var instanceWindowManager;
    var instanceSidebarEditor;


    /**
     * I18N Messages
     */
    const messages = {
        'fr': {
            'cside-portlet-label': 'Mes liens',
            'cside-edit-title': 'Ajouter et supprimer les liens',
            'cside-edit-label': 'Modifier les liens',
            'sidebareditor-title': 'Personnaliser le menu latéral',
            'sidebareditor-nolinkmessage': 'Aucun lien présent.<br>Ajoutez-en via le bouton ci-dessous !',
            'sidebareditor-action-save': 'Sauvegarder',
            'sidebareditor-action-cancel': 'Annuler',
            'sidebareditor-action-add': 'Ajouter',
            'sidebareditor-action-delete': 'Supprimer',
            'sidebareditor-notif-success': 'Le nouveau menu latéral a été sauvegardé.',
            'cside-linkfield-label': 'Texte',
            'cside-linkfield-target': 'Lien cible',
            'cside-linkfield-target-help': 'Ce champ peut contenir au choix un lien interne ou externe. Des mots magiques permettent de changer dynamiquement certaines valeurs : {$1}, {$2}, {$3}, {$4}, {$5}, {$6}, {$7}, {$8} et {$9}. Les 5 premiers mots magiques peuvent être accompagnés d\'un nombre pour décaler la période ; par exemple {$1-2} pour avant-hier ou {$4+1} pour le mois prochain.',
        }
    };
    mw.messages.set( messages.fr );
    var lang = mw.config.get( 'wgUserLanguage' );
    if ( lang !== 'fr' && lang in messages ) {
        mw.messages.set( messages[ lang ] );
    }


    /**
     * ...
     */
    function fetchLinks() {
        var fetchedLinks,
            raw = mw.user.options.get( PREFID );

        // If there is nothing stored, default to an empty array
        if ( raw === null ) {
            return [];
        }

        fetchedLinks = JSON.parse( raw );
        if ( ! Array.isArray( fetchedLinks ) ) {
            return [];
        }

        return fetchedLinks;
    }

    function createPortlet() {
        var $portlet = $( '<div class="vector-main-menu-group vector-menu mw-portlet mw-portlet-tb vector-menu-portal portal" role="navigation" id="' + PORTLETID + '" aria-labelledby="' + PORTLETID + '-label">' ),
            editButton = new OO.ui.ButtonWidget( {
                framed: false,
                icon: 'settings',
                label: mw.msg( 'cside-edit-label' ),
                invisibleLabel: true,
                title: mw.msg( 'cside-edit-title' ),
                classes: [ 'cs-editbutton' ]
            } ),
            $title = $( '<p class="vector-menu-heading"><span class="vector-menu-heading-label" id="' + PORTLETID + '-label">' ).text( mw.msg( 'cside-portlet-label' ) ),
            $wrapper = $( '<div class="vector-menu-content">' );

        $portlet.append( editButton.$element, $title, $wrapper );

        $links = $( '<ul class="vector-menu-content-list">' );
        $wrapper.append( $links );

        // Manage events
        editButton.on( 'click', function() {
            mw.loader.using( [ 'oojs-ui', 'mediawiki.notification' ], function() {
                bootstrapOnce();
                instanceSidebarEditor.open();
            } );

        } );

        // Add the new portlet to the DOM
        $( '#p-tb' ).after( $portlet );
    }

    // Returns the ISO week of the date.
    function getWeek( d ) {
        var date = new Date(d.getTime());
        date.setHours(0, 0, 0, 0);
        // Thursday in current week decides the year.
        date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
        // January 4 is always in week 1.
        var week1 = new Date(date.getFullYear(), 0, 4);
        // Adjust to Thursday in week 1 and count number of weeks from date to week1.
        return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000 - 3 + (week1.getDay() + 6) % 7) / 7);
    }

    function daysInYear( year ) {
        if ( year % 4 === 0 && year % 100 !== 0 || year % 400 === 0 ) {
            return 366;
        }
        return 365;
    }

    function daysInMonth( month, year ) {
        return new Date( year, month + 1, 0 ).getDate();
    }

    function fillPortlet( links ) {
        var i, j, target, today, deltaDay, deltaWeek, deltaMonth, deltaYear, coef, offset;

        // Empty the portlet in case of a second run
        $links.empty();

        // Fill the portlet with all the given links
        for ( i = 0; i < links.length; i++ ) {
            target = links[ i ][ 0 ];
            today = new Date();

            // Change the date if asked to
            deltaDay = parseInt( ( deltaDayRegex.exec( target ) || [0,0])[1]);
            deltaWeek = parseInt( ( deltaWeekRegex.exec( target ) || [0,0])[1]);
            deltaMonth = parseInt( ( deltaMonthRegex.exec( target ) || [0,0])[1]);
            deltaYear = parseInt( ( deltaYearRegex.exec( target ) || [0,0])[1]);
            if ( mw.config.get( 'debug' ) === true ) {
                console.log( '===', links[ i ][ 1 ] );
                console.log( 'deltaDay', deltaDay );
                console.log( 'deltaWeek', deltaWeek );
                console.log( 'deltaMonth', deltaMonth );
                console.log( 'deltaYear', deltaYear );
            }

            today.setTime( today.getTime() + ( ( deltaDay + ( deltaWeek * 7 ) ) * 24 * 3600 * 1000 ) );
            coef = deltaMonth < 0 ? -1 : 1;
            offset = deltaMonth < 0 ? -1 : 0;
            for ( j = Math.abs( deltaMonth ); j > 0; j--) {
                today.setTime( today.getTime() + ( coef * daysInMonth( today.getMonth() + offset, today.getYear() ) * 24 * 3600 * 1000 ) );
            }
            coef = deltaYear < 0 ? -1 : 1;
            offset = deltaYear < 0 ? -1 : 0;
            for ( j = Math.abs( deltaYear ); j > 0; j--) {
                today.setTime( today.getTime() + ( coef * daysInYear( today.getYear() + offset ) * 24 * 3600 * 1000 ) );
            }

            // Render magic words
            target = target.replace( dayRegex, today.getDate() );
            target = target.replace( weekRegex, getWeek( today ) );
            target = target.replace( monthRegex, ( today.getMonth() + 1 ) );
            target = target.replace( monthnameRegex, MONTH_NAMES[ today.getMonth() ] );
            target = target.replace( yearRegex, today.getFullYear() );
            target = target.replace( pageRegex, mw.config.get( 'wgPageName' ) );
            target = target.replace( titleRegex, mw.config.get( 'wgTitle' ) );
            target = target.replace( diffRegex, mw.config.get( 'wgRevisionId' ) );
            target = target.replace( userRegex, mw.config.get( 'wgRelevantUserName' ) || '' );

            // Convert internal links to URL
            if ( ! ( target.lastIndexOf( 'https://', 0) === 0 || target.lastIndexOf( 'http://', 0) === 0 ) ) {
                target = mw.util.getUrl( target );
            }

            mw.util.addPortletLink( PORTLETID, target, links[ i ][ 1 ] );
        }
    }


    /**
     * Main function
     */
    $( function ( $ ) {
        links = fetchLinks();
        createPortlet();
        fillPortlet( links );
    } );


    // Instanciate SidebarEditor and add it to MediaWiki's UI.
    function bootstrapOnce() {
        if (isBootstrapped) {
            return;
        }

        isBootstrapped = true;

        /**
         * Main class of the configuration windows SidebarEditor,
         * which is displayed as a ProcessDialog
         *
         * @class
         * @extends OO.ui.ProcessDialog
         *
         * @constructor
         */
        var SidebarEditor = function () {
            // Initialize config
            var config = { size: 'large' };

            // Parent constructor
            SidebarEditor.parent.call( this, config );

            // Properties
            this.api = new mw.Api( { timeout: 7000 } );

            // Graphical properties
            this.linkFields = [];
            this.$noLinkMessage = $( '<p class="cs-nolinkmessage">' ).html( mw.msg( 'sidebareditor-nolinkmessage' ) );
            this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
            this.layout = new OO.ui.Widget( { content: [] } );
            this.$body;
        };

        /* Setup */
        OO.inheritClass( SidebarEditor, OO.ui.ProcessDialog );

        /* Static Properties */
        SidebarEditor.static.name = 'sidebareditor';
        SidebarEditor.static.title = mw.msg( 'sidebareditor-title' );
        SidebarEditor.static.actions = [
            { action: 'save', label: mw.msg( 'sidebareditor-action-save' ), flags: [ 'primary', 'progressive' ] },
            { action: 'cancel', label: mw.msg( 'sidebareditor-action-cancel' ), flags: [ 'safe', 'back' ] },
            { action: 'add', label: mw.msg( 'sidebareditor-action-add' ), flags: 'other' }
        ];

        /* ProcessDialog-related Methods */

        /**
         * Build the interface displayed inside the ProcessDialog box.
         */
        SidebarEditor.prototype.initialize = function () {
            var i = 0;
            SidebarEditor.parent.prototype.initialize.apply( this, arguments );

            for ( i = 0; i < links.length; i++ ) {
                this.addField( links[ i ][ 0 ], links[ i ][ 1 ] );
            }

            this.content.$element.append( this.$noLinkMessage );
            this.content.$element.append( this.layout.$element );
            this.$body.append( this.content.$element );

            this.setSize( this.size );
            this.updateSize();
        };

        /**
         * Get a process for taking action.
         *
         * This method is called within the ProcessDialog when the user clicks
         * on an action button (the one defined in SidebarEditor.static.actions).
         * @param {string} action Name of the action button clicked.
         * @return {OO.ui.Process} Action process.
         */
        SidebarEditor.prototype.getActionProcess = function ( action ) {
            var process = new OO.ui.Process();

            if ( action === 'cancel' || action === '' ) { // empty string when closing with Escape key
                process.next( this.closeDialog, this );
            }
            else if ( action === 'add' ) {
                process.next( this.addField, this );
            }
            else if ( action === 'save' ) {
                process.next( this.save, this )
                    .next( this.success, this )
                    .next( this.closeDialog, this );
            }

            return process;
        };

        /**
         * Close the window.
         *
         * @return {jQuery.Promise} Promise resolved when window is closed
         */
        SidebarEditor.prototype.closeDialog = function () {
            var dialog = this;

            var lifecycle = dialog.close();

            return lifecycle.closed;
        };

        /**
         * Get the height of the window body.
         * Used by the ProcessDialog to set an accurate height to the dialog.
         *
         * @return {number} Height in px the dialog should be.
         */
        SidebarEditor.prototype.getBodyHeight = function () {
            return 400; //this.content.$element.outerHeight( true );
        };

        /* Process step methods */
        SidebarEditor.prototype.addField = function ( target, label ) {
            var i = this.linkFields.push( new LinkField( target, label ) );
            this.linkFields[ i - 1 ].on( 'delete', this.removeField.bind( this, this.linkFields[ i - 1 ] ) );
            this.layout.$element.append( this.linkFields[ i - 1 ].$element );
            this.$noLinkMessage.hide();
        };

        SidebarEditor.prototype.removeField = function ( field ) {
            var i = this.linkFields.indexOf( field );

            this.linkFields[ i ].$element.detach();
            this.linkFields.splice( i, 1 );

            if ( this.linkFields.length === 0 ) {
                this.$noLinkMessage.show();
            }
        };

        SidebarEditor.prototype.save = function () {
            var i;
            this.newLinks = [];

            for ( i = 0; i < this.linkFields.length; i++ ) {
                this.newLinks.push( this.linkFields[ i ].getValue() );
            }

            return this.api.saveOption( PREFID, JSON.stringify( this.newLinks ) );
        };

        SidebarEditor.prototype.success = function () {
            // Reload the sidebar with the new links
            fillPortlet( this.newLinks );
            links = this.newLinks;

            // Notify the user that it's a success!
            mw.notification.notify( mw.msg( 'sidebareditor-notif-success' ), { autoHide: true } );
        };


        /**
         * ...
         *
         * @class
         * @extends OO.ui.Widget
         *
         * @constructor
         */
        var LinkField = function ( target, label, config ) {
            // Initialize config
            config = config || {};

            // Parent constructor
            LinkField.parent.call( this, config );

            // Properties
            this.api = new mw.Api( { timeout: 15000 } );

            // Graphical properties
            this.targetTextInput = new OO.ui.TextInputWidget( {
                value: target,

            } );
            this.labelTextInput = new OO.ui.TextInputWidget( {
                value: label,
            } );
            this.deleteButton = new OO.ui.ButtonWidget( {
                icon: 'trash',
                framed: false,
                label: mw.msg( 'sidebareditor-action-delete' ),
                invisibleLabel: true,
                title: mw.msg( 'sidebareditor-action-delete' ),
                flags: [ 'destructive' ]
            } );

            // Layouts
            var line1 = new OO.ui.ActionFieldLayout( this.labelTextInput, this.deleteButton, {
                align: 'left',
                label: mw.msg( 'cside-linkfield-label' )
            } );
            var line2 = new OO.ui.FieldLayout( this.targetTextInput, {
                align: 'left',
                label: mw.msg( 'cside-linkfield-target' ),
                help: mw.msg( 'cside-linkfield-target-help', MAGIC_WORDS.day, MAGIC_WORDS.week, MAGIC_WORDS.month, MAGIC_WORDS.monthname, MAGIC_WORDS.year, MAGIC_WORDS.page, MAGIC_WORDS.title, MAGIC_WORDS.diff, MAGIC_WORDS.user )
            } );

            // Events
            this.deleteButton.on( 'click', this.emit.bind( this, 'delete' ) );

            // Add all those widgets to the layout
            this.$element.append( line1.$element )
                .append( line2.$element );
            this.$element.addClass( 'cs-linkfield' );
        };

        /* Setup */
        OO.inheritClass( LinkField, OO.ui.Widget );

        LinkField.prototype.getValue = function () {
            return [ this.targetTextInput.getValue(), this.labelTextInput.getValue() ];
        };


        instanceWindowManager = new OO.ui.WindowManager();
        $( 'body' ).append( instanceWindowManager.$element );

        instanceSidebarEditor = new SidebarEditor();
        instanceWindowManager.addWindows( [ instanceSidebarEditor ] );
    }

} );

/* </nowiki> */