User:Fred Gandt/quickLinks.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. |
This user script seems to have a documentation page at User:Fred Gandt/quickLinks and an accompanying .css page at User:Fred Gandt/quickLinks.css. |
/*********************************************************************************************************************************
* Currently still in development, this is designed to provide a custom list of Quick Links to Wikipedia pages.
* If you encounter any problems using this script, please tell User:Fred_Gandt on either my talk page or this script's talk page.
*
*********************************************************************************************************************************/
/* TODO: Handle #sections */
/* TODO: Reduce API calls */
( function() {
"use strict";
var eByTn = function( p, n, i, nl ) { nl = p.getElementsByTagName( n ); return i !== undefined ? nl[ i ] : nl; },
eById = function( id ) { return document.getElementById( id ); },
cE = function( e ) { return document.createElement( e ); },
nl2a = function( nl ) { return [].slice.call( nl ); },
WG_pagename = mw.config.get( "wgPageName" ),
BASE = "fg-quick-links",
EXT = BASE + "-",
SWITCH = EXT + "switch",
VIEW = EXT + "view",
EMPTY = EXT + "empty",
OPEN = EXT + "open",
TITLE = EXT + "title",
STORAGE = EXT + "storage",
QL = EXT + "ql",
QLE,
NPT,
namespace = l => /^(?:([^\:]+)\:)?(.+)$/.exec( l ),
toggleBase = e => ( e || ql.ui ).classList.toggle( BASE ),
underspace = ( s, b ) => b ? s.replace( /_/g, " " ) : s.replace( / /g, "_" ),
gotIt = v => ql.ui.querySelector( 'a[title="' + underspace( v || WG_pagename, true ).replace( /\"/g, "\\\"" ) + '"]' ),
ql = {
optnnm: { local: EXT + mw.config.get( "wgUserName" ).replace( / /g, "-" ), global: "userjs-" + BASE },
optnvlu: [ { "Mainspace": [] }, { "Mainspace talk": [] } ],
alss: { undefined: "Mainspace", "talk": "Mainspace talk" },
ui: cE( "li" )
},
initOptionValue = function() {
var fns = mw.config.get( "wgFormattedNamespaces" ),
nsi = mw.config.get( "wgNamespaceIds" ),
ns, cns, cnsi, tmp;
for ( ns in nsi ) {
cnsi = nsi[ ns ];
cns = fns[ cnsi ];
if ( underspace( cns ).toLowerCase() === ns && cnsi !== 0 && cnsi !== 1 ) {
tmp = {};
tmp[ cns ] = [];
ql.optnvlu.push( tmp );
} else {
ql.alss[ ns ] = cns;
}
}
return ql.optnvlu;
},
linkify = function( v, d ) {
v = v.replace( /^Mainspace(?:[ _]{1}talk)?\:/i, "" );
var u = underspace( v, true );
if ( d ) {
u = u.replace( /[\.\%]{1}(2[1-9a-c]{1}|[357][b-e]{1}|[23]f|[46]0|c2[\.\%]{1}a([01]{1}))/gi, function( m, g1, g2 ) {
if ( g2 ) {
return { "0": " ", "1": "¡" }[ g2.toLowerCase() ];
}
return { "21": "!", "22": """, "23": "#", "24": "$", "25": "%", "26": "&", "27": "'", "28": "(", "29": ")", "2a": "*", "2b": "+", "2c": ",", "2f": "/",
"3b": ";", "3c": "<", "3d": "=", "3e": ">", "3f": "?", "5b": "[", "5c": "\", "5d": "]", "5e": "^", "7b": "{", "7c": "|", "7d": "}", "7e": "~",
"40": "@", "60": "`" }[ g1.toLowerCase() ];
} );
}
return '<a href="/wiki/' + mw.util.wikiUrlencode( v ) + '" title="' + u.replace( /\"/g, """ ) + '">' + namespace( u )[ 2 ] + '</a>';
},
quickLinks = function() {
var vlus = ql.optnvlu, o = [], vlu, qls, on, oa, ok,
iterate = function( a ) {
var i = [], v;
for ( v in a ) {
i.push( linkify( a[ v ] ) );
}
return i.join( '</li><li>' );
},
filler = function( a ) {
if ( a.length ) {
return '<li>' + iterate( a ) + '</li>';
}
return "";
},
brynner = function( t, c, f ) {
var u = cE( "ul" );
u.id = underspace( EXT + t );
if ( c ) {
u.setAttribute( "class", c );
}
u.innerHTML = '<li class="' + TITLE + '">' + t + '</li>' + f;
return u.outerHTML;
};
for ( vlu in vlus ) {
qls = vlus[ vlu ];
ok = Object.keys( qls );
on = ok[ 0 ];
oa = qls[ on ];
o.push( brynner( on, !oa.length ? EMPTY : ( ok[ 1 ] ? OPEN : false ), filler( oa ) ) );
}
return o.join( "" );
},
switchSwitch = function( t ) {
var s = eById( SWITCH );
if ( t ) {
toggleBase( s );
} else {
s.classList.toggle( BASE, gotIt() );
}
},
save = function( ss ) {
var uls = nl2a( eByTn( ql.ui, "ul" ) ), tmpoptnvlu = [],
ul, la, tmp, cul,
titleArray = function( a ) {
var l, ta = [];
for ( l in a ) {
ta.push( underspace( eByTn( a[ l ], "a", 0 ).title ) );
}
return ta.sort();
},
showError = function( e ) {
alert( "Something went wrong:\n\n" + e );
};
for ( ul in uls ) {
tmp = {};
cul = uls[ ul ];
la = nl2a( eByTn( cul, "li" ) );
tmp[ la[ 0 ].textContent ] = titleArray( la.slice( 1 ) );
if ( cul.classList.contains( OPEN ) ) {
tmp.open = true;
}
tmpoptnvlu.push( tmp );
}
$.ajax( {
type: "POST",
url: "/w/api.php",
dataType: "json",
data: {
format: "json",
action: "options",
token: mw.user.tokens.values.csrfToken,
optionname: ql.optnnm.global,
optionvalue: JSON.stringify( tmpoptnvlu )
},
success: function( data ) {
if ( !data.error ) {
localStorage[ STORAGE ] = JSON.stringify( QLE.innerHTML );
ql.optnvlu = tmpoptnvlu;
switchSwitch( ss );
} else {
QLE.innerHTML = quickLinks();
showError( data.error.info );
}
},
error: function( something, went, wrong ) {
QLE.innerHTML = quickLinks();
console.error( something );
showError( went + ":\n\n" + wrong );
}
} );
},
addThis = function( v, d ) {
var alias = function( q ) {
return ql.alss[ q ? q.toLowerCase() : q ] || q;
},
li, ul = eById( EXT + underspace( alias( namespace( v )[ 1 ] ) ) );
if ( ul ) {
li = cE( "li" );
li.innerHTML = linkify( v, d );
ul.appendChild( li );
ul.classList.remove( EMPTY );
return li;
}
return false;
},
removeThis = function( t ) {
var tp = t.parentElement, tpp = tp.parentElement;
tpp.removeChild( tp );
tpp.classList.toggle( EMPTY, nl2a( eByTn( tpp, "li" ) ).length < 2 );
},
setListeners = function() {
var prepText = function( txt ) {
return ( /(?:^.*w(?:iki)?\/(?:.+title\=)?)?([^&]+)/ ).exec( txt.trim() )[ 1 ];
},
processText = function( vlu, d ) {
if ( vlu && !gotIt( vlu ) ) {
if ( addThis( underspace( vlu ), d ) ) {
save();
NPT.value = "";
} else if ( !confirm( "Something about that value isn't correct.\nModify it and try again?" ) ) {
NPT.value = "";
}
}
};
ql.ui.addEventListener( "click", evt => {
var trg = evt.target, nn = trg.nodeName.toLowerCase(), ths = gotIt();
if ( nn === "button" ) {
toggleBase();
} else if ( nn === "a" ) {
if ( trg.id === SWITCH ) {
evt.preventDefault();
if ( !ths ) {
addThis( WG_pagename );
} else {
removeThis( ths );
}
save( true );
} else if ( trg.id === VIEW ) {
evt.preventDefault();
toggleBase();
}
} else if ( nn === "li" ) {
if ( !trg.classList.contains( TITLE ) ) {
removeThis( eByTn( trg, "a", 0 ) );
} else {
trg.parentElement.classList.toggle( OPEN );
}
save();
}
}, false );
ql.ui.addEventListener( "dragover", evt => evt.preventDefault() );
ql.ui.addEventListener( "drop", evt => {
evt.preventDefault();
processText( prepText( evt.dataTransfer.getData( "text" ) ), true );
} );
NPT.addEventListener( "paste", evt => {
evt.preventDefault();
processText( prepText( evt.clipboardData.getData( "text" ) ), true );
}, false );
NPT.addEventListener( "change", evt => processText( NPT.value ) );
window.addEventListener( "storage", evt => {
var k = evt.key, nv = evt.newValue;
if ( k && k === STORAGE && nv ) {
QLE.innerHTML = JSON.parse( nv );
switchSwitch();
delete localStorage[ STORAGE ];
}
}, false );
};
ql.optnvlu = JSON.parse( mw.user.options.values[ ql.optnnm.global ] || JSON.stringify( initOptionValue() ) );
$( document ).ready( () => {
ql.ui.id = BASE;
ql.ui.innerHTML = `<span><a id="${SWITCH}" href="#"></a><span><a id="${VIEW}" href="#"></a><div><input type="text" placeholder="Add a new link"><div id="${QL}">${quickLinks()}</div><button>Close</button></div></span></span>`;
const STYLE_SHEET = new CSSStyleSheet();
document.adoptedStyleSheets = [ ...document.adoptedStyleSheets, STYLE_SHEET ];
STYLE_SHEET.replaceSync( `#fg-quick-links-switch {
text-decoration: none;
padding: .5em .2em;
font-size: 1.7em;
background: none;
height: 1.46em;
color: #ffbc41;
width: 1em;
}
#fg-quick-links-switch::before {
content: "☆";
}
#fg-quick-links-switch.fg-quick-links::before {
content: "★";
}
#fg-quick-links-view {
text-decoration: none;
padding: .8em .3em;
background: none;
font-size: 1.1em;
height: 2.3em;
color: unset;
opacity: .5;
width: 2em;
}
#fg-quick-links-view::before {
content: "🔍";
}
#fg-quick-links span > span {
display: inline;
}
#fg-quick-links span > div {
display: none;
position: absolute;
min-width: 300px;
background: #fff;
z-index: 2000;
margin-top: 2.2em;
padding: 1em;
border: 1px solid #a7d7f9;
border-radius: 3px;
box-shadow: 2px 2px 15px -2px rgba(0, 0, 0, 0.5);
}
#fg-quick-links-ql {
max-height: calc( 80vh - 13em );
padding-right: 2em;
overflow: auto;
overflow-x: hidden;
overscroll-behavior: contain;
}
#fg-quick-links-ql ul {
float: none !important;
background: none;
}
#fg-quick-links-ql ul.fg-quick-links-empty {
display: none;
}
#fg-quick-links-ql li {
float: none !important;
height: auto;
background: none;
}
#fg-quick-links-ql li.fg-quick-links-title {
font-weight: bold;
color: #666;
cursor: pointer;
}
#fg-quick-links-ql li:not( .fg-quick-links-title ) {
display: none;
margin-left: 1.3em;
}
#fg-quick-links-ql li a {
padding: 0;
float: none;
height: auto;
display: block;
margin-left: 1.3em;
background-image: none;
}
#fg-quick-links-ql ul li.fg-quick-links-title::before {
content: "► ";
float: left;
color: #aaa;
}
#fg-quick-links-ql ul.fg-quick-links-open li.fg-quick-links-title::before {
content: "▼ ";
}
#fg-quick-links-ql li:not( [class=fg-quick-links-title] )::before {
content: "x";
float: left;
color: #fff;
background: rgba( 255, 0, 0, 0.5 );
border-radius: 100%;
padding: 1px 3px;
font-size: 10px;
line-height: 10px;
margin-top: 2px;
cursor: pointer;
}
#fg-quick-links input {
margin-bottom: 1em;
width: calc( 100% - 2em - 2px );
padding: 0.5em 1em 0.6em;
border: 1px solid #aaa;
border-radius: 3px;
}
#fg-quick-links button {
display: none;
margin-top: 1em;
}
#p-views,
#fg-quick-links span:hover > div,
#fg-quick-links.fg-quick-links button,
#fg-quick-links.fg-quick-links span > div,
#fg-quick-links-ql ul.fg-quick-links-open li {
display: block;
}` );
eByTn( eById( "p-views" ), "ul", 0 ).append( ql.ui );
NPT = eByTn( ql.ui, "input", 0 );
QLE = eById( QL );
switchSwitch();
setListeners();
}, { once: true } );
} () );