User:Fred Gandt/aceEditorOptions.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
Documentation for this user script can be added at User:Fred Gandt/aceEditorOptions. |
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" );
}
} );