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
This commit is contained in:
Santhosh Thottingal
2021-09-02 16:12:55 +05:30
committed by jenkins-bot
parent 03ab8c3b0b
commit 5e6838ebdf
11 changed files with 358 additions and 11 deletions

View File

@@ -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": {

View File

@@ -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"
}

View File

@@ -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."
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<path d="m5.83 9 5.58-5.58L10 2l-8 8 8 8 1.41-1.41L5.83 11H18V9z"/>
</svg>

After

Width:  |  Height:  |  Size: 199 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><title>ellipsis</title><circle cx="10" cy="10" r="2"/><circle cx="3" cy="10" r="2"/><circle cx="17" cy="10" r="2"/></svg>

After

Width:  |  Height:  |  Size: 242 B

View File

@@ -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;
}() );

View File

@@ -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();
}() );

View File

@@ -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 = '<div class="uls-menu uls-language-actions-dialog">' +
'<div class="uls-language-actions-title">' +
'<button class="mw-ui-button mw-ui-quiet uls-language-actions-close"></button>' +
'<span> <strong></strong> </span>' +
'</div>' +
'<div class="uls-language-action-items"></div>' +
'</div>';
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;
}() );

View File

@@ -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 $( '<button>' ).addClass( classes );
}
/**
* @param {jQuery} actionsMenuTrigger
* @param {number} menuItemsCount
*/
function setActionsMenuTriggerIconClass( actionsMenuTrigger, menuItemsCount ) {
var iconClass, iconClasses = {
single: 'uls-language-actions-button--single',
multiple: 'uls-language-actions-button--multiple'
};
if ( menuItemsCount > 1 ) {
iconClass = iconClasses.multiple;
} else {
iconClass = iconClasses.single;
}
iconClasses = Object.keys( iconClasses ).map( function ( key ) {
return iconClasses[ key ];
} );
// reset icon classes
// The following classes are being removed here (if present):
// * uls-language-actions-button--multiple
// * uls-language-actions-button--single
actionsMenuTrigger.removeClass( iconClasses );
// One of the following classes are being added here:
// * uls-language-actions-button--multiple
// OR
// * uls-language-actions-button--single
actionsMenuTrigger.addClass( iconClass );
}
/**
* @param {jQuery} $element
* @param {Function} onCloseHandler
* @param {Object} uls
*/
function openLanguageSettings( $element, onCloseHandler, uls ) {
mw.loader.using( languageSettingsModules ).then( function () {
$element.languagesettings( {
defaultModule: 'display',
onClose: onCloseHandler,
onPosition: uls.position.bind( uls ),
onVisible: uls.hide.bind( uls )
} ).trigger( 'click' );
} );
}
/**
* Add language actions menu
*
* @param {Object} uls The ULS object
*/
function addActionsMenuTrigger( uls ) {
var actionsMenuDialog;
function openActionsMenuEventHandler( event ) {
event.stopPropagation();
function onMenuClose() {
actionsMenuDialog.hide();
uls.show();
}
openLanguageSettings( $( event.target ), onMenuClose, uls );
}
var languageSettingsMenuItem = {
name: 'languageSettings',
icon: 'settings',
text: $.i18n( 'ext-uls-actions-menu-language-settings-item-label' ),
handler: openActionsMenuEventHandler
};
require( './ext.uls.actions.menu.items.registry.js' );
var actionItemsRegistry = mw.uls.ActionsMenuItemsRegistry;
actionItemsRegistry.register( languageSettingsMenuItem );
// first hide #uls-settings-block div since it's unused, and it causes
// an unwanted extra border to show up at the bottom of the menu
uls.$menu.find( '#uls-settings-block' ).eq( 0 ).hide();
var $actionsMenuTrigger = createActionsMenuTrigger();
setActionsMenuTriggerIconClass( $actionsMenuTrigger, actionItemsRegistry.size() );
function registerTriggerListener() {
$actionsMenuTrigger.off( 'click' );
$actionsMenuTrigger.on( 'click', function () {
var menuItemsLength = actionItemsRegistry.size();
if ( menuItemsLength === 1 ) {
openLanguageSettings( $actionsMenuTrigger, uls.show.bind( uls ), uls );
$actionsMenuTrigger.off( 'click' );
} else if ( menuItemsLength > 1 ) {
actionsMenuDialog = actionsMenuDialog || new ActionsMenu( {
actions: actionItemsRegistry.getItems(),
onPosition: uls.position.bind( uls ),
onClose: uls.show.bind( uls )
} );
actionsMenuDialog.render();
uls.hide();
}
} );
}
function onActionItemAdded( item ) {
var itemsLength = actionItemsRegistry.size();
setActionsMenuTriggerIconClass( $actionsMenuTrigger, itemsLength );
registerTriggerListener();
if ( actionsMenuDialog ) {
actionsMenuDialog.renderAction( item );
}
}
actionItemsRegistry.on( 'register', onActionItemAdded );
uls.$menu.append( $actionsMenuTrigger );
registerTriggerListener();
}
/**
* Add display settings link to the settings bar in ULS
*
@@ -495,9 +615,8 @@
// Provide access to display and input settings if this entry point is the single
// point of access to all language settings.
uls = $target.data( 'uls' );
loadDisplayAndInputSettings( uls ).always( function () {
$target.trigger( 'click' );
} );
addActionsMenuTrigger( uls );
$target.trigger( 'click' );
} else {
$target.trigger( 'click' );
}