From 5e6838ebdfb1d34ebbd67357c8aae0fefdcd87cb Mon Sep 17 00:00:00 2001 From: Santhosh Thottingal Date: Thu, 2 Sep 2021 16:12:55 +0530 Subject: [PATCH] Add actions menu inside content language selector This patch replaces the display and input settings menu bar at the bottom of the content language selector, with a floating icon that opens a menu containing all the available language actions. In case that only the language settings action is available, the language settings menu is being opened instead. In order to provide extensibility and support the addition of new action items from other extensions, a registry class that inherits from OO.Registry class is created. This class is used to create a singleton registry object that holds all action items that should be rendered inside the menu. Other modules/extensions can use this registry to add new actions items to the menu, by passing the item as argument, in the following form: { name: "", icon: "", text: "", handler: function() {} } Bug: T289840 Change-Id: Iee017a9e3e6a654145e9fdd2b7df35baa348697d --- extension.json | 18 ++- i18n/en.json | 4 +- i18n/qqq.json | 4 +- resources/css/ext.uls.actions.menu.less | 25 ++++ resources/css/ext.uls.interface.less | 17 +++ resources/images/arrow-previous.svg | 4 + resources/images/ellipsis.svg | 1 + resources/js/ext.uls.actions.menu.item.js | 19 +++ .../js/ext.uls.actions.menu.items.registry.js | 35 +++++ resources/js/ext.uls.actions.menu.js | 113 +++++++++++++++ resources/js/ext.uls.interface.js | 129 +++++++++++++++++- 11 files changed, 358 insertions(+), 11 deletions(-) create mode 100644 resources/css/ext.uls.actions.menu.less create mode 100644 resources/images/arrow-previous.svg create mode 100644 resources/images/ellipsis.svg create mode 100644 resources/js/ext.uls.actions.menu.item.js create mode 100644 resources/js/ext.uls.actions.menu.items.registry.js create mode 100644 resources/js/ext.uls.actions.menu.js 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 @@ +ellipsis \ 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 $( '