MediaWiki:Gadget-CustomSidebar.js

/*
* 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 skin = mw.config.get( 'skin' );
		var 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' ]
			} );
		// Manage events
		editButton.on( 'click', function() {
			mw.loader.using( [ 'oojs-ui', 'mediawiki.notification' ], function() {
				bootstrapOnce();
				instanceSidebarEditor.open();
			} );

		} );
		if (skin === 'minerva') {
			$links = $( '<ul class="toggle-list__list" id="' + PORTLETID + '">' );
			var	$title = $( '<h4>' ).text( mw.msg( 'cside-portlet-label' ) );

			// Add the new portlet to the DOM
			$( '#p-navigation' ).after( editButton.$element, $title, $links );
		} else {
			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">' ),
				$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 );

			// 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> */

Content Disclaimer

Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.

  1. The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
  2. There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
  3. It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
  4. Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
  5. Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.