User:Fred Gandt/aceEditorOptions.js

var fg_aceEditorOptions_debugging = false; /* NOTE: available if needed */

// TODO: have it initialize on Modules during preview

$( document ).ready( => {	"use strict";	// TODO: figure out why and fix very rare non existence of ace	if ( mw.config.get( "wgAction" ) === "edit" && window.hasOwnProperty( "ace" ) ) {		let changed_options = {},				ace_default_options,				ace_editor;		const USER_NAME = mw.config.get( "wgUserName" ),					OPTIONS_FORM = document.createElement( "form" ),					USER_OPTIONS_NAME = "userjs-fg-ace-editor-options",					WIKIEDITOR_TEXT = document.querySelector( "#editform .wikiEditor-ui-text" ),					USER_OPTIONS = JSON.parse( mw.user.options.values[ USER_OPTIONS_NAME ] || {} ),					DEFAULT_OPTIONS = {},					BUILT_OPTIONS = {},					STYLES = {},			debugMsg = ( msg, force_type ) => {				if ( fg_aceEditorOptions_debugging || force_type ) {					console[ force_type || "log" ]( "AEO", msg );				}			},			errorNotification = ( specifics, console_object ) => {				debugMsg( console_object, "error" );				mw.notify( `${specifics}; take a look at your browser's console [ctrl+shift+j] for some possibly helpful information`, { tag: "aceEditorOptions", type: "error", autoHide: false } ); },			api = ( dt, fnc ) => { dt.format = "json"; $.ajax( {					type: "POST",					dataType: dt.format,					url: "/w/api.php",					data: dt,					success: data => fnc( data ),					error: ( type, status, thrown ) => errorNotification( "HTTP request error", { "api": { "dt": dt, "fnc": fnc, "error": { "type": type, "status": status, "thrown": thrown } } } )				} ); },			unsavedChanges = are_there_any => { const UC = Object.entries( changed_options ).filter( ( [ key, val ] ) => BUILT_OPTIONS[ key ] !== val ); debugMsg( { "unsavedChanges": { "UC": UC, "are_there_any": are_there_any } } ); return are_there_any ? !!UC.length : Object.fromEntries( UO ); },			userOptions = objectified => { const UOA = Object.entries( Object.assign( {}, BUILT_OPTIONS, changed_options ) ).filter( ( [ key, val ] ) => DEFAULT_OPTIONS[ key ] !== val ); debugMsg( { "userOptions": { "UOA": UOA } } ); return objectified ? Object.fromEntries( UOA ) : UOA; },			saveUserOptions = resetting => { let options = {}; if ( !resetting ) { options = userOptions( true ); }				api( {					action: "options",					optionname: USER_OPTIONS_NAME,					optionvalue: JSON.stringify( options ),					token: mw.user.tokens.values.csrfToken				}, data => {					if ( data.options && data.options === "success" ) {						OPTIONS_FORM.classList.add( "hide" );						SETTINGS.setLabel( "Ace editor options" ).setFlags( { destructive: false } );						mw.notify( "Ace editor options settings saved", { tag: "aceEditorOptions", type: "success" } );						if ( resetting ) {							changed_options = {};							setUserOptions( userOptions.reduce( ( result, [ key, val ] ) => {								const INPUT = OPTIONS_FORM[ key ];								result[ key ] = INPUT[ INPUT.type === "checkbox" ? "checked" : "value" ] = DEFAULT_OPTIONS[ key ];								return result;							}, {} ) );						}					} else {						errorNotification( "Failure to save Ace editor options settings", { "saveUserOptions": { "resetting": resetting, "options": options, "data": data } } ); }				} );			},			handleFGStyleSheets = ( name, value, text ) => {				debugMsg( { "handleFGStyleSheets": { "name": name, "value": value, "text": text } } );				let style_sheet = STYLES[ name ];				if ( !style_sheet ) {					style_sheet = new CSSStyleSheet;					STYLES[ name ] = style_sheet;					document.adoptedStyleSheets = [ ...document.adoptedStyleSheets, style_sheet ];				}				style_sheet.disabled = !value;				if ( value && text ) {					style_sheet.replaceSync( text );				}			},			setUserOptions = options => {				debugMsg( { "setUserOptions": { "options": options } } );				Object.entries( options ).forEach( ( [ key, val ] ) => { debugMsg( { "setUserOptions": { "key": key, "val": val } } ); if ( /^fg_/.test( key ) ) { /* NOTE: it's all very important */ switch ( key ) { case "fg_pinkProtectedPages": { handleFGStyleSheets( key, !val, `#wpTextbox1.mw-textarea-protected + .ui-resizable {	border-width: 1em 0 1em 1em !important;	border-color: #c14848 !important;	border-style: solid !important; }								break;							}							case "fg_containEditorOverscroll": { /* TODO: something less janky */								handleFGStyleSheets( key, val, "body { overflow: hidden !important; }" );								break;							}							case "fg_hidePageNotices": {								handleFGStyleSheets( key, val, "#mw-content-text > div:not( #wikiPreview, #wikiDiff, .printfooter ) { display: none !important; }" );								break;							}							case "fg_hidePrintMargin": {								handleFGStyleSheets( key, val, ".ace_editor .ace_print-margin { background-color: transparent !important; }" );								break;							}							case "fg_selectedWordBorderColor": {								handleFGStyleSheets( key, true, `.ace_editor .ace_selected-word { border-color: ${val} !important; }` ); break; }							case "fg_selectionColor": { handleFGStyleSheets( key, true, `.ace_editor .ace_selection { background-color: ${val} !important; }` ); break; }						}					} else { ace_editor.setOption( key, val ); }				} );			},			appendFormButton = ( value, fnc ) => {				const INPUT = document.createElement( "input" );				INPUT.type = "button";				INPUT.value = value;				INPUT.addEventListener( "click", fnc, { passive: true } );				OPTIONS_FORM.append( INPUT );			},			labelledInput = ( option_name, input_object, option_default, option_value ) => {				debugMsg( { "labelledInput": { "option_name": option_name, "input_object": input_object, "option_default": option_default, "option_value": option_value } } );				const INPUT = document.createElement( "input" ),							LABEL = document.createElement( "label" ),							ATTRIBUTES = input_object.attributes;				INPUT.name = option_name; /* NOTE: allowed to be overwritten */				Object.entries( ATTRIBUTES ).forEach( ( [ key, val ] ) => INPUT[ key ] = val );				if ( option_default === "fg_" ) {					DEFAULT_OPTIONS[ INPUT.name ] = ATTRIBUTES.checked ?? ATTRIBUTES.value; }				if ( option_value === "fg_" ) { BUILT_OPTIONS[ INPUT.name ] = ATTRIBUTES.checked ?? ATTRIBUTES.value; } else { if ( INPUT.type === "radio" ) { if ( INPUT.checked = ATTRIBUTES.value === ( option_value ?? "" ) ) {							BUILT_OPTIONS[ INPUT.name ] = option_value; }					} else { if ( INPUT.type === "checkbox" ) { INPUT.checked = option_value; } else { INPUT.value = option_value; }						BUILT_OPTIONS[ INPUT.name ] = option_value; }				}				LABEL.textContent = input_object.label; LABEL.append( INPUT ); return LABEL; },			optionsForm = => { if ( OPTIONS_FORM.id ) { const UC = unsavedChanges( "?" ); OPTIONS_FORM.classList.toggle( "hide" ); SETTINGS.setLabel( UC ? "Unsaved changes" : "Ace editor options" ).setFlags( { destructive: UC } ); } else { api( {						action: "query",						prop: "revisions",						rvprop: "content",						rvslots: "main",						titles: "User:Fred Gandt/aceEditorOptions.json"					}, data => {						if ( data.hasOwnProperty( "batchcomplete" ) ) {							const CONFIG = JSON.parse( data.query.pages[ Object.keys( data.query.pages )[ 0 ] ].revisions[ 0 ].slots.main[ "*" ] );							if ( CONFIG ) {								/* NOTE: so much slicker than loading from source */								handleFGStyleSheets( "fg_aceEditorOptionsForm", true, `#fgAceEditorOptionsForm { border-radius: 0.2em 0px 0px 0.2em; height: calc(100% - 1px - 10.8em); overscroll-behavior: contain; contain: layout style paint; border: 1px solid #a7d7f9; background-color: white; position: absolute; overflow: auto; font-size: 85%; padding: 1em; right: 1.5em; top: 4.1em; }	padding-bottom: 0.5em; border-radius: .2em; margin: 0.2em 0; }	margin-top: .7em; cursor: pointer; display: block; }` );								OPTIONS_FORM.id = "fgAceEditorOptionsForm";								CONFIG.build.forEach( option_name => { const OPTION_DEFAULT = /^fg_/.test( option_name ) ? "fg_" : ( ace_default_options[ option_name ] ), OPTION_VALUE = USER_OPTIONS[ option_name ] ?? OPTION_DEFAULT, CONFIG_OPTION = CONFIG.options[ option_name ], CONFIG_OPTION_TYPE = CONFIG_OPTION.type; if ( CONFIG_OPTION_TYPE ) { if ( CONFIG_OPTION_TYPE === "select" ) { const SELECT = document.createElement( "select" ), LABEL = document.createElement( "label" ); LABEL.setAttribute( "for", SELECT.id = `${OPTIONS_FORM.id}-${option_name}` ); LABEL.textContent = SELECT.name = option_name; OPTIONS_FORM.append( LABEL ); CONFIG_OPTION.optgroups.forEach( group => {												const OPTGROUP = document.createElement( "optgroup" );												OPTGROUP.label = group.label;												group.options.forEach( groupie => { const OPTION = document.createElement( "option" ); OPTION.textContent = groupie.label; OPTION.selected = ( OPTION.value = groupie.value ) === OPTION_VALUE; OPTGROUP.append( OPTION ); } );												SELECT.append( OPTGROUP );											} ); OPTIONS_FORM.append( SELECT ); BUILT_OPTIONS[ option_name ] = OPTION_VALUE; } else if ( CONFIG_OPTION_TYPE === "fieldset" ) { const FIELDSET = document.createElement( "fieldset" ), LEGEND = document.createElement( "legend" ); LEGEND.textContent = CONFIG_OPTION.legend; FIELDSET.name = option_name; FIELDSET.append( LEGEND ); CONFIG_OPTION.members.forEach( member => FIELDSET.append( labelledInput( option_name, member, OPTION_DEFAULT, OPTION_VALUE ) ) ); OPTIONS_FORM.append( FIELDSET ); }									} else { OPTIONS_FORM.append( labelledInput( option_name, CONFIG_OPTION, OPTION_DEFAULT, OPTION_VALUE ) ); }								} );								Object.assign( DEFAULT_OPTIONS, ace_default_options );								debugMsg( { "optionsForm": { "BUILT_OPTIONS": BUILT_OPTIONS, "DEFAULT_OPTIONS": DEFAULT_OPTIONS } } );								OPTIONS_FORM.addEventListener( "input", evt => { const TARGET = evt.target, TYPE = TARGET.type; let value = TARGET.value, name = TARGET.name; if ( TYPE === "checkbox" ) { value = TARGET.checked; } else if ( !isNaN( +value ) ) { value = +value; }									changed_options[ name ] = value; setUserOptions( { [ name ]: value } ); } );								appendFormButton( "Save these options", => saveUserOptions );								appendFormButton( "Reset to default",  => saveUserOptions( true ) );								WIKIEDITOR_TEXT.append( OPTIONS_FORM );							}						}					} ); }			},			initAEO = => { const ACE_EDITOR_CONTAINER = WIKIEDITOR_TEXT.querySelector( "div.editor.ace_editor" ); if ( ACE_EDITOR_CONTAINER ) { ace_editor = ace.edit( ACE_EDITOR_CONTAINER ); ace_default_options = ace_editor.getOptions; setUserOptions( USER_OPTIONS ); debugMsg( { "initAEO": { "ace_default_options": ace_default_options, "ace_editor": ace_editor } }, "log" ); }			},			SETTINGS = new OO.ui.ToggleButtonWidget( { label: "Ace editor options", icon: "settings", framed: false } ), OBSERVER = new MutationObserver( mutants => {				const ADDED_NODE = mutants[ 0 ].addedNodes[ 0 ];				if ( ADDED_NODE?.classList.contains( "ui-resizable" ) ) {					SETTINGS.setDisabled( false );					initAEO;				} else if ( ADDED_NODE !== OPTIONS_FORM ) {					OPTIONS_FORM.classList?.add( "hide" ); /* TODO: unsaved changes indicator color gets switched if the form is open when toggling away and back */					SETTINGS.setDisabled( true );				}			} ); initAEO; SETTINGS.onChange = optionsForm; SETTINGS.on( "change", SETTINGS.onChange ); document.querySelector( '#wikiEditor-section-main span[rel="lineWrapping"]' )?.remove; /* NOTE: removing as potentially conflicted */ document.querySelector( '#wikiEditor-section-main span[rel="invisibleChars"]' )?.remove; /* NOTE: removing as potentially conflicted */ $( "#wikiEditor-section-secondary > div" ).removeClass( "empty" ).append( SETTINGS.$element ); OBSERVER.observe( WIKIEDITOR_TEXT, { childList: true } ); debugMsg( { "USER_OPTIONS": USER_OPTIONS }, "log" ); } } );
 * 1) wpTextbox1.mw-textarea-protected + .ui-resizable .ace_content { background-color: unset !important }` );
 * 1) fgAceEditorOptionsForm > fieldset {
 * 1) fgAceEditorOptionsForm.hide { display: none }
 * 2) fgAceEditorOptionsForm label { display: block }
 * 3) fgAceEditorOptionsForm label input { margin-left: 0.4em }
 * 4) fgAceEditorOptionsForm > label + fieldset { margin-top: 0 }
 * 5) fgAceEditorOptionsForm > fieldset > legend { padding: 0 .4em .3em }
 * 6) fgAceEditorOptionsForm > label, #fgAceEditorOptionsForm > input { margin-top: 0.3em }
 * 7) fgAceEditorOptionsForm > label[for], #fgAceEditorOptionsForm > select > optgroup { text-transform: capitalize }
 * 8) fgAceEditorOptionsForm > label > input[type="color"] { vertical-align: middle }
 * 9) fgAceEditorOptionsForm > label > input[type="number"] { width: 8ch }
 * 10) fgAceEditorOptionsForm > label > input[type="text"] { width: 20ch }
 * 11) fgAceEditorOptionsForm > input[type="button"] {