mediawiki-skins-Citizen/resources/skins.citizen.preferences/skins.citizen.preferences.js
2023-07-07 18:00:14 -04:00

410 lines
11 KiB
JavaScript

/* global applyPref */
/**
* TODO: Maybe combine the localStorage keys into one object
*/
const
CLASS = 'citizen-pref',
PREFIX_KEY = 'skin-citizen-';
/**
* Set the value of the input element
*
* @param {string} key
* @param {string} value
*/
function setInputValue( key, value ) {
const element = document.getElementById( CLASS + '-' + key + '__input' );
if ( element ) {
element.value = value;
}
}
/**
* Set the text of the indicator element
*
* @param {string} key
* @param {string} value
*/
function setIndicator( key, value ) {
const element = document.getElementById( CLASS + '-' + key + '__value' );
if ( element ) {
element.innerText = value;
}
}
/**
* Convert the pref values for use with the form input
*
* @param {Object} pref
* @return {Object}
*/
function convertForForm( pref ) {
return {
theme: pref.theme,
fontsize: Number( pref.fontsize.slice( 0, -1 ) ) / 10 - 8,
pagewidth: Number( pref.pagewidth.slice( 0, -2 ) ) / 120 - 6,
lineheight: ( pref.lineheight - 1 ) * 10
};
}
/**
* Retrieve localstorage or default preferences
*
* @return {Object} pref
*/
function getPref() {
const rootStyle = window.getComputedStyle( document.documentElement );
// Create an initial value for font size
const initFontSize = () => {
// Get browser default font size
const getDefaultFontSize = () => {
const element = document.createElement( 'div' );
element.style.width = '1rem';
element.style.display = 'none';
document.body.append( element );
const widthMatch = window
.getComputedStyle( element )
.getPropertyValue( 'width' )
.match( /\d+/ );
element.remove();
if ( !widthMatch || widthMatch.length < 1 ) {
return null;
}
const result = Number( widthMatch[ 0 ] );
return !isNaN( result ) ? result : null;
};
const browserFontSize = getDefaultFontSize() + 'px',
rootFontSize = rootStyle.getPropertyValue( 'font-size' );
// If browser font size is same as HTML font size, return 100%
// Some wiki might have custom font size defined in root/HTML,
// in that case return the defined value
return ( ( browserFontSize === rootFontSize ) ? '100%' : rootFontSize );
};
const pref = {
theme: localStorage.getItem( PREFIX_KEY + 'theme' ),
fontsize: localStorage.getItem( PREFIX_KEY + 'fontsize' ) || initFontSize(),
pagewidth: localStorage.getItem( PREFIX_KEY + 'pagewidth' ) || rootStyle.getPropertyValue( '--width-layout' ),
lineheight: localStorage.getItem( PREFIX_KEY + 'lineheight' ) || rootStyle.getPropertyValue( '--line-height' )
};
return pref;
}
/**
* Save to localstorage if preference is changed
*
* @return {void}
*/
function setPref() {
const
// eslint-disable-next-line compat/compat
formData = Object.fromEntries( new FormData( document.getElementById( CLASS + '-form' ) ) ),
currentPref = convertForForm( getPref() ),
newPref = {
theme: formData[ CLASS + '-theme' ],
fontsize: Number( formData[ CLASS + '-fontsize' ] ),
pagewidth: Number( formData[ CLASS + '-pagewidth' ] ),
lineheight: Number( formData[ CLASS + '-lineheight' ] )
};
if ( currentPref.theme !== newPref.theme ) {
localStorage.setItem( PREFIX_KEY + 'theme', newPref.theme );
} else if ( currentPref.fontsize !== newPref.fontsize ) {
const formattedFontSize = ( newPref.fontsize + 8 ) * 10 + '%';
localStorage.setItem( PREFIX_KEY + 'fontsize', formattedFontSize );
setIndicator( 'fontsize', formattedFontSize );
} else if ( currentPref.pagewidth !== newPref.pagewidth ) {
let formattedPageWidth;
// Max setting would be full browser width
if ( newPref.pagewidth === 10 ) {
formattedPageWidth = '100vw';
} else {
formattedPageWidth = ( newPref.pagewidth + 6 ) * 120 + 'px';
}
localStorage.setItem( PREFIX_KEY + 'pagewidth', formattedPageWidth );
setIndicator( 'pagewidth', formattedPageWidth );
} else if ( currentPref.lineheight !== newPref.lineheight ) {
const formattedLineHeight = newPref.lineheight / 10 + 1;
localStorage.setItem( PREFIX_KEY + 'lineheight', formattedLineHeight );
setIndicator( 'lineheight', formattedLineHeight );
}
applyPref();
}
/**
* Reset preference by clearing localStorage and inline styles
*
* @return {void}
*/
function resetPref() {
// Do not reset theme as its default value is defined somewhere else
const keys = [ 'fontsize', 'pagewidth', 'lineheight' ];
// Remove style
if ( document.getElementById( 'citizen-style' ) ) {
document.getElementById( 'citizen-style' ).remove();
}
// Remove localStorage
keys.forEach( ( key ) => {
const keyName = PREFIX_KEY + key;
if ( localStorage.getItem( keyName ) ) {
localStorage.removeItem( keyName );
}
} );
const pref = getPref(),
prefValue = convertForForm( pref );
keys.forEach( ( key ) => {
const keyName = PREFIX_KEY + key;
localStorage.setItem( keyName, pref[ key ] );
setIndicator( key, pref[ key ] );
setInputValue( key, prefValue[ key ] );
} );
applyPref();
}
/**
* Dismiss the prefernce panel when clicked outside
*
* @param {Event} event
*/
function dismissOnClickOutside( event ) {
const pref = document.getElementById( CLASS );
if ( event.target instanceof Node && !pref.contains( event.target ) ) {
const panel = document.getElementById( CLASS + '-panel' );
if ( panel.classList.contains( CLASS + '-panel--active' ) ) {
togglePanel();
}
}
}
/**
* Dismiss the prefernce panel when ESCAPE is pressed
*
* @param {Event} event
*/
function dismissOnEscape( event ) {
if ( event.key !== 'Escape' ) {
return;
}
togglePanel();
}
/**
* Add/remove toggle class and form input eventlistener
*
* @return {void}
*/
function togglePanel() {
// .citizen-pref-panel--active
const CLASS_PANEL_ACTIVE = CLASS + '-panel--active';
const
toggle = document.getElementById( CLASS + '-toggle' ),
panel = document.getElementById( CLASS + '-panel' ),
form = document.getElementById( CLASS + '-form' ),
themeOption = document.getElementById( CLASS + '-theme' ),
resetButton = document.getElementById( CLASS + '-resetbutton' );
if ( !panel.classList.contains( CLASS_PANEL_ACTIVE ) ) {
panel.classList.add( CLASS_PANEL_ACTIVE );
toggle.setAttribute( 'aria-expanded', true );
form.addEventListener( 'input', setPref );
// Some browser doesn't fire input events when checking radio buttons
themeOption.addEventListener( 'click', setPref );
resetButton.addEventListener( 'click', resetPref );
window.addEventListener( 'click', dismissOnClickOutside );
window.addEventListener( 'keydown', dismissOnEscape );
} else {
panel.classList.remove( CLASS_PANEL_ACTIVE );
toggle.setAttribute( 'aria-expanded', false );
form.removeEventListener( 'input', setPref );
themeOption.removeEventListener( 'click', setPref );
resetButton.removeEventListener( 'click', resetPref );
window.removeEventListener( 'click', dismissOnClickOutside );
window.removeEventListener( 'keydown', dismissOnEscape );
}
}
/**
* Get MW message and return as object to be used in Mustache
*
* @return {Object}
*/
function getMessages() {
const keys = [
'preferences',
'prefs-citizen-theme-label',
'prefs-citizen-theme-option-auto',
'prefs-citizen-theme-option-light',
'prefs-citizen-theme-option-dark',
'prefs-citizen-fontsize-label',
'prefs-citizen-pagewidth-label',
'prefs-citizen-lineheight-label',
'prefs-citizen-resetbutton-label'
],
data = {};
keys.forEach( ( key ) => {
const templateKey = 'msg-' + key;
// Message keys already defined above
// eslint-disable-next-line mediawiki/msg-doc
data[ templateKey ] = mw.message( key ).text();
} );
return data;
}
/**
* Set up the DOM and initial input states for the panel
* It only loads when user first clicked the toggle
*
* @param {Event} event
* @return {void}
*/
function initPanel( event ) {
const template = mw.template.get(
'skins.citizen.preferences',
'resources/skins.citizen.preferences/templates/preferences.mustache'
),
data = getMessages(),
pref = getPref(),
prefValue = convertForForm( pref ),
keys = [ 'fontsize', 'pagewidth', 'lineheight' ];
// To Mustache is to jQuery sigh
// TODO: Use ES6 template literals when RL does not screw up multiline
const panel = template.render( data ).get()[ 1 ];
// The priorities is as follow:
// 1. User-set theme (localStorage)
// 2. Site default theme (wgCitizenThemeDefault)
// 3. Fallback to auto
const currentTheme = prefValue.theme ||
require( './config.json' ).wgCitizenThemeDefault ||
'auto';
// Attach panel after button
event.currentTarget.parentNode.insertBefore( panel, event.currentTarget.nextSibling );
// Set up initial state
document.getElementById( CLASS + '-theme__input__' + currentTheme ).checked = true;
keys.forEach( ( key ) => {
setIndicator( key, pref[ key ] );
setInputValue( key, prefValue[ key ] );
} );
togglePanel();
event.currentTarget.addEventListener( 'click', togglePanel );
event.currentTarget.removeEventListener( 'click', initPanel );
}
/**
* Test if storage is avaliable
* Taken from https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API
*
* @param {string} type
* @return {boolean|Error}
*/
function storageAvailable( type ) {
let storage;
try {
storage = window[ type ];
const x = '__storage_test__';
storage.setItem( x, x );
storage.removeItem( x );
return true;
} catch ( /** @type {Error} */ e ) {
return e instanceof DOMException && (
// everything except Firefox
e.code === 22 ||
// Firefox
e.code === 1014 ||
// test name field too, because code might not be present
// everything except Firefox
e.name === 'QuotaExceededError' ||
// Firefox
e.name === 'NS_ERROR_DOM_QUOTA_REACHED' ) &&
// acknowledge QuotaExceededError only if there's something already stored
( storage && storage.length !== 0 );
}
}
/**
* Set up the container and toggle
*
* @param {Window} window
* @return {void}
*/
function initPref( window ) {
// Object.fromEntries() polyfill https://github.com/feross/fromentries
// MIT. Copyright (c) Feross Aboukhadijeh.
if ( typeof Object.fromEntries !== 'function' ) {
Object.defineProperty( Object, 'fromEntries', {
value( iterable ) {
return Array.from( iterable ).reduce( ( obj, [ key, val ] ) => {
obj[ key ] = val;
return obj;
}, {} );
}
} );
}
if ( storageAvailable( 'localStorage' ) ) {
if ( typeof window.mw !== 'undefined' ) {
const headerTools = document.querySelector( '.citizen-header__end' ),
container = document.createElement( 'div' ),
button = document.createElement( 'button' ),
icon = document.createElement( 'span' );
// citizen-pref
container.id = CLASS;
container.classList.add( CLASS, 'citizen-header__item' );
button.id = CLASS + '-toggle';
button.classList.add( CLASS + '__button', 'citizen-header__button' );
button.setAttribute( 'title', mw.message( 'preferences' ).text() );
button.setAttribute( 'aria-label', mw.message( 'preferences' ).text() );
button.setAttribute( 'aria-controls', CLASS + '-panel' );
button.setAttribute( 'aria-expanded', false );
icon.classList.add( 'citizen-ui-icon', 'mw-ui-icon-wikimedia-settings' );
button.prepend( icon );
container.prepend( button );
headerTools.prepend( container );
button.addEventListener( 'click', initPanel, { once: true } );
}
} else {
// eslint-disable-next-line no-console
console.log( 'Preference module is disabled due to localStoarge being not avaliable.' );
}
}
initPref( window );