Client preferences supports toggle switches
Bug: T350418 Change-Id: I359924874e7232eaee73b7dc3678b9e8e26794ac
这个提交包含在:
父节点
7d5caf3f66
当前提交
8c49b1eb49
|
@ -32,10 +32,10 @@ module.exports = {
|
|||
// An object that configures minimum threshold enforcement for coverage results
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 31,
|
||||
functions: 39,
|
||||
lines: 38,
|
||||
statements: 38
|
||||
branches: 35,
|
||||
functions: 45,
|
||||
lines: 48,
|
||||
statements: 48
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
* @typedef {Object} ClientPreference
|
||||
* @property {string[]} options that are valid for this client preference
|
||||
* @property {string} preferenceKey for registered users.
|
||||
* @property {string} [type] defaults to radio. Supported: radio, switch
|
||||
*/
|
||||
let /** @type {MwApi} */ api;
|
||||
/**
|
||||
|
@ -62,6 +63,48 @@ function toggleDocClassAndSave( featureName, value, config ) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} featureName
|
||||
* @param {string} value
|
||||
* @return {string}
|
||||
*/
|
||||
const getInputId = ( featureName, value ) => `skin-client-pref-${ featureName }-value-${ value }`;
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
* @param {string} featureName
|
||||
* @param {string} value
|
||||
* @return {HTMLInputElement}
|
||||
*/
|
||||
function makeInputElement( type, featureName, value ) {
|
||||
const input = document.createElement( 'input' );
|
||||
const name = `skin-client-pref-${ featureName }-group`;
|
||||
const id = getInputId( featureName, value );
|
||||
input.name = name;
|
||||
input.id = id;
|
||||
input.type = type;
|
||||
if ( type === 'checkbox' ) {
|
||||
input.checked = value === '1';
|
||||
} else {
|
||||
input.value = value;
|
||||
}
|
||||
input.setAttribute( 'data-event-name', id );
|
||||
return input;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} featureName
|
||||
* @param {string} value
|
||||
* @return {HTMLLabelElement}
|
||||
*/
|
||||
function makeLabelElement( featureName, value ) {
|
||||
const label = document.createElement( 'label' );
|
||||
// eslint-disable-next-line mediawiki/msg-doc
|
||||
label.textContent = mw.msg( `${ featureName }-${ value }-label` );
|
||||
label.setAttribute( 'for', getInputId( featureName, value ) );
|
||||
return label;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} parent
|
||||
* @param {string} featureName
|
||||
|
@ -70,27 +113,17 @@ function toggleDocClassAndSave( featureName, value, config ) {
|
|||
* @param {Record<string,ClientPreference>} config
|
||||
*/
|
||||
function appendRadioToggle( parent, featureName, value, currentValue, config ) {
|
||||
const input = document.createElement( 'input' );
|
||||
const name = `skin-client-pref-${ featureName }-group`;
|
||||
const id = `skin-client-pref-${ featureName }-value-${ value }`;
|
||||
input.name = name;
|
||||
input.id = id;
|
||||
input.type = 'radio';
|
||||
input.value = value;
|
||||
const input = makeInputElement( 'radio', featureName, value );
|
||||
input.classList.add( 'cdx-radio__input' );
|
||||
if ( currentValue === value ) {
|
||||
input.checked = true;
|
||||
}
|
||||
const icon = document.createElement( 'span' );
|
||||
icon.classList.add( 'cdx-radio__icon' );
|
||||
const label = document.createElement( 'label' );
|
||||
const label = makeLabelElement( featureName, value );
|
||||
label.classList.add( 'cdx-radio__label' );
|
||||
// eslint-disable-next-line mediawiki/msg-doc
|
||||
label.textContent = mw.msg( `${ featureName }-${ value }-label` );
|
||||
label.setAttribute( 'for', id );
|
||||
const container = document.createElement( 'div' );
|
||||
container.classList.add( 'cdx-radio' );
|
||||
input.setAttribute( 'data-event-name', id );
|
||||
container.appendChild( input );
|
||||
container.appendChild( icon );
|
||||
container.appendChild( label );
|
||||
|
@ -100,6 +133,34 @@ function appendRadioToggle( parent, featureName, value, currentValue, config ) {
|
|||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} form
|
||||
* @param {string} featureName
|
||||
* @param {HTMLElement} labelElement
|
||||
* @param {string} currentValue
|
||||
* @param {Record<string,ClientPreference>} config
|
||||
*/
|
||||
function appendToggleSwitch( form, featureName, labelElement, currentValue, config ) {
|
||||
const input = makeInputElement( 'checkbox', featureName, currentValue );
|
||||
input.classList.add( 'cdx-toggle-switch__input' );
|
||||
const switcher = document.createElement( 'span' );
|
||||
switcher.classList.add( 'cdx-toggle-switch__switch' );
|
||||
const grip = document.createElement( 'span' );
|
||||
grip.classList.add( 'cdx-toggle-switch__switch__grip' );
|
||||
switcher.appendChild( grip );
|
||||
const label = labelElement || makeLabelElement( featureName, currentValue );
|
||||
label.classList.add( 'cdx-toggle-switch__label' );
|
||||
const toggleSwitch = document.createElement( 'span' );
|
||||
toggleSwitch.classList.add( 'cdx-toggle-switch' );
|
||||
toggleSwitch.appendChild( input );
|
||||
toggleSwitch.appendChild( switcher );
|
||||
toggleSwitch.appendChild( label );
|
||||
input.addEventListener( 'change', () => {
|
||||
toggleDocClassAndSave( featureName, input.checked ? '1' : '0', config );
|
||||
} );
|
||||
form.appendChild( toggleSwitch );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} className
|
||||
* @return {Element}
|
||||
|
@ -110,6 +171,16 @@ function createRow( className ) {
|
|||
return row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the label for the feature.
|
||||
*
|
||||
* @param {string} featureName
|
||||
* @return {MwMessage}
|
||||
*/
|
||||
const getFeatureLabelMsg = ( featureName ) =>
|
||||
// eslint-disable-next-line mediawiki/msg-doc
|
||||
mw.message( `${ featureName }-name` );
|
||||
|
||||
/**
|
||||
* adds a toggle button
|
||||
*
|
||||
|
@ -117,7 +188,7 @@ function createRow( className ) {
|
|||
* @param {Record<string,ClientPreference>} config
|
||||
* @return {Element|null}
|
||||
*/
|
||||
function makeClientPreferenceBinaryToggle( featureName, config ) {
|
||||
function makeControl( featureName, config ) {
|
||||
const pref = config[ featureName ];
|
||||
if ( !pref ) {
|
||||
return null;
|
||||
|
@ -130,9 +201,21 @@ function makeClientPreferenceBinaryToggle( featureName, config ) {
|
|||
}
|
||||
const row = createRow( '' );
|
||||
const form = document.createElement( 'form' );
|
||||
pref.options.forEach( ( value ) => {
|
||||
appendRadioToggle( form, featureName, value, currentValue, config );
|
||||
} );
|
||||
const type = pref.type || 'radio';
|
||||
switch ( type ) {
|
||||
case 'radio':
|
||||
pref.options.forEach( ( value ) => {
|
||||
appendRadioToggle( form, featureName, value, currentValue, config );
|
||||
} );
|
||||
break;
|
||||
case 'switch': {
|
||||
const labelElement = document.createElement( 'label' );
|
||||
labelElement.textContent = getFeatureLabelMsg( featureName ).text();
|
||||
appendToggleSwitch( form, featureName, labelElement, currentValue, config );
|
||||
break;
|
||||
} default:
|
||||
throw new Error( 'Unknown client preference! Only switch or radio are supported.' );
|
||||
}
|
||||
row.appendChild( form );
|
||||
return row;
|
||||
}
|
||||
|
@ -143,8 +226,7 @@ function makeClientPreferenceBinaryToggle( featureName, config ) {
|
|||
* @param {Record<string,ClientPreference>} config
|
||||
*/
|
||||
function makeClientPreference( parent, featureName, config ) {
|
||||
// eslint-disable-next-line mediawiki/msg-doc
|
||||
const labelMsg = mw.message( `${ featureName }-name` );
|
||||
const labelMsg = getFeatureLabelMsg( featureName );
|
||||
// If the user is not debugging messages and no language exists exit as its a hidden client preference.
|
||||
if ( !labelMsg.exists() && mw.config.get( 'wgUserLanguage' ) !== 'qqx' ) {
|
||||
return;
|
||||
|
@ -152,18 +234,18 @@ function makeClientPreference( parent, featureName, config ) {
|
|||
const id = `skin-client-prefs-${ featureName }`;
|
||||
// @ts-ignore TODO: upstream patch URL
|
||||
const portlet = mw.util.addPortlet( id, labelMsg.text() );
|
||||
const labelElement = portlet.querySelector( 'label' );
|
||||
// eslint-disable-next-line mediawiki/msg-doc
|
||||
const descriptionMsg = mw.message( `${ featureName }-description` );
|
||||
if ( descriptionMsg.exists() ) {
|
||||
const desc = document.createElement( 'div' );
|
||||
desc.classList.add( 'mw-portlet-description' );
|
||||
const desc = document.createElement( 'span' );
|
||||
desc.classList.add( 'skin-client-pref-description' );
|
||||
desc.textContent = descriptionMsg.text();
|
||||
const refNode = portlet.querySelector( 'label' );
|
||||
if ( refNode && refNode.parentNode ) {
|
||||
refNode.parentNode.insertBefore( desc, refNode.nextSibling );
|
||||
if ( labelElement && labelElement.parentNode ) {
|
||||
labelElement.appendChild( desc );
|
||||
}
|
||||
}
|
||||
const row = makeClientPreferenceBinaryToggle( featureName, config );
|
||||
const row = makeControl( featureName, config );
|
||||
parent.appendChild( portlet );
|
||||
if ( row ) {
|
||||
const tmp = mw.util.addPortletLink( id, '', '' );
|
||||
|
@ -190,13 +272,11 @@ function render( selector, config ) {
|
|||
return Promise.reject();
|
||||
}
|
||||
return new Promise( ( resolve ) => {
|
||||
mw.loader.using( 'codex-styles' ).then( () => {
|
||||
getVisibleClientPreferences( config ).forEach( ( pref ) => {
|
||||
makeClientPreference( node, pref, config );
|
||||
} );
|
||||
mw.requestIdleCallback( () => {
|
||||
resolve( node );
|
||||
} );
|
||||
getVisibleClientPreferences( config ).forEach( ( pref ) => {
|
||||
makeClientPreference( node, pref, config );
|
||||
} );
|
||||
mw.requestIdleCallback( () => {
|
||||
resolve( node );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`clientPreferences render empty 1`] = `""`;
|
||||
|
||||
exports[`clientPreferences render font size 1`] = `"<div><li><div class=\\"\\"><form><div class=\\"cdx-radio\\"><input name=\\"skin-client-pref-vector-feature-custom-font-size-group\\" id=\\"skin-client-pref-vector-feature-custom-font-size-value-0\\" type=\\"radio\\" value=\\"0\\" data-event-name=\\"skin-client-pref-vector-feature-custom-font-size-value-0\\" class=\\"cdx-radio__input\\"><span class=\\"cdx-radio__icon\\"></span><label for=\\"skin-client-pref-vector-feature-custom-font-size-value-0\\" class=\\"cdx-radio__label\\">vector-feature-custom-font-size-0-label</label></div><div class=\\"cdx-radio\\"><input name=\\"skin-client-pref-vector-feature-custom-font-size-group\\" id=\\"skin-client-pref-vector-feature-custom-font-size-value-1\\" type=\\"radio\\" value=\\"1\\" data-event-name=\\"skin-client-pref-vector-feature-custom-font-size-value-1\\" class=\\"cdx-radio__input\\"><span class=\\"cdx-radio__icon\\"></span><label for=\\"skin-client-pref-vector-feature-custom-font-size-value-1\\" class=\\"cdx-radio__label\\">vector-feature-custom-font-size-1-label</label></div><div class=\\"cdx-radio\\"><input name=\\"skin-client-pref-vector-feature-custom-font-size-group\\" id=\\"skin-client-pref-vector-feature-custom-font-size-value-2\\" type=\\"radio\\" value=\\"2\\" data-event-name=\\"skin-client-pref-vector-feature-custom-font-size-value-2\\" class=\\"cdx-radio__input\\"><span class=\\"cdx-radio__icon\\"></span><label for=\\"skin-client-pref-vector-feature-custom-font-size-value-2\\" class=\\"cdx-radio__label\\">vector-feature-custom-font-size-2-label</label></div></form></div></li></div>"`;
|
||||
|
||||
exports[`clientPreferences render toggle 1`] = `"<div><li><div class=\\"\\"><form><span class=\\"cdx-toggle-switch\\"><input name=\\"skin-client-pref-expandAll-group\\" id=\\"skin-client-pref-expandAll-value-1\\" type=\\"checkbox\\" data-event-name=\\"skin-client-pref-expandAll-value-1\\" class=\\"cdx-toggle-switch__input\\"><span class=\\"cdx-toggle-switch__switch\\"><span class=\\"cdx-toggle-switch__switch__grip\\"></span></span><label class=\\"cdx-toggle-switch__label\\">msg:expandAll-name</label></span></form></div></li></div>"`;
|
|
@ -0,0 +1,59 @@
|
|||
const clientPreferences = require( '../../resources/skins.vector.clientPreferences/clientPreferences.js' );
|
||||
|
||||
let cp;
|
||||
describe( 'clientPreferences', () => {
|
||||
beforeEach( () => {
|
||||
document.body.innerHTML = '';
|
||||
cp = document.createElement( 'div' );
|
||||
cp.id = 'cp';
|
||||
document.body.appendChild( cp );
|
||||
mw.requestIdleCallback = ( callback ) => callback();
|
||||
mw.user.clientPrefs = {
|
||||
get: jest.fn( () => '1' )
|
||||
};
|
||||
const portlet = document.createElement( 'div' );
|
||||
mw.util.addPortlet = jest.fn( () => portlet );
|
||||
mw.util.addPortletLink = jest.fn( () => {
|
||||
const li = document.createElement( 'li' );
|
||||
const a = document.createElement( 'a' );
|
||||
li.appendChild( a );
|
||||
portlet.appendChild( li );
|
||||
return li;
|
||||
} );
|
||||
mw.message = jest.fn( ( key ) => ( {
|
||||
text: () => `msg:${ key }`,
|
||||
exists: () => true
|
||||
} ) );
|
||||
} );
|
||||
|
||||
test( 'render empty', () => {
|
||||
return clientPreferences.render( '#cp', {} ).then( () => {
|
||||
expect( cp.innerHTML ).toMatchSnapshot();
|
||||
} );
|
||||
} );
|
||||
|
||||
test( 'render font size', () => {
|
||||
document.documentElement.setAttribute( 'class', 'vector-feature-custom-font-size-clientpref-2' );
|
||||
return clientPreferences.render( '#cp', {
|
||||
'vector-feature-custom-font-size': {
|
||||
options: [ '0', '1', '2' ],
|
||||
preferenceKey: 'vector-font-size'
|
||||
}
|
||||
} ).then( () => {
|
||||
expect( cp.innerHTML ).toMatchSnapshot();
|
||||
} );
|
||||
} );
|
||||
|
||||
test( 'render toggle', () => {
|
||||
document.documentElement.setAttribute( 'class', 'expandAll-clientpref-1' );
|
||||
return clientPreferences.render( '#cp', {
|
||||
expandAll: {
|
||||
options: [ '0', '1' ],
|
||||
preferenceKey: 'expandAll',
|
||||
type: 'switch'
|
||||
}
|
||||
} ).then( () => {
|
||||
expect( cp.innerHTML ).toMatchSnapshot();
|
||||
} );
|
||||
} );
|
||||
} );
|
正在加载...
在新工单中引用