MediaWiki:BlueprintLoader.js: Difference between revisions
Created page with "/** * EQL Wiki - Dynamic Page Blueprint Loader * * On the source editor for a page that does not yet exist: * * 1. Reads blueprint headings dynamically from Help:Contents. * 2. Displays a blueprint selector above the normal source editor. * 3. Fetches the selected Help:Contents section on demand. * 4. Extracts the first <pre>...</pre> block from that section. * 5. Loads that exact wikitext into #wpTextbox1. * * Help:Contents remains the single source of truth...." |
Refresh Icon Finder static-cache client |
||
| (5 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
/** | /** | ||
* EQL Wiki - Dynamic | * EQL Wiki - Editor Tools / Dynamic Blueprint Loader | ||
* | * | ||
* | * Source-editor behavior: | ||
* | * - On a new, truly empty page: show the Help:Contents Blueprint selector. | ||
* - On every source edit page: show the Icon Finder button. | |||
* | * - Heavy Icon Finder logic is lazy-loaded only when opened. | ||
* | |||
*/ | */ | ||
( function ( mw, $ ) { | ( function ( mw, $ ) { | ||
| Line 16: | Line 11: | ||
var BLUEPRINT_PAGE = 'Help:Contents'; | var BLUEPRINT_PAGE = 'Help:Contents'; | ||
var | var TOOLS_ID = 'eql-editor-tools'; | ||
var ICON_FINDER_PAGE = 'MediaWiki:IconFinder.js'; | |||
function htmlToText( html ) { | function htmlToText( html ) { | ||
var div = document.createElement( 'div' ); | var div = document.createElement( 'div' ); | ||
div.innerHTML = String( html || '' ); | div.innerHTML = String( html || '' ); | ||
return ( div.textContent || div.innerText || '' ).trim(); | return ( div.textContent || div.innerText || '' ).trim(); | ||
} | } | ||
function makeLabel( heading ) { | function makeLabel( heading ) { | ||
return heading | return heading | ||
| Line 47: | Line 26: | ||
} | } | ||
function getEditorContents( $textarea ) { | function getEditorContents( $textarea ) { | ||
try { | try { | ||
return String( | return String( $textarea.textSelection( 'getContents' ) || '' ); | ||
} catch ( e ) { | } catch ( e ) { | ||
return String( $textarea.val() || '' ); | return String( $textarea.val() || '' ); | ||
| Line 63: | Line 34: | ||
} | } | ||
function setEditorContents( $textarea, text ) { | function setEditorContents( $textarea, text ) { | ||
try { | try { | ||
$textarea.textSelection( | $textarea.textSelection( 'setContents', text ); | ||
} catch ( e ) { | } catch ( e ) { | ||
$textarea.val( text ); | $textarea.val( text ); | ||
| Line 84: | Line 46: | ||
} | } | ||
function extractBlueprint( sectionText ) { | function extractBlueprint( sectionText ) { | ||
var match; | var match; | ||
| Line 112: | Line 62: | ||
blueprint = match[ 1 ]; | blueprint = match[ 1 ]; | ||
blueprint = blueprint.replace( | blueprint = blueprint.replace( | ||
/^\s*<nowiki>([\s\S]*?)<\/nowiki>\s*$/i, | /^\s*<nowiki>([\s\S]*?)<\/nowiki>\s*$/i, | ||
| Line 121: | Line 67: | ||
); | ); | ||
blueprint = blueprint | blueprint = blueprint | ||
.replace( /^\s*\n/, '' ) | .replace( /^\s*\n/, '' ) | ||
| Line 132: | Line 74: | ||
} | } | ||
function fetchBlueprintSections( api ) { | function fetchBlueprintSections( api ) { | ||
return api.get( { | return api.get( { | ||
| Line 170: | Line 105: | ||
fallbackData && | fallbackData && | ||
fallbackData.parse && | fallbackData.parse && | ||
Array.isArray( | Array.isArray( fallbackData.parse.sections ) | ||
) | ) | ||
? fallbackData.parse.sections | ? fallbackData.parse.sections | ||
| Line 180: | Line 113: | ||
} | } | ||
function normalizeBlueprintSections( sections ) { | function normalizeBlueprintSections( sections ) { | ||
return sections | return sections | ||
.map( function ( section ) { | .map( function ( section ) { | ||
var heading = htmlToText( | var heading = htmlToText( section.line || '' ); | ||
return { | return { | ||
heading: heading, | heading: heading, | ||
label: makeLabel( heading ), | label: makeLabel( heading ), | ||
index: String( | index: String( section.index || '' ), | ||
anchor: | anchor: | ||
section.linkAnchor || | section.linkAnchor || | ||
| Line 212: | Line 131: | ||
return ( | return ( | ||
section.index && | section.index && | ||
/\bBlueprints?\s*$/i.test( | /\bBlueprints?\s*$/i.test( section.heading ) | ||
); | ); | ||
} ); | } ); | ||
} | } | ||
function fetchBlueprintWikitext( api, sectionIndex ) { | |||
function fetchBlueprintWikitext( | |||
return api.get( { | return api.get( { | ||
action: 'parse', | action: 'parse', | ||
| Line 239: | Line 150: | ||
: ''; | : ''; | ||
if ( | if ( | ||
wikitext && | wikitext && | ||
typeof wikitext === 'object' && | typeof wikitext === 'object' && | ||
Object.prototype.hasOwnProperty.call( | Object.prototype.hasOwnProperty.call( wikitext, '*' ) | ||
) { | ) { | ||
wikitext = wikitext[ '*' ]; | wikitext = wikitext[ '*' ]; | ||
| Line 259: | Line 163: | ||
function addStyles() { | function addStyles() { | ||
if ( | if ( document.getElementById( 'eql-editor-tools-styles' ) ) { | ||
return; | return; | ||
} | } | ||
$( '<style>', { | $( '<style>', { | ||
id: 'eql- | id: 'eql-editor-tools-styles', | ||
text: | text: | ||
'#' + | '#' + TOOLS_ID + '{' + | ||
'box-sizing:border-box;' + | 'box-sizing:border-box;' + | ||
'margin:0 0 1rem 0;' + | 'margin:0 0 1rem 0;' + | ||
'padding:1rem 1.1rem;' + | 'padding:1rem 1.1rem;' + | ||
'background: | 'background:linear-gradient(180deg,rgba(16,21,29,.97),rgba(9,13,19,.98));' + | ||
'border:1px solid rgba(216,183,92,.38);' + | 'border:1px solid rgba(216,183,92,.38);' + | ||
'border-left:5px solid rgba(216,183,92,.82);' + | 'border-left:5px solid rgba(216,183,92,.82);' + | ||
'border-radius:8px;' + | 'border-radius:8px;' + | ||
'box-shadow: | 'box-shadow:0 9px 24px rgba(0,0,0,.3),inset 0 1px 0 rgba(255,255,255,.035);' + | ||
'}' + | |||
'#' + TOOLS_ID + ' .eql-editor-tools-row{' + | |||
'display:grid;' + | |||
'grid-template-columns:minmax(0,1fr) auto;' + | |||
'gap:1rem;' + | |||
'align-items:end;' + | |||
'}' + | '}' + | ||
'#' + | '#' + TOOLS_ID + ' .eql-blueprint-title,' + | ||
'#' + TOOLS_ID + ' .eql-iconfinder-title{' + | |||
'margin:0 0 .35rem 0;' + | 'margin:0 0 .35rem 0;' + | ||
'color:#d8b75c;' + | 'color:#d8b75c;' + | ||
'font-family:Georgia, | 'font-family:Georgia,Times New Roman,serif;' + | ||
'font-size:1.05rem;' + | |||
'font-size:1. | |||
'font-weight:700;' + | 'font-weight:700;' + | ||
'letter-spacing:.035em;' + | 'letter-spacing:.035em;' + | ||
'}' + | '}' + | ||
'#' + | '#' + TOOLS_ID + ' .eql-blueprint-description{' + | ||
'margin:0 0 . | 'margin:0 0 .7rem 0;' + | ||
'color:#aeb8c7;' + | 'color:#aeb8c7;' + | ||
'font-size:. | 'font-size:.9rem;' + | ||
'line-height:1. | 'line-height:1.35;' + | ||
'}' + | '}' + | ||
'#' + | '#' + TOOLS_ID + ' .eql-blueprint-controls{' + | ||
'display:flex;' + | 'display:flex;' + | ||
'flex-wrap:wrap;' + | 'flex-wrap:wrap;' + | ||
| Line 312: | Line 212: | ||
'}' + | '}' + | ||
'#' + | '#' + TOOLS_ID + ' select{' + | ||
'box-sizing:border-box;' + | 'box-sizing:border-box;' + | ||
'min-width:260px;' + | 'min-width:260px;' + | ||
| Line 324: | Line 224: | ||
'}' + | '}' + | ||
'#' + | '#' + TOOLS_ID + ' button{' + | ||
'min-height:2.35rem;' + | 'min-height:2.35rem;' + | ||
'padding:.35rem .85rem;' + | 'padding:.35rem .85rem;' + | ||
'background: | 'background:linear-gradient(180deg,#806d32,#594817);' + | ||
'border:1px solid #b69c4b;' + | 'border:1px solid #b69c4b;' + | ||
'border-radius:5px;' + | 'border-radius:5px;' + | ||
| Line 340: | Line 235: | ||
'}' + | '}' + | ||
'#' + | '#' + TOOLS_ID + ' button:hover:not(:disabled){' + | ||
'filter:brightness(1.08);' + | |||
'}' + | |||
'#' + TOOLS_ID + ' button:disabled{' + | |||
'cursor:default;' + | 'cursor:default;' + | ||
'opacity:.55;' + | 'opacity:.55;' + | ||
'}' + | '}' + | ||
'#' + | '#' + TOOLS_ID + ' .eql-blueprint-source{' + | ||
'color:#b9c8ff;' + | 'color:#b9c8ff;' + | ||
'font-size:.9rem;' + | 'font-size:.9rem;' + | ||
'}' + | '}' + | ||
'#' + | '#' + TOOLS_ID + ' .eql-blueprint-status{' + | ||
'display:block;' + | 'display:block;' + | ||
'margin-top:. | 'margin-top:.55rem;' + | ||
'min-height:1.2em;' + | 'min-height:1.2em;' + | ||
'color:#aeb8c7;' + | 'color:#aeb8c7;' + | ||
'font-size:. | 'font-size:.86rem;' + | ||
'}' + | |||
'#' + TOOLS_ID + ' .eql-iconfinder-launch{' + | |||
'display:flex;' + | |||
'flex-direction:column;' + | |||
'align-items:stretch;' + | |||
'justify-content:flex-end;' + | |||
'min-width:150px;' + | |||
'}' + | |||
'#eql-icon-finder-toggle{' + | |||
'white-space:nowrap;' + | |||
'}' + | |||
'#eql-icon-finder-host{' + | |||
'margin-top:1rem;' + | |||
'padding-top:1rem;' + | |||
'border-top:1px solid rgba(216,183,92,.22);' + | |||
'}' + | |||
'#eql-icon-finder-host[hidden]{display:none!important;}' + | |||
'@media(max-width:760px){' + | |||
'#' + TOOLS_ID + ' .eql-editor-tools-row{' + | |||
'grid-template-columns:1fr;' + | |||
'}' + | |||
'#' + TOOLS_ID + ' .eql-iconfinder-launch{' + | |||
'min-width:0;' + | |||
'}' + | |||
'#' + TOOLS_ID + ' select{' + | |||
'width:100%;' + | |||
'min-width:0;' + | |||
'}' + | |||
'}' | '}' | ||
} ).appendTo( document.head ); | } ).appendTo( document.head ); | ||
} | } | ||
function | function loadIconFinderScript() { | ||
if ( window.EQLIconFinder ) { | |||
return Promise.resolve( window.EQLIconFinder ); | |||
} | |||
if ( | if ( window.eqlIconFinderLoadingPromise ) { | ||
return window.eqlIconFinderLoadingPromise; | |||
return; | |||
} | } | ||
window.eqlIconFinderLoadingPromise = new Promise( function ( resolve, reject ) { | |||
var script = document.createElement( 'script' ); | |||
script.src = mw.util.getUrl( ICON_FINDER_PAGE, { | |||
action: 'raw', | |||
ctype: 'text/javascript', | |||
v: '9' | |||
} ); | |||
script.async = true; | |||
script.onload = function () { | |||
if ( window.EQLIconFinder ) { | |||
resolve( window.EQLIconFinder ); | |||
} else { | |||
reject( new Error( 'IconFinder.js loaded without exposing EQLIconFinder.' ) ); | |||
} | |||
}; | |||
script.onerror = function () { | |||
reject( new Error( 'Could not load IconFinder.js.' ) ); | |||
}; | |||
document.head.appendChild( script ); | |||
} ); | |||
return window.eqlIconFinderLoadingPromise; | |||
} | |||
function buildIconFinderLaunch( $row, $tools ) { | |||
var $launch = $( '<div>', { | |||
class: 'eql-iconfinder-launch' | |||
} ).appendTo( $row ); | |||
$( '<div>', { | |||
class: 'eql-iconfinder-title', | |||
text: 'ICON FINDER' | |||
} ).appendTo( $launch ); | |||
var $button = $( '<button>', { | |||
id: 'eql-icon-finder-toggle', | |||
type: 'button', | |||
text: 'Icon Finder', | |||
'aria-expanded': 'false' | |||
} ).appendTo( $launch ); | |||
var $host = $( '<div>', { | |||
id: 'eql-icon-finder-host', | |||
hidden: true | |||
} ).appendTo( $tools ); | |||
$button.on( 'click', function () { | |||
var opening = $host.prop( 'hidden' ); | |||
if ( !opening ) { | |||
$host.prop( 'hidden', true ); | |||
$button.attr( 'aria-expanded', 'false' ); | |||
return; | |||
} | |||
$button.prop( 'disabled', true ); | |||
$button.text( 'Loading…' ); | |||
loadIconFinderScript() | |||
.then( function ( finder ) { | |||
return finder.init( $host[ 0 ] ); | |||
} ) | |||
.then( function () { | |||
$host.prop( 'hidden', false ); | |||
$button.attr( 'aria-expanded', 'true' ); | |||
} ) | |||
.catch( function ( error ) { | |||
if ( window.console && console.error ) { | |||
console.error( 'EQL Icon Finder:', error ); | |||
} | |||
$host | |||
.prop( 'hidden', false ) | |||
.text( 'Icon Finder could not be loaded.' ); | |||
$button.attr( 'aria-expanded', 'true' ); | |||
} ) | |||
.then( function () { | |||
$button.prop( 'disabled', false ); | |||
$button.text( 'Icon Finder' ); | |||
} ); | |||
} ); | } ); | ||
} | |||
function buildBlueprintArea( $row, $textarea, api ) { | |||
var $area = $( '<div>', { | |||
class: 'eql-blueprint-area' | |||
} ).appendTo( $row ); | |||
$( '<div>', { | $( '<div>', { | ||
class: 'eql-blueprint-title', | class: 'eql-blueprint-title', | ||
text: 'CREATE FROM A BLUEPRINT' | text: 'CREATE FROM A BLUEPRINT' | ||
} ).appendTo( $ | } ).appendTo( $area ); | ||
$( '<div>', { | $( '<div>', { | ||
class: 'eql-blueprint-description', | class: 'eql-blueprint-description', | ||
text: | text: | ||
'Load a standard page skeleton from | 'Load a standard page skeleton from Help:Contents, ' + | ||
'or continue editing the current source below.' | |||
' | } ).appendTo( $area ); | ||
} ).appendTo( $ | |||
var $controls = $( '<div>', { | var $controls = $( '<div>', { | ||
class: 'eql-blueprint-controls' | class: 'eql-blueprint-controls' | ||
} ).appendTo( $ | } ).appendTo( $area ); | ||
$select = $( '<select>', { | var $select = $( '<select>', { | ||
'aria-label': 'Select page blueprint' | 'aria-label': 'Select page blueprint', | ||
disabled: true | |||
} ).appendTo( $controls ); | } ).appendTo( $controls ); | ||
$( '<option>', { | $( '<option>', { | ||
value: '', | value: '', | ||
text: ' | text: 'Loading blueprints...' | ||
} ).appendTo( $select ); | } ).appendTo( $select ); | ||
var $button = $( '<button>', { | |||
type: 'button', | type: 'button', | ||
text: 'Load Blueprint', | text: 'Load Blueprint', | ||
| Line 430: | Line 427: | ||
} ).appendTo( $controls ); | } ).appendTo( $controls ); | ||
$sourceLink = $( '<a>', { | var $sourceLink = $( '<a>', { | ||
class: 'eql-blueprint-source', | class: 'eql-blueprint-source', | ||
href: mw.util.getUrl( | href: mw.util.getUrl( BLUEPRINT_PAGE ), | ||
text: 'View Blueprints', | text: 'View Blueprints', | ||
target: '_blank' | target: '_blank', | ||
rel: 'noopener' | |||
} ).appendTo( $controls ); | } ).appendTo( $controls ); | ||
$status = $( '<span>', { | var $status = $( '<span>', { | ||
class: 'eql-blueprint-status', | class: 'eql-blueprint-status', | ||
'aria-live': 'polite' | 'aria-live': 'polite' | ||
} ).appendTo( $ | } ).appendTo( $area ); | ||
fetchBlueprintSections( api ) | |||
.then( function ( sections ) { | |||
var blueprints = normalizeBlueprintSections( sections ); | |||
$select.empty(); | |||
$( '<option>', { | |||
value: '', | |||
text: blueprints.length ? | |||
'Select a blueprint...' : | |||
'No blueprints found' | |||
} ).appendTo( $select ); | |||
blueprints.forEach( function ( blueprint ) { | |||
$( '<option>', { | |||
value: blueprint.index, | |||
text: blueprint.label | |||
} ) | |||
.attr( 'data-anchor', blueprint.anchor ) | |||
.appendTo( $select ); | |||
} ); | |||
$select.prop( 'disabled', !blueprints.length ); | |||
} ) | |||
.catch( function ( error ) { | |||
if ( window.console && console.error ) { | |||
console.error( 'EQL Blueprint Loader:', error ); | |||
} | |||
$select.empty().append( | |||
$( '<option>', { | |||
value: '', | |||
text: 'Could not load blueprints' | |||
} ) | |||
); | |||
$status.text( | |||
'Could not read Blueprint headings from Help:Contents.' | |||
$ | |||
' | |||
); | ); | ||
} ); | |||
var anchor = | $select.on( 'change', function () { | ||
var $option = $select.find( 'option:selected' ); | |||
var anchor = $option.attr( 'data-anchor' ) || ''; | |||
$button.prop( | $button.prop( 'disabled', !$select.val() ); | ||
if ( anchor ) { | if ( anchor ) { | ||
$sourceLink.attr( | $sourceLink.attr( | ||
'href', | 'href', | ||
mw.util.getUrl( | mw.util.getUrl( BLUEPRINT_PAGE ) + '#' + anchor | ||
); | ); | ||
$sourceLink.text( 'View This Blueprint' ); | |||
} else { | } else { | ||
$sourceLink.attr( | $sourceLink.attr( 'href', mw.util.getUrl( BLUEPRINT_PAGE ) ); | ||
$sourceLink.text( 'View Blueprints' ); | |||
$sourceLink.text( | |||
} | } | ||
| Line 503: | Line 502: | ||
$button.on( 'click', function () { | $button.on( 'click', function () { | ||
var sectionIndex = | var sectionIndex = $select.val(); | ||
var existing; | var existing; | ||
| Line 512: | Line 509: | ||
} | } | ||
existing = | existing = getEditorContents( $textarea ); | ||
if ( | if ( | ||
existing.trim() && | existing.trim() && | ||
!window.confirm( | !window.confirm( | ||
'The editor already contains text. | 'The editor already contains text. Replace it with the selected blueprint?' | ||
) | ) | ||
) { | ) { | ||
| Line 533: | Line 520: | ||
} | } | ||
$button.prop( | $button.prop( 'disabled', true ); | ||
$select.prop( 'disabled', true ); | |||
$status.text( 'Loading current blueprint from Help:Contents...' ); | |||
$select.prop( | |||
$status.text( | |||
fetchBlueprintWikitext( | fetchBlueprintWikitext( api, sectionIndex ) | ||
.then( function ( sectionText ) { | |||
var blueprint = extractBlueprint( sectionText ); | |||
var blueprint = | |||
if ( blueprint === null ) { | if ( blueprint === null ) { | ||
throw new Error( | throw new Error( 'No <pre> block found.' ); | ||
} | } | ||
setEditorContents( | setEditorContents( $textarea, blueprint ); | ||
$status.text( | |||
'Blueprint loaded. Edit the fields below, then publish normally.' | |||
); | ); | ||
} ) | |||
.catch( function ( error ) { | |||
if ( window.console && console.error ) { | |||
console.error( 'EQL Blueprint Loader:', error ); | |||
} | |||
$status.text( | $status.text( | ||
' | 'Could not load this blueprint. Check Help:Contents and make sure the section contains a <pre> block.' | ||
); | ); | ||
} | } ) | ||
.then( function () { | |||
$select.prop( 'disabled', false ); | |||
$button.prop( 'disabled', !$select.val() ); | |||
} ); | |||
} ); | } ); | ||
} | } | ||
function init() { | function init() { | ||
var action = mw.config.get( 'wgAction' ); | |||
var $textarea; | var $textarea; | ||
var $tools; | |||
var $row; | |||
var $editorHost; | |||
var api; | var api; | ||
if ( [ 'edit', 'submit' ].indexOf( action ) === -1 ) { | |||
if ( | |||
return; | return; | ||
} | } | ||
| Line 641: | Line 572: | ||
} | } | ||
if ( document.getElementById( TOOLS_ID ) ) { | |||
if ( | |||
return; | return; | ||
} | } | ||
| Line 660: | Line 578: | ||
api = new mw.Api(); | api = new mw.Api(); | ||
addStyles(); | |||
). | $tools = $( '<div>', { | ||
id: TOOLS_ID | |||
} ); | |||
$row = $( '<div>', { | |||
class: 'eql-editor-tools-row' | |||
} ).appendTo( $tools ); | |||
buildBlueprintArea( $row, $textarea, api ); | |||
buildIconFinderLaunch( $row, $tools ); | |||
$editorHost = $textarea.closest( '.wikiEditor-ui' ); | |||
if ( !$editorHost.length ) { | |||
$editorHost = $textarea; | |||
} | |||
$tools.insertBefore( $editorHost ); | |||
} | } | ||
Latest revision as of 04:27, 10 August 2026
/**
* EQL Wiki - Editor Tools / Dynamic Blueprint Loader
*
* Source-editor behavior:
* - On a new, truly empty page: show the Help:Contents Blueprint selector.
* - On every source edit page: show the Icon Finder button.
* - Heavy Icon Finder logic is lazy-loaded only when opened.
*/
( function ( mw, $ ) {
'use strict';
var BLUEPRINT_PAGE = 'Help:Contents';
var TOOLS_ID = 'eql-editor-tools';
var ICON_FINDER_PAGE = 'MediaWiki:IconFinder.js';
function htmlToText( html ) {
var div = document.createElement( 'div' );
div.innerHTML = String( html || '' );
return ( div.textContent || div.innerText || '' ).trim();
}
function makeLabel( heading ) {
return heading
.replace( /\s+Blueprints?\s*$/i, '' )
.trim() + ' Blueprint';
}
function getEditorContents( $textarea ) {
try {
return String( $textarea.textSelection( 'getContents' ) || '' );
} catch ( e ) {
return String( $textarea.val() || '' );
}
}
function setEditorContents( $textarea, text ) {
try {
$textarea.textSelection( 'setContents', text );
} catch ( e ) {
$textarea.val( text );
}
$textarea.trigger( 'input' );
$textarea.trigger( 'change' );
$textarea.trigger( 'focus' );
}
function extractBlueprint( sectionText ) {
var match;
var blueprint;
sectionText = String( sectionText || '' );
match = sectionText.match(
/<pre(?:\s[^>]*)?>([\s\S]*?)<\/pre>/i
);
if ( !match ) {
return null;
}
blueprint = match[ 1 ];
blueprint = blueprint.replace(
/^\s*<nowiki>([\s\S]*?)<\/nowiki>\s*$/i,
'$1'
);
blueprint = blueprint
.replace( /^\s*\n/, '' )
.replace( /\s+$/, '' );
return blueprint + '\n';
}
function fetchBlueprintSections( api ) {
return api.get( {
action: 'parse',
page: BLUEPRINT_PAGE,
prop: 'tocdata',
formatversion: 2
} ).then( function ( data ) {
var tocdata =
data &&
data.parse &&
data.parse.tocdata;
var sections =
tocdata &&
Array.isArray( tocdata.sections )
? tocdata.sections
: [];
if ( sections.length ) {
return sections;
}
return api.get( {
action: 'parse',
page: BLUEPRINT_PAGE,
prop: 'sections',
formatversion: 2
} ).then( function ( fallbackData ) {
return (
fallbackData &&
fallbackData.parse &&
Array.isArray( fallbackData.parse.sections )
)
? fallbackData.parse.sections
: [];
} );
} );
}
function normalizeBlueprintSections( sections ) {
return sections
.map( function ( section ) {
var heading = htmlToText( section.line || '' );
return {
heading: heading,
label: makeLabel( heading ),
index: String( section.index || '' ),
anchor:
section.linkAnchor ||
section.anchor ||
''
};
} )
.filter( function ( section ) {
return (
section.index &&
/\bBlueprints?\s*$/i.test( section.heading )
);
} );
}
function fetchBlueprintWikitext( api, sectionIndex ) {
return api.get( {
action: 'parse',
page: BLUEPRINT_PAGE,
prop: 'wikitext',
section: sectionIndex,
formatversion: 2
} ).then( function ( data ) {
var wikitext =
data &&
data.parse
? data.parse.wikitext
: '';
if (
wikitext &&
typeof wikitext === 'object' &&
Object.prototype.hasOwnProperty.call( wikitext, '*' )
) {
wikitext = wikitext[ '*' ];
}
return String( wikitext || '' );
} );
}
function addStyles() {
if ( document.getElementById( 'eql-editor-tools-styles' ) ) {
return;
}
$( '<style>', {
id: 'eql-editor-tools-styles',
text:
'#' + TOOLS_ID + '{' +
'box-sizing:border-box;' +
'margin:0 0 1rem 0;' +
'padding:1rem 1.1rem;' +
'background:linear-gradient(180deg,rgba(16,21,29,.97),rgba(9,13,19,.98));' +
'border:1px solid rgba(216,183,92,.38);' +
'border-left:5px solid rgba(216,183,92,.82);' +
'border-radius:8px;' +
'box-shadow:0 9px 24px rgba(0,0,0,.3),inset 0 1px 0 rgba(255,255,255,.035);' +
'}' +
'#' + TOOLS_ID + ' .eql-editor-tools-row{' +
'display:grid;' +
'grid-template-columns:minmax(0,1fr) auto;' +
'gap:1rem;' +
'align-items:end;' +
'}' +
'#' + TOOLS_ID + ' .eql-blueprint-title,' +
'#' + TOOLS_ID + ' .eql-iconfinder-title{' +
'margin:0 0 .35rem 0;' +
'color:#d8b75c;' +
'font-family:Georgia,Times New Roman,serif;' +
'font-size:1.05rem;' +
'font-weight:700;' +
'letter-spacing:.035em;' +
'}' +
'#' + TOOLS_ID + ' .eql-blueprint-description{' +
'margin:0 0 .7rem 0;' +
'color:#aeb8c7;' +
'font-size:.9rem;' +
'line-height:1.35;' +
'}' +
'#' + TOOLS_ID + ' .eql-blueprint-controls{' +
'display:flex;' +
'flex-wrap:wrap;' +
'align-items:center;' +
'gap:.65rem;' +
'}' +
'#' + TOOLS_ID + ' select{' +
'box-sizing:border-box;' +
'min-width:260px;' +
'max-width:100%;' +
'min-height:2.35rem;' +
'padding:.35rem .55rem;' +
'background:#0b1017;' +
'border:1px solid rgba(216,183,92,.36);' +
'border-radius:5px;' +
'color:#e2e2cf;' +
'}' +
'#' + TOOLS_ID + ' button{' +
'min-height:2.35rem;' +
'padding:.35rem .85rem;' +
'background:linear-gradient(180deg,#806d32,#594817);' +
'border:1px solid #b69c4b;' +
'border-radius:5px;' +
'color:#fff5cc;' +
'font-weight:700;' +
'cursor:pointer;' +
'}' +
'#' + TOOLS_ID + ' button:hover:not(:disabled){' +
'filter:brightness(1.08);' +
'}' +
'#' + TOOLS_ID + ' button:disabled{' +
'cursor:default;' +
'opacity:.55;' +
'}' +
'#' + TOOLS_ID + ' .eql-blueprint-source{' +
'color:#b9c8ff;' +
'font-size:.9rem;' +
'}' +
'#' + TOOLS_ID + ' .eql-blueprint-status{' +
'display:block;' +
'margin-top:.55rem;' +
'min-height:1.2em;' +
'color:#aeb8c7;' +
'font-size:.86rem;' +
'}' +
'#' + TOOLS_ID + ' .eql-iconfinder-launch{' +
'display:flex;' +
'flex-direction:column;' +
'align-items:stretch;' +
'justify-content:flex-end;' +
'min-width:150px;' +
'}' +
'#eql-icon-finder-toggle{' +
'white-space:nowrap;' +
'}' +
'#eql-icon-finder-host{' +
'margin-top:1rem;' +
'padding-top:1rem;' +
'border-top:1px solid rgba(216,183,92,.22);' +
'}' +
'#eql-icon-finder-host[hidden]{display:none!important;}' +
'@media(max-width:760px){' +
'#' + TOOLS_ID + ' .eql-editor-tools-row{' +
'grid-template-columns:1fr;' +
'}' +
'#' + TOOLS_ID + ' .eql-iconfinder-launch{' +
'min-width:0;' +
'}' +
'#' + TOOLS_ID + ' select{' +
'width:100%;' +
'min-width:0;' +
'}' +
'}'
} ).appendTo( document.head );
}
function loadIconFinderScript() {
if ( window.EQLIconFinder ) {
return Promise.resolve( window.EQLIconFinder );
}
if ( window.eqlIconFinderLoadingPromise ) {
return window.eqlIconFinderLoadingPromise;
}
window.eqlIconFinderLoadingPromise = new Promise( function ( resolve, reject ) {
var script = document.createElement( 'script' );
script.src = mw.util.getUrl( ICON_FINDER_PAGE, {
action: 'raw',
ctype: 'text/javascript',
v: '9'
} );
script.async = true;
script.onload = function () {
if ( window.EQLIconFinder ) {
resolve( window.EQLIconFinder );
} else {
reject( new Error( 'IconFinder.js loaded without exposing EQLIconFinder.' ) );
}
};
script.onerror = function () {
reject( new Error( 'Could not load IconFinder.js.' ) );
};
document.head.appendChild( script );
} );
return window.eqlIconFinderLoadingPromise;
}
function buildIconFinderLaunch( $row, $tools ) {
var $launch = $( '<div>', {
class: 'eql-iconfinder-launch'
} ).appendTo( $row );
$( '<div>', {
class: 'eql-iconfinder-title',
text: 'ICON FINDER'
} ).appendTo( $launch );
var $button = $( '<button>', {
id: 'eql-icon-finder-toggle',
type: 'button',
text: 'Icon Finder',
'aria-expanded': 'false'
} ).appendTo( $launch );
var $host = $( '<div>', {
id: 'eql-icon-finder-host',
hidden: true
} ).appendTo( $tools );
$button.on( 'click', function () {
var opening = $host.prop( 'hidden' );
if ( !opening ) {
$host.prop( 'hidden', true );
$button.attr( 'aria-expanded', 'false' );
return;
}
$button.prop( 'disabled', true );
$button.text( 'Loading…' );
loadIconFinderScript()
.then( function ( finder ) {
return finder.init( $host[ 0 ] );
} )
.then( function () {
$host.prop( 'hidden', false );
$button.attr( 'aria-expanded', 'true' );
} )
.catch( function ( error ) {
if ( window.console && console.error ) {
console.error( 'EQL Icon Finder:', error );
}
$host
.prop( 'hidden', false )
.text( 'Icon Finder could not be loaded.' );
$button.attr( 'aria-expanded', 'true' );
} )
.then( function () {
$button.prop( 'disabled', false );
$button.text( 'Icon Finder' );
} );
} );
}
function buildBlueprintArea( $row, $textarea, api ) {
var $area = $( '<div>', {
class: 'eql-blueprint-area'
} ).appendTo( $row );
$( '<div>', {
class: 'eql-blueprint-title',
text: 'CREATE FROM A BLUEPRINT'
} ).appendTo( $area );
$( '<div>', {
class: 'eql-blueprint-description',
text:
'Load a standard page skeleton from Help:Contents, ' +
'or continue editing the current source below.'
} ).appendTo( $area );
var $controls = $( '<div>', {
class: 'eql-blueprint-controls'
} ).appendTo( $area );
var $select = $( '<select>', {
'aria-label': 'Select page blueprint',
disabled: true
} ).appendTo( $controls );
$( '<option>', {
value: '',
text: 'Loading blueprints...'
} ).appendTo( $select );
var $button = $( '<button>', {
type: 'button',
text: 'Load Blueprint',
disabled: true
} ).appendTo( $controls );
var $sourceLink = $( '<a>', {
class: 'eql-blueprint-source',
href: mw.util.getUrl( BLUEPRINT_PAGE ),
text: 'View Blueprints',
target: '_blank',
rel: 'noopener'
} ).appendTo( $controls );
var $status = $( '<span>', {
class: 'eql-blueprint-status',
'aria-live': 'polite'
} ).appendTo( $area );
fetchBlueprintSections( api )
.then( function ( sections ) {
var blueprints = normalizeBlueprintSections( sections );
$select.empty();
$( '<option>', {
value: '',
text: blueprints.length ?
'Select a blueprint...' :
'No blueprints found'
} ).appendTo( $select );
blueprints.forEach( function ( blueprint ) {
$( '<option>', {
value: blueprint.index,
text: blueprint.label
} )
.attr( 'data-anchor', blueprint.anchor )
.appendTo( $select );
} );
$select.prop( 'disabled', !blueprints.length );
} )
.catch( function ( error ) {
if ( window.console && console.error ) {
console.error( 'EQL Blueprint Loader:', error );
}
$select.empty().append(
$( '<option>', {
value: '',
text: 'Could not load blueprints'
} )
);
$status.text(
'Could not read Blueprint headings from Help:Contents.'
);
} );
$select.on( 'change', function () {
var $option = $select.find( 'option:selected' );
var anchor = $option.attr( 'data-anchor' ) || '';
$button.prop( 'disabled', !$select.val() );
if ( anchor ) {
$sourceLink.attr(
'href',
mw.util.getUrl( BLUEPRINT_PAGE ) + '#' + anchor
);
$sourceLink.text( 'View This Blueprint' );
} else {
$sourceLink.attr( 'href', mw.util.getUrl( BLUEPRINT_PAGE ) );
$sourceLink.text( 'View Blueprints' );
}
$status.text( '' );
} );
$button.on( 'click', function () {
var sectionIndex = $select.val();
var existing;
if ( !sectionIndex ) {
return;
}
existing = getEditorContents( $textarea );
if (
existing.trim() &&
!window.confirm(
'The editor already contains text. Replace it with the selected blueprint?'
)
) {
return;
}
$button.prop( 'disabled', true );
$select.prop( 'disabled', true );
$status.text( 'Loading current blueprint from Help:Contents...' );
fetchBlueprintWikitext( api, sectionIndex )
.then( function ( sectionText ) {
var blueprint = extractBlueprint( sectionText );
if ( blueprint === null ) {
throw new Error( 'No <pre> block found.' );
}
setEditorContents( $textarea, blueprint );
$status.text(
'Blueprint loaded. Edit the fields below, then publish normally.'
);
} )
.catch( function ( error ) {
if ( window.console && console.error ) {
console.error( 'EQL Blueprint Loader:', error );
}
$status.text(
'Could not load this blueprint. Check Help:Contents and make sure the section contains a <pre> block.'
);
} )
.then( function () {
$select.prop( 'disabled', false );
$button.prop( 'disabled', !$select.val() );
} );
} );
}
function init() {
var action = mw.config.get( 'wgAction' );
var $textarea;
var $tools;
var $row;
var $editorHost;
var api;
if ( [ 'edit', 'submit' ].indexOf( action ) === -1 ) {
return;
}
$textarea = $( '#wpTextbox1' );
if ( !$textarea.length ) {
return;
}
if ( document.getElementById( TOOLS_ID ) ) {
return;
}
api = new mw.Api();
addStyles();
$tools = $( '<div>', {
id: TOOLS_ID
} );
$row = $( '<div>', {
class: 'eql-editor-tools-row'
} ).appendTo( $tools );
buildBlueprintArea( $row, $textarea, api );
buildIconFinderLaunch( $row, $tools );
$editorHost = $textarea.closest( '.wikiEditor-ui' );
if ( !$editorHost.length ) {
$editorHost = $textarea;
}
$tools.insertBefore( $editorHost );
}
mw.loader.using( [
'mediawiki.api',
'mediawiki.util',
'jquery.textSelection'
] ).then( function () {
$( init );
} );
}( mediaWiki, jQuery ) );