User:Fred Gandt/aceEditorOptions.js
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
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;
}
#wpTextbox1.mw-textarea-protected + .ui-resizable .ace_content { background-color: unset !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;
}
#fgAceEditorOptionsForm > fieldset {
padding-bottom: 0.5em;
border-radius: .2em;
margin: 0.2em 0;
}
#fgAceEditorOptionsForm.hide { display: none }
#fgAceEditorOptionsForm label { display: block }
#fgAceEditorOptionsForm label input { margin-left: 0.4em }
#fgAceEditorOptionsForm > label + fieldset { margin-top: 0 }
#fgAceEditorOptionsForm > fieldset > legend { padding: 0 .4em .3em }
#fgAceEditorOptionsForm > label, #fgAceEditorOptionsForm > input { margin-top: 0.3em }
#fgAceEditorOptionsForm > label[for], #fgAceEditorOptionsForm > select > optgroup { text-transform: capitalize }
#fgAceEditorOptionsForm > label > input[type="color"] { vertical-align: middle }
#fgAceEditorOptionsForm > label > input[type="number"] { width: 8ch }
#fgAceEditorOptionsForm > label > input[type="text"] { width: 20ch }
#fgAceEditorOptionsForm > input[type="button"] {
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" );
}
} );