Skip to content

Commit

Permalink
feat(preferences): ✨ implement a localStorage version of mw.user.clie…
Browse files Browse the repository at this point in the history
…ntPrefs

This is the first step of migrating to the clientPrefs library when it is avaliable.
Currently only themes are using it and the standard classes are added to the HTML element.

Related: #780
  • Loading branch information
alistair3149 committed Apr 24, 2024
1 parent 59aa0a9 commit ed226a4
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 1 deletion.
116 changes: 116 additions & 0 deletions resources/skins.citizen.preferences/clientPrefs.localStorage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* mw.user.clientPrefs modified to only use localStorage
* TODO: Revisit when we move to MW 1.43 and the interface is more stable
*/

const CLIENTPREF_STORAGE_NAME = 'mwclientpreferences';
const CLIENTPREF_SUFFIX = '-clientpref-';
const CLIENTPREF_DELIMITER = ',';

/**
* Check if the feature name is composed of valid characters.
*
* A valid feature name may contain letters, numbers, and "-" characters.
*
* @private
* @param {string} value
* @return {boolean}
*/
function isValidFeatureName( value ) {
return value.match( /^[a-zA-Z0-9-]+$/ ) !== null;
}

/**
* Check if the value is composed of valid characters.
*
* @private
* @param {string} value
* @return {boolean}
*/
function isValidFeatureValue( value ) {
return value.match( /^[a-zA-Z0-9]+$/ ) !== null;
}

/**
* Save the feature value to the client preferences localStorage.
* Modified from the original to use localStorage instead of cookie.
*
* @private
* @param {string} feature
* @param {string} value
*/
function saveClientPrefs( feature, value ) {
const existingStorage = mw.storage.get( CLIENTPREF_STORAGE_NAME ) || '';
const data = {};
existingStorage.split( CLIENTPREF_DELIMITER ).forEach( function ( keyValuePair ) {
const m = keyValuePair.match( /^([\w-]+)-clientpref-(\w+)$/ );
if ( m ) {
data[ m[ 1 ] ] = m[ 2 ];
}
} );
data[ feature ] = value;

const newStorage = Object.keys( data ).map( function ( key ) {
return key + CLIENTPREF_SUFFIX + data[ key ];
} ).join( CLIENTPREF_DELIMITER );
mw.storage.set( CLIENTPREF_STORAGE_NAME, newStorage );
}

function clientPrefs() {
return {
/**
* Change the class on the HTML document element, and save the value in a localStorage.
*
* @memberof mw.user.clientPrefs
* @param {string} feature
* @param {string} value
* @return {boolean} True if feature was stored successfully, false if the value
* uses a forbidden character or the feature is not recognised
* e.g. a matching class was not defined on the HTML document element.
*/
set: function ( feature, value ) {
if ( !isValidFeatureName( feature ) || !isValidFeatureValue( value ) ) {
return false;
}
const currentValue = this.get( feature );

const oldFeatureClass = feature + CLIENTPREF_SUFFIX + currentValue;
const newFeatureClass = feature + CLIENTPREF_SUFFIX + value;
// The following classes are removed here:
// * feature-name-clientpref-<old-feature-value>
// * e.g. vector-font-size--clientpref-small
document.documentElement.classList.remove( oldFeatureClass );
// The following classes are added here:
// * feature-name-clientpref-<feature-value>
// * e.g. vector-font-size--clientpref-xlarge
document.documentElement.classList.add( newFeatureClass );
saveClientPrefs( feature, value );
return true;
},

/**
* Retrieve the current value of the feature from the HTML document element.
*
* @memberof mw.user.clientPrefs
* @param {string} feature
* @return {string|boolean} returns boolean if the feature is not recognized
* returns string if a feature was found.
*/
get: function ( feature ) {
const featurePrefix = feature + CLIENTPREF_SUFFIX;
const docClass = document.documentElement.className;
// eslint-disable-next-line security/detect-non-literal-regexp
const featureRegEx = new RegExp(
'(^| )' + mw.util.escapeRegExp( featurePrefix ) + '([a-zA-Z0-9]+)( |$)'
);
const match = docClass.match( featureRegEx );

// check no further matches if we replaced this occurance.
const isAmbiguous = docClass.replace( featureRegEx, '$1$3' ).match( featureRegEx ) !== null;
return !isAmbiguous && match ? match[ 2 ] : false;
}
};
}

/** @module clientPrefs */
module.exports = clientPrefs;
13 changes: 13 additions & 0 deletions resources/skins.citizen.preferences/skins.citizen.preferences.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ const
CLASS = 'citizen-pref',
PREFIX_KEY = 'skin-citizen-';

/**
* Clientprefs names theme differently from Citizen, we will need to translate it
* TODO: Migrate to clientprefs fully on MW 1.43
*/
const CLIENTPREFS_THEME_MAP = {
auto: 'os',
light: 'day',
dark: 'night'
};

const clientPrefs = require( './clientPrefs.localStorage.js' )();

/**
* Set the value of the input element
*
Expand Down Expand Up @@ -121,6 +133,7 @@ function setPref() {

if ( currentPref.theme !== newPref.theme ) {
localStorage.setItem( PREFIX_KEY + 'theme', newPref.theme );
clientPrefs.set( 'skin-theme', CLIENTPREFS_THEME_MAP[ newPref.theme ] );

} else if ( currentPref.fontsize !== newPref.fontsize ) {
const formattedFontSize = ( newPref.fontsize + 8 ) * 10 + '%';
Expand Down
7 changes: 6 additions & 1 deletion skin.json
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,8 @@
{
"name": "resources/skins.citizen.preferences/config.json",
"callback": "MediaWiki\\Skins\\Citizen\\Hooks\\ResourceLoaderHooks::getCitizenPreferencesResourceLoaderConfig"
}
},
"resources/skins.citizen.preferences/clientPrefs.localStorage.js"
],
"messages": [
"preferences",
Expand All @@ -238,6 +239,10 @@
"prefs-citizen-lineheight-label",
"prefs-citizen-resetbutton-label"
],
"dependencies": [
"mediawiki.storage",
"mediawiki.util"
],
"targets": [
"desktop",
"mobile"
Expand Down

0 comments on commit ed226a4

Please sign in to comment.