diff --git a/extension.json b/extension.json index ea4b8dbe..26b4096d 100644 --- a/extension.json +++ b/extension.json @@ -243,20 +243,30 @@ "targets": [ "desktop", "mobile" ], "packageFiles": [ "js/ext.uls.interface.js", - "js/ext.uls.launch.js" + "js/ext.uls.launch.js", + "js/ext.uls.actions.menu.js", + "js/ext.uls.actions.menu.item.js", + "js/ext.uls.actions.menu.items.registry.js" + ], + "styles": [ + "css/ext.uls.interface.less", + "css/ext.uls.actions.menu.less" ], - "styles": "css/ext.uls.interface.less", "dependencies": [ "mediawiki.jqueryMsg", "mediawiki.storage", "mediawiki.user", - "ext.uls.webfonts" + "ext.uls.webfonts", + "oojs-ui-widgets", + "oojs-ui.styles.icons-interactions" ], "messages": [ "uls-plang-title-languages", "ext-uls-select-language-settings-icon-tooltip", "ext-uls-undo-language-tooltip-text", - "ext-uls-undo-language-tooltip-text-local" + "ext-uls-undo-language-tooltip-text-local", + "ext-uls-actions-menu-language-settings-item-label", + "ext-uls-actions-menu-header" ] }, "ext.uls.interlanguage": { diff --git a/i18n/en.json b/i18n/en.json index 75bc21f7..b9db5f76 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -72,5 +72,7 @@ "ext-uls-setlang-heading": "Change interface language?", "ext-uls-setlang-accept": "Accept change", "ext-uls-setlang-loading": "Applying...", - "ext-uls-setlang-cancel": "Don't change" + "ext-uls-setlang-cancel": "Don't change", + "ext-uls-actions-menu-header": "More options", + "ext-uls-actions-menu-language-settings-item-label": "Open language settings" } diff --git a/i18n/qqq.json b/i18n/qqq.json index cd424842..b083445c 100644 --- a/i18n/qqq.json +++ b/i18n/qqq.json @@ -76,5 +76,7 @@ "ext-uls-setlang-heading": "Message shown as heading to the user on the set language preference dialog. Parameters:\n* $1 - Language name", "ext-uls-setlang-accept": "Button label for accepting the suggested language in language preference dialog.", "ext-uls-setlang-loading": "Button label displayed while the API call is in progress after the user clicks on the accept button. See {{msg-mw|ext-uls-setlang-accept}}.", - "ext-uls-setlang-cancel": "Button label for cancel on the language preference dialog." + "ext-uls-setlang-cancel": "Button label for cancel on the language preference dialog.", + "ext-uls-actions-menu-header": "Title of the dialog that contains the quick actions menu, that opens when user clicks on the settings icon inside content language selector", + "ext-uls-actions-menu-language-settings-item-label": "Label of the button that opens the language settings inside the quick actions menu of the content language selector." } diff --git a/resources/css/ext.uls.actions.menu.less b/resources/css/ext.uls.actions.menu.less new file mode 100644 index 00000000..816212ae --- /dev/null +++ b/resources/css/ext.uls.actions.menu.less @@ -0,0 +1,25 @@ +.uls-menu.uls-language-actions-dialog { + min-width: 248px; + + .uls-language-actions-title { + border-bottom: 1px solid #c8ccd1; + display: flex; + align-items: center; + height: 32px; + padding: 5px 0; + + .uls-language-actions-close { + min-width: unset; + width: 44px; + background: transparent url( ../images/arrow-previous.svg ) no-repeat center; + } + } + + .uls-language-action-items { + .uls-language-action { + margin: 0; + padding: 12px 8px; + display: block; + } + } +} diff --git a/resources/css/ext.uls.interface.less b/resources/css/ext.uls.interface.less index ce60cf07..a66d868c 100644 --- a/resources/css/ext.uls.interface.less +++ b/resources/css/ext.uls.interface.less @@ -36,6 +36,23 @@ } } +.mw-ui-button.uls-language-actions-button { + position: absolute; + bottom: 0; + // set a value that doesn't cause overlap with scrollbar + right: 16px; + background: center transparent no-repeat; + min-width: unset; + + &--single { + background-image: url( ../images/cog.svg ); + } + + &--multiple { + background-image: url( ../images/ellipsis.svg ); + } +} + .uls-tipsy.uls-tipsy { z-index: 1000; } diff --git a/resources/images/arrow-previous.svg b/resources/images/arrow-previous.svg new file mode 100644 index 00000000..80b725e5 --- /dev/null +++ b/resources/images/arrow-previous.svg @@ -0,0 +1,4 @@ + + diff --git a/resources/images/ellipsis.svg b/resources/images/ellipsis.svg new file mode 100644 index 00000000..6c37297f --- /dev/null +++ b/resources/images/ellipsis.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/js/ext.uls.actions.menu.item.js b/resources/js/ext.uls.actions.menu.item.js new file mode 100644 index 00000000..e06f47bc --- /dev/null +++ b/resources/js/ext.uls.actions.menu.item.js @@ -0,0 +1,19 @@ +( function () { + var ActionsMenuItem = function ( icon, text, handler ) { + this.icon = icon; + this.text = text; + this.handler = handler; + }; + + ActionsMenuItem.prototype.render = function () { + return new OO.ui.ButtonWidget( { + framed: false, + icon: this.icon, + label: this.text, + classes: [ 'uls-language-action' ], + flags: [ 'progressive' ] + } ); + }; + + module.exports = ActionsMenuItem; +}() ); diff --git a/resources/js/ext.uls.actions.menu.items.registry.js b/resources/js/ext.uls.actions.menu.items.registry.js new file mode 100644 index 00000000..62778c56 --- /dev/null +++ b/resources/js/ext.uls.actions.menu.items.registry.js @@ -0,0 +1,35 @@ +( function () { + 'use strict'; + + function ActionsMenuItemsRegistry() { + ActionsMenuItemsRegistry.super.apply( this, arguments ); + } + + OO.inheritClass( ActionsMenuItemsRegistry, OO.Registry ); + + ActionsMenuItemsRegistry.prototype.size = function () { + return Object.keys( this.registry ).length; + }; + + ActionsMenuItemsRegistry.prototype.getItems = function () { + var registry = this.registry; + return Object.keys( registry ).map( function ( key ) { + return registry[ key ]; + } ); + }; + + /** + * Register an action item with the factory. + * Actions items are required to include all necessary properties, + * i.e. name, icon, text and handler function. + * + * @param {{name: string, icon: string, text: string, handler: Function}} item + */ + ActionsMenuItemsRegistry.prototype.register = function ( item ) { + // Parent method + ActionsMenuItemsRegistry.super.prototype.register.call( this, item.name, item ); + }; + + mw.uls = mw.uls || {}; + mw.uls.ActionsMenuItemsRegistry = new ActionsMenuItemsRegistry(); +}() ); diff --git a/resources/js/ext.uls.actions.menu.js b/resources/js/ext.uls.actions.menu.js new file mode 100644 index 00000000..9178734f --- /dev/null +++ b/resources/js/ext.uls.actions.menu.js @@ -0,0 +1,113 @@ +( function () { + 'use strict'; + + var ActionsMenuItem = require( './ext.uls.actions.menu.item.js' ); + + function ActionsMenu( options ) { + this.options = options; + this.$template = $( ActionsMenu.template ); + this.actionItems = options.actions.map( function ( action ) { + return new ActionsMenuItem( action.icon, action.text, action.handler ); + } ); + this.rendered = false; + this.shown = false; + } + + ActionsMenu.template = '
'; + + ActionsMenu.prototype = { + + /** + * Render the module into a given target + */ + render: function () { + if ( this.rendered ) { + this.shown = true; + this.$template.show(); + return; + } + this.actionItems.forEach( function ( actionItem ) { + this.renderAction( actionItem ); + }.bind( this ) ); + + this.i18n(); + $( document.body ).append( this.$template ); + this.$template.css( this.position() ); + this.$template.show(); + this.$template.find( '.uls-language-actions-close' ).on( 'click', function ( event ) { + event.stopPropagation(); + this.close(); + }.bind( this ) ); + + $( document.body ).on( 'click', this.cancel.bind( this ) ); + + this.shown = true; + this.rendered = true; + }, + + position: function () { + if ( this.options.onPosition ) { + return this.options.onPosition.call( this ); + } + }, + + /** + * @param {ActionsMenuItem | Object} actionItem + */ + renderAction: function ( actionItem ) { + if ( !( actionItem instanceof ActionsMenuItem ) ) { + actionItem = new ActionsMenuItem( + actionItem.icon, + actionItem.text, + actionItem.handler + ); + } + var actionButton = actionItem.render(); + this.$template.find( '.uls-language-action-items' ).prepend( + actionButton.$element + ); + actionButton.$element.one( 'click', actionItem.handler ); + }, + + i18n: function () { + this.$template.find( '.uls-language-actions-title strong' ) + .text( $.i18n( 'ext-uls-actions-menu-header' ) ); + }, + + hide: function () { + this.shown = false; + this.$template.hide(); + }, + + cancel: function ( e ) { + if ( e && ( this.$template.is( e.target ) || + $.contains( this.$template[ 0 ], e.target ) ) ) { + return; + } + + this.hide(); + }, + + close: function () { + if ( !this.shown ) { + return; + } + + this.hide(); + // optional callback + if ( this.options.onClose ) { + this.options.onClose(); + } + + } + }; + + module.exports = ActionsMenu; + +}() ); diff --git a/resources/js/ext.uls.interface.js b/resources/js/ext.uls.interface.js index 513f422a..1b7df873 100644 --- a/resources/js/ext.uls.interface.js +++ b/resources/js/ext.uls.interface.js @@ -20,8 +20,8 @@ ( function () { 'use strict'; var languageSettingsModules = [ 'ext.uls.displaysettings' ], - launchULS = require( './ext.uls.launch.js' ); - + launchULS = require( './ext.uls.launch.js' ), + ActionsMenu = require( './ext.uls.actions.menu.js' ); /** * Construct the display settings link * @@ -63,6 +63,126 @@ return $( '#p-lang-btn' ).length > 0 || mw.config.get( 'wgULSDisplaySettingsInInterlanguage' ); } + /** + * @return {jQuery} + */ + function createActionsMenuTrigger() { + var classes = [ 'mw-ui-button', 'mw-ui-quiet', 'uls-language-actions-button' ]; + return $( '