diff --git a/.eslintrc.json b/.eslintrc.json index 98aa43b..2126db8 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -8,7 +8,8 @@ "jest": true }, "globals": { - "mw": "readonly" + "mw": "readonly", + "mediaWiki": "readonly" }, "parserOptions": { "sourceType": "module", // eslint-config-wikimedia default is "commonjs", but we need "module" for our unit tests diff --git a/GANReviewTool/modules/GANReviewWikicodeGenerator.js b/GANReviewTool/modules/GANReviewWikicodeGenerator.js index 7a99159..e78ba6a 100644 --- a/GANReviewTool/modules/GANReviewWikicodeGenerator.js +++ b/GANReviewTool/modules/GANReviewWikicodeGenerator.js @@ -126,7 +126,7 @@ export class GANReviewWikicodeGenerator { getLogMessageToAppend( username, action, reviewTitle, reviewRevisionID, talkRevisionID, gaRevisionID, error ) { let textToAppend = '\n* '; if ( error ) { - textToAppend += `ERROR: ${ error }. `; + textToAppend += `ERROR: ${ error }. `; } let verb = ''; diff --git a/GANReviewTool/modules/GARCloserWikicodeGenerator.js b/GANReviewTool/modules/GARCloserWikicodeGenerator.js index b36553f..95c8437 100644 --- a/GANReviewTool/modules/GARCloserWikicodeGenerator.js +++ b/GANReviewTool/modules/GARCloserWikicodeGenerator.js @@ -51,7 +51,7 @@ __TOC__`; let textToAppend = '\n* '; if ( error ) { - textToAppend += `ERROR: ${ error }. `; + textToAppend += `ERROR: ${ error }. `; } const keepOrDelistPastTense = this.getKeepOrDelistPastTense( keepOrDelist ); diff --git a/GANReviewTool/tests/GANReviewWikicodeGenerator.test.js b/GANReviewTool/tests/GANReviewWikicodeGenerator.test.js index 534228b..7a00001 100644 --- a/GANReviewTool/tests/GANReviewWikicodeGenerator.test.js +++ b/GANReviewTool/tests/GANReviewWikicodeGenerator.test.js @@ -1119,7 +1119,7 @@ describe( 'getLogMessageToAppend(username, passOrFail, reviewTitle, reviewRevisi const talkRevisionID = undefined; const gaRevisionID = undefined; const error = 'ReferenceError: getPassWikicodeForGANPage is not defined'; - const output = '\n* ERROR: ReferenceError: getPassWikicodeForGANPage is not defined. [[User:Novem Linguae|Novem Linguae]] passed [[Talk:Thomas Carlyle (Millais)/GA1]] at ~~~~~. '; + const output = '\n* ERROR: ReferenceError: getPassWikicodeForGANPage is not defined. [[User:Novem Linguae|Novem Linguae]] passed [[Talk:Thomas Carlyle (Millais)/GA1]] at ~~~~~. '; expect( wg.getLogMessageToAppend( username, passOrFail, reviewTitle, reviewRevisionID, talkRevisionID, gaRevisionID, error ) ).toBe( output ); } ); diff --git a/GANReviewTool/tests/GARCloserWikicodeGenerator.test.js b/GANReviewTool/tests/GARCloserWikicodeGenerator.test.js index 30bc5fa..872e409 100644 --- a/GANReviewTool/tests/GARCloserWikicodeGenerator.test.js +++ b/GANReviewTool/tests/GARCloserWikicodeGenerator.test.js @@ -395,7 +395,7 @@ describe( 'makeScriptLogEntryToAppend(username, keepOrDelist, reviewTitle, talkR const garArchiveTemplateRevisionID = undefined; const categoryRevisionID = undefined; const error = 'ReferenceError: getPassWikicodeForGANPage is not defined'; - const output = '\n* ERROR: ReferenceError: getPassWikicodeForGANPage is not defined. [[User:Novem Linguae|Novem Linguae]] delisted [[Wikipedia:Good article reassessment/WIN Television/1]] at ~~~~~. [[Special:Diff/987|[Atop]]][[Special:Diff/123|[Talk]]][[Special:Diff/456|[Article]]][[Special:Diff/789|[List]]][[Special:Diff/101112|[Log]]]'; + const output = '\n* ERROR: ReferenceError: getPassWikicodeForGANPage is not defined. [[User:Novem Linguae|Novem Linguae]] delisted [[Wikipedia:Good article reassessment/WIN Television/1]] at ~~~~~. [[Special:Diff/987|[Atop]]][[Special:Diff/123|[Talk]]][[Special:Diff/456|[Article]]][[Special:Diff/789|[List]]][[Special:Diff/101112|[Log]]]'; expect( wg.makeScriptLogEntryToAppend( username, keepOrDelist, reviewTitle, garRevisionID, talkRevisionID, articleRevisionID, gaListRevisionID, garLogRevisionID, garArchiveTemplateRevisionID, error, categoryRevisionID ) ).toBe( output ); } ); } ); diff --git a/Links.js b/Links.js index 3952983..5ef2e7f 100644 --- a/Links.js +++ b/Links.js @@ -29,10 +29,9 @@ - modern - monobook - timeless + - minerva (TODO: make less ugly) - right menu (tools menu) - vector-2022 - - not displayed at all - - minerva */ class Links { @@ -52,9 +51,11 @@ class Links { } createBottomLeftMenuContainer() { - // Works as expected in vector, modern, monobook, timeless. Is in right menu instead of left menu in vector-2022. Doesn't show up at all in minerva. - // Could fix in those skins by adding it before an existing nearby portlet, then using the p.parentNode.appendChild( p ); trick. - this.mw.util.addPortlet( 'p-links', 'More tools', '#p-coll-print_export' ); + if ( this.mw.config.get( 'skin' ) === 'minerva' ) { + this.mw.util.addPortlet( 'p-links', 'More tools', '#p-navigation' ); + } else { + this.mw.util.addPortlet( 'p-links', 'More tools', '#p-tb' ); + } } async generateUserspaceLinks() { diff --git a/QuickVandalBlock.js b/QuickVandalBlock.js index a9a6cd6..89abbef 100644 --- a/QuickVandalBlock.js +++ b/QuickVandalBlock.js @@ -1,9 +1,15 @@ -// Copied content from https://en.wikipedia.org/wiki/User:Enterprisey/quick-vand-block.js. Please see that page's history for attribution. +// Forked from https://en.wikipedia.org/wiki/User:Enterprisey/quick-vand-block.js, with thanks to the original creator, Enterprisey. + +// This is a script to help admins process [[WP:AIV]] requests quickly. + +// In diffs and filter logs, places ( indef ) links next to usernames, and ( 31h | proxy ) links next to IPs. Clicking one of these will prompt you "are you sure you want to block X?", then will block them for that duration, and leave vandalism-related talk page notifications and edit summaries. Note that clicking "proxy" will skip leaving a talk page notification. + /* Changes: * linted/refactored * added ? to end of question in popup * added proxy block +* fixed the indef button to work with temporary accounts */ // @@ -17,17 +23,19 @@ $.when( mw.loader.using( [ 'mediawiki.api', 'mediawiki.util' ] ), $.ready ).then let $linkAndListener; if ( isIp ) { - $linkAndListener = $( '' ).attr( 'href', '#' ) + $linkAndListener = $( '' ) + .attr( 'href', '#' ) .text( '31h' ) .on( 'click', function () { const username = $( this ).parent().get( 0 ).previousElementSibling.textContent; const duration = '31 hours'; const logReason = '[[Wikipedia:Vandalism|Vandalism]]'; const templateName = 'uw-vblock'; - const templateParams = {}; - templateParams.anon = 'yes'; - templateParams.time = '31 hours'; - templateParams.sig = 'yes'; + const templateParams = { + anon: 'yes', + time: '31 hours', + sig: 'yes' + }; const isMainspaceSpecialOrMedia = mw.config.get( 'wgNamespaceNumber' ) < 1; if ( !isMainspaceSpecialOrMedia ) { templateParams.page = mw.config.get( 'wgPageName' ); @@ -36,7 +44,8 @@ $.when( mw.loader.using( [ 'mediawiki.api', 'mediawiki.util' ] ), $.ready ).then } ); $( element ).contents().last().before( ' | ', $linkAndListener ); - $linkAndListener = $( '' ).attr( 'href', '#' ) + $linkAndListener = $( '' ) + .attr( 'href', '#' ) .text( 'proxy' ) .on( 'click', function () { const username = $( this ).parent().get( 0 ).previousElementSibling.textContent; @@ -53,16 +62,18 @@ $.when( mw.loader.using( [ 'mediawiki.api', 'mediawiki.util' ] ), $.ready ).then } ); $( element ).contents().last().before( ' | ', $linkAndListener ); } else { - $linkAndListener = $( '' ).attr( 'href', '#' ) + $linkAndListener = $( '' ) + .attr( 'href', '#' ) .text( 'indef' ) .on( 'click', function () { - const username = $( this ).parent().get( 0 ).previousElementSibling.textContent; + const username = $( this ).parent().siblings( '.mw-userlink' ).find( 'bdi' ).text(); const duration = 'never'; const logReason = '[[Wikipedia:Vandalism|Vandalism]]'; const templateName = 'uw-vblock'; - const templateParams = {}; - templateParams.indef = 'yes'; - templateParams.sig = 'yes'; + const templateParams = { + indef: 'yes', + sig: 'yes' + }; const isMainspaceSpecialOrMedia = mw.config.get( 'wgNamespaceNumber' ) < 1; if ( !isMainspaceSpecialOrMedia ) { templateParams.page = mw.config.get( 'wgPageName' ); diff --git a/ReFillLink.js b/ReFillLink.js index a93a478..5fc747c 100644 --- a/ReFillLink.js +++ b/ReFillLink.js @@ -4,7 +4,7 @@ const notSpecialOrFileNamespace = mw.config.get( 'wgNamespaceNumber' ) >= 0; if ( pageExists && notSpecialOrFileNamespace ) { mw.util.addPortletLink( 'p-cactions', - 'https://refill.toolforge.org/ng/result.php?page=' + encodeURIComponent( mw.config.get( 'wgPageName' ) ) + '&defaults=y&nowatch=y&wiki=en', + 'https://refill.toolforge.org/result.php?page=' + encodeURIComponent( mw.config.get( 'wgPageName' ) ) + '&defaults=y&nowatch=y&wiki=en', 'reFill' ); } diff --git a/SpeciesHelper/modules/SpeciesHelperFunctions.js b/SpeciesHelper/modules/SpeciesHelperFunctions.js index 9b86b31..41b8e26 100644 --- a/SpeciesHelper/modules/SpeciesHelperFunctions.js +++ b/SpeciesHelper/modules/SpeciesHelperFunctions.js @@ -255,14 +255,14 @@ export class SpeciesHelperFunctions { } taxaStringToArray( taxa ) { - // Double check that {{#invoke:Autotaxobox|listAll|*}} didn't return garbage. It can return garbage if Template:Taxonomy/* is formatted incorrectly. - if ( !taxa.match( /^[A-Za-z\-/ ,()]+$/ ) ) { - throw new Error(); - } // get rid of "Life" at the end taxa = taxa.replace( ', Life-', '' ); // convert to array taxa = taxa.split( ', ' ); + // Double check that {{#invoke:Autotaxobox|listAll|*}} didn't return garbage. It can return garbage if Template:Taxonomy/* has weird code. We'll always be checking a genus, so there should be lots of stuff in the array. {{#invoke:Autotaxobox|listAll|*}} only returns 2 entries when Template:Taxonomy/* is corrupted. + if ( taxa.length < 3 ) { + throw new Error(); + } // get rid of both -Genus and /Plantae taxa = taxa.map( ( str ) => str.replace( /[-/].*?$/, '' ) ); return taxa; diff --git a/SpeciesHelper/tests/SpeciesHelperFunctions.test.js b/SpeciesHelper/tests/SpeciesHelperFunctions.test.js index df9e5c0..4425a85 100644 --- a/SpeciesHelper/tests/SpeciesHelperFunctions.test.js +++ b/SpeciesHelper/tests/SpeciesHelperFunctions.test.js @@ -108,6 +108,84 @@ describe( 'getPagesToCheck(taxa, listOfNonLatinSpeciesCategories)', () => { } ); } ); +describe( 'taxaStringToArray(taxa)', () => { + test( 'Handles underscore in "unranked_classis"', () => { + // {{#invoke:Autotaxobox|listAll|Macratria}} + const taxa = 'Macratria-genus, Macratriinae-subfamilia, Anthicidae-familia, Tenebrionoidea-superfamilia, Cucujiformia-infraordo, Polyphaga-subordo, Coleoptera-ordo, Coleopterida-cladus, Neuropteroidea-cladus, Aparaglossata-clade, Holometabola-clade, Eumetabola-clade, Neoptera-infraclassis, Pterygota-subclassis, Dicondylia-unranked, Insecta-classis, Hexapoda-subphylum, Allotriocarida-clade, Pancrustacea-clade, Mandibulata-clade, Deuteropoda-unranked_classis, Arthropoda-phylum, Panarthropoda-clade, Ecdysozoa-superphylum, Protostomia-clade, Nephrozoa-clade, Bilateria-clade, ParaHoxozoa-cladus, Eumetazoa-subregnum, Animalia-regnum, Choanozoa-cladus, Filozoa-clade, Holozoa-clade, Opisthokonta-clade, Obazoa-cladus, Amorphea-cladus, Podiata-clade, Eukaryota-domain, Life-'; + const output = [ + 'Macratria', + 'Macratriinae', + 'Anthicidae', + 'Tenebrionoidea', + 'Cucujiformia', + 'Polyphaga', + 'Coleoptera', + 'Coleopterida', + 'Neuropteroidea', + 'Aparaglossata', + 'Holometabola', + 'Eumetabola', + 'Neoptera', + 'Pterygota', + 'Dicondylia', + 'Insecta', + 'Hexapoda', + 'Allotriocarida', + 'Pancrustacea', + 'Mandibulata', + 'Deuteropoda', + 'Arthropoda', + 'Panarthropoda', + 'Ecdysozoa', + 'Protostomia', + 'Nephrozoa', + 'Bilateria', + 'ParaHoxozoa', + 'Eumetazoa', + 'Animalia', + 'Choanozoa', + 'Filozoa', + 'Holozoa', + 'Opisthokonta', + 'Obazoa', + 'Amorphea', + 'Podiata', + 'Eukaryota' + ]; + expect( f.taxaStringToArray( taxa ) ).toStrictEqual( output ); + } ); + + test( 'Removes "/Plantae"', () => { + // {{#invoke:Autotaxobox|listAll|Cycas}} + const taxa = 'Cycas-genus, Cycadaceae-familia, Cycadineae-subordo, Cycadales-ordo, Cycadopsida-classis, Cycadophyta-divisio, Gymnosperms-cladus, Spermatophytes/Plantae-clade, Tracheophytes/Plantae-clade, Polysporangiophytes/Plantae-clade, Embryophytes/Plantae-clade, Plantae-regnum, Archaeplastida-clade, CAM-clade, Diaphoretickes-cladus, Eukaryota-domain, Life-'; + const output = [ + 'Cycas', + 'Cycadaceae', + 'Cycadineae', + 'Cycadales', + 'Cycadopsida', + 'Cycadophyta', + 'Gymnosperms', + 'Spermatophytes', + 'Tracheophytes', + 'Polysporangiophytes', + 'Embryophytes', + 'Plantae', + 'Archaeplastida', + 'CAM', + 'Diaphoretickes', + 'Eukaryota' + ]; + expect( f.taxaStringToArray( taxa ) ).toStrictEqual( output ); + } ); + + test( 'Throws error when result array is too small. Means that Template:Taxonomy/* is corrupted.', () => { + // {{#invoke:Autotaxobox|listAll|Sandbox}} with Template:Taxonomy/Sandbox's wikitext being `Test`. + const taxa = 'Sandbox-test, Test-'; + expect( () => f.taxaStringToArray( taxa ) ).toThrow( Error ); + } ); +} ); + describe( 'deleteAllStubs(wikicode)', () => { test( '1 stub', () => { const wikicode = '{{Rayfinned-fish-stub}}\n'; diff --git a/Superlinks.js b/Superlinks.js new file mode 100644 index 0000000..ae8ae02 --- /dev/null +++ b/Superlinks.js @@ -0,0 +1,679 @@ +// Forked from https://en.wikipedia.org/w/index.php?title=User:Bradv/Scripts/Superlinks.js&oldid=1171577516, with thanks to the author of that user script, Bradv. + +/* +Changes: +- added support for most skins, not just Vector 2010 +- fixed a bug that was breaking article superlinks (talk and user were working OK) +- ran eslint-config-wikimedia linter on the code +- refactored the code to use function keyword, to be flatter (no nested functions), to split apart big functions into multiple smaller functions, and to change short function/variable names to be more descriptive +*/ + +/* eslint-disable no-irregular-whitespace, no-jquery/no-constructor-attributes, no-jquery/no-done-fail, no-jquery/no-each-util */ + +( function ( $, mw ) { + const app = { + links: {} + }; + + function execute() { + // It shows up in modern, but in the top right (hard to see), and in the panel the text is hard to read. Color problems. Disabling in modern for now. + // It doesn't show up at all in minerva. No mw-indicators element perhaps? + // It shows up in CologneBlue. The panels are an inch too high. Going to leave it enabled though. It mostly works. + const allowedSkins = [ 'vector', 'vector-2022', 'monobook', 'timeless', 'cologneblue' ]; + if ( !allowedSkins.includes( mw.config.get( 'skin' ) ) ) { + return; + } + + loadCss(); + placeHtmlAndClickListeners(); + } + + function loadCss() { + mw.util.addCSS( ` + .vector-body-before-content { + /* needed for vector-2022, since .vector-body-before-content is overflow: hidden */ + overflow: visible; + } + #superlinks { + display:inline-block; + line-height: 24px; + } + #superlinks-links { + font-size: 0.75em; + color:#555; + } + #superlinks-links label { + padding: 0 0.2em; + } + #superlinks-links > span:before { + content: '('; + } + #superlinks-links > span:after { + content: ') '; + } + #superlinks-links > span > a { + text-transform: lowercase; + } + #superlinks-links a { + padding: 0 0.2em; + color: #0645ad !important; + text-decoration: none !important; + } + #superlinks-links a.active, + #superlinks-links a:hover { + border-bottom: 2px solid #a7d7f9; + } + #superlinks-window { + position: absolute; + top: 48px; + right: 5px; + border: 2px solid #a7d7f9; + width: 45vw; + height: 75vh; + box-shadow: 0px 0px 4px 2px #eee; + z-index: 1; + } + #superlinks-window:before { + content: ''; + position: absolute; + top: -20px; + width: 0; + height: 0; + border: 10px solid transparent; + border-bottom-color: #a7d7f9; + margin-right: -16px; + } + #superlinks-window:after { + content: ''; + position: absolute; + top: -17px; + width: 0; + height: 0; + border: 10px solid transparent; + border-bottom-color: transparent; + margin-right: -16px; + } + #superlinks-icons { + position:absolute; + left:-2px; + top:-14px; + font-size:10px; + line-height:10px; + background-color:#a7d7f9; + padding: 2px 8px; + } + #superlinks-icons a:not(:first-child) { + margin-left: 20px; + } + #superlinks-icons img { + height: 8px; + margin-top: -1px; + } + #superlinks-content { + overflow: auto; + height: calc(100% - 10px); + width: calc(100% - 10px); + font-size: 90%; + padding:5px; + background-color: #f6fbfe; + } + #superlinks-content > ul#pagehistory > li > input { + display: none; + } + #superlinks-content > #empty { + position:absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: #a7d7f9; + font-size: 1.5em; + font-weight: bold; + text-align:center; + } + #superlinks-content > #empty img { + width: 30px; + height: 30px; + opacity:0.5; + } + #superlinks-content > #empty #empty-sub { + font-size: 0.8em; + font-weight: normal; + padding-top: 1em; + } + #superlinks-content > #empty #empty-sub a { + opacity: 0.5; + } + #superlinks-content .mw-changeslist-separator:empty:before { + content: '. .'; + } + #superlinks-content .mw-changeslist-links { + display: inline-block; + margin: 0; + padding: 0; + } + #superlinks-content .mw-changeslist-links:before { + content: '('; + } + #superlinks-content .mw-changeslist-links:after { + content: ')'; + } + #superlinks-content .mw-changeslist-links > span:not(:first-child):before { + content: ' | '; + } + #superlinks-content .snippet { + line-height: 1.5em; + margin-bottom: 1.5em; + } + #superlinks-content .searchmatch { + font-weight: bold; + } + ` ); + } + + function makeLink( caption, $group ) { + let htmlClass = 'superlinks-link-'; + if ( $group ) { + htmlClass += $group.prev().text().toLowerCase().replace( /\s/g, '-' ) + '-'; + } + htmlClass += caption.toLowerCase().replace( /\s/g, '-' ); + + const $link = $( '', { + href: '#', + class: htmlClass + } ).text( caption ); + + if ( $group ) { + $group.append( $link ); + } else { + $( '#superlinks-links' ).append( $link ); + } + + $link.on( 'click', ( e ) => { + handleClick( e ); + } ); + + return $link[ 0 ]; + } + + /** + * @return {jQuery} + */ + function makeLinkGroup( caption ) { + if ( caption ) { + $( '#superlinks-links' ).append( $( '
', { id: 'superlinks' } ).text( '[' ).append( $( '', { id: 'superlinks-links' } ) ); + app.$root.prependTo( $( 'div.mw-indicators' ) ); + + if ( isSpecialPage ) { // on special page, like contribs + if ( app.relevantUser ) { // on a page associated with a specific user, such as Special:Contributions/TheirName + makeSpecialPageLinks(); + } + } else { + if ( app.$articleElement.hasClass( 'selected' ) ) { // on article or article history + makeArticleOrArticleHistoryLinks(); + } else { // on talk or talk history + makeTalkOrTalkHistoryLinks(); + } + + if ( app.relevantUser ) { + makeUserLinks(); + } + + const ns = mw.config.get( 'wgNamespaceNumber' ); + if ( mw.config.get( 'wgWikiID' ) == 'enwiki' && ns == 0 && mw.loader.getState( 'ext.pageTriage.util' ) != 'registered' ) { + app.links.nppflowchart = makeLink( 'NPP Flowchart' ); + } + + if ( mw.config.get( 'wgWikiID' ) == 'enwiki' && mw.config.get( 'wgCategories' ).includes( 'Pending AfC submissions' ) ) { + app.links.afcflowchart = makeLink( 'AfC Flowchart' ); + } + } + + app.$root.append( ']' ); + } + + function putMessageInPanel( text ) { // blank for loading icon + app.$content.empty(); + const $div = $( '
', { id: 'empty' } ); + if ( text ) { + $div.text( text ); + } else { + $div.append( $( '', { src: 'https://upload.wikimedia.org/wikipedia/commons/3/30/Chromiumthrobber.svg' } ) ); + } + app.$content.append( $div ); + } + + function putErrorMessageInPanel() { + app.$content.empty().append( $( ` + + ` ) ); + } + + function doFinalPanelLoadingTasks() { + if ( app.$content.children().length === 0 ) { + putMessageInPanel( 'No results' ); + } + app.$content.find( 'a[href]' ).not( '[href^="#"]' ).attr( 'target', '_blank' ); + app.$content.find( 'input' ).remove(); + try { + app.$content.find( '.mw-collapsible' ).makeCollapsible(); + } catch ( e ) {} + } + + function checkForErrors( response, status ) { + if ( status == 'success' ) { + doFinalPanelLoadingTasks(); + } else { + putErrorMessageInPanel(); + } + } + + function openPanel( url ) { + app.$root.find( '#superlinks-popout' ).remove(); + if ( url ) { + app.$root.find( '#superlinks-icons' ).append( + $( '', { id: 'superlinks-popout', href: url, target: '_blank' } ).append( + $( '', { src: 'https://upload.wikimedia.org/wikipedia/commons/e/eb/OOjs_UI_icon_newWindow-ltr.svg' } ) + ) + ); + } + } + + function handleClick( e ) { + e.preventDefault(); + if ( app.active == e.target ) { + closeAllPanels( e ); + } else { + closeAllPanels( e ); + app.active = e.target; + $( app.active ).addClass( 'active' ); + + app.$window = $( '
', { id: 'superlinks-window' } ).appendTo( app.$root ); + + const el = $( e.target )[ 0 ]; + const pos = el.offsetParent.offsetWidth - el.offsetLeft - ( el.offsetWidth / 2 ); + app.styleSheetFragment = mw.util.addCSS( '#superlinks-window:before {right: ' + pos + 'px} #superlinks-window:after {right: ' + pos + 'px}' ); + + // Draw the outline of the panel + app.$window.append( + $( '', { id: 'superlinks-icons' } ) + .append( + $( '', { href: '#' } ) + .text( 'close' ) + .on( 'click', ( e ) => closeAllPanels( e ) ) + ) + ); + + // Draw the inside of the panel, including a spinner + app.$content = $( '
', { id: 'superlinks-content' } ).appendTo( app.$window ); + putMessageInPanel(); + + // Run the API query and put real content in the panel + let url, $obj; + switch ( e.target ) { + case app.links.userpage: + url = '/w/index.php?action=render&title=User:' + app.relevantUser; + app.$content.load( url, checkForErrors ); + openPanel( '/wiki/User:' + app.relevantUser ); + break; + case app.links.usertalk: + url = '/w/index.php?action=render&title=User_talk:' + app.relevantUser; + app.$content.load( url, checkForErrors ); + openPanel( '/wiki/User_talk:' + app.relevantUser ); + break; + case app.links.article: + url = app.$articleElement.find( 'a' )[ 0 ].href.replace( '/wiki/', '/w/index.php?action=render&title=' ); + app.$content.load( url, checkForErrors ); + openPanel( app.$articleElement.find( 'a' )[ 0 ].href ); + break; + case app.links.articleHistory: + url = app.$articleElement.find( 'a' )[ 0 ].href.replace( '/wiki/', '/w/index.php?action=history&safemode=1&limit=100&title=' ); + app.$content.load( url + ' #pagehistory', checkForErrors ); + openPanel( app.$articleElement.find( 'a' )[ 0 ].href.replace( '/wiki/', '/w/index.php?action=history&title=' ) ); + break; + case app.links.talk: + url = $( '#ca-talk > a' )[ 0 ].href.replace( '/wiki/', '/w/index.php?action=render&title=' ); + app.$content.load( url, checkForErrors ); + openPanel( $( '#ca-talk > a' )[ 0 ].href ); + break; + case app.links.history: + url = $( '#ca-history > a' )[ 0 ].href + '&safemode=1&limit=100'; + app.$content.load( url + ' #pagehistory', checkForErrors ); + openPanel( $( '#ca-history > a' )[ 0 ].href ); + break; + case app.links.articleLogs: + url = app.$articleElement.find( 'a' )[ 0 ].href.replace( '/wiki/', '/wiki/Special:Log?wpfilters%5B%5D=patrol&safemode=1&limit=100&page=' ); + app.$content.load( url + ' #mw-content-text ul', checkForErrors ); + openPanel( app.$articleElement.find( 'a' )[ 0 ].href.replace( '/wiki/', '/wiki/Special:Log?wpfilters%5B%5D=patrol&page=' ) ); + break; + case app.links.logs: + url = '/w/index.php?title=Special:Log&wpfilters%5B%5D=patrol&page=' + mw.config.get( 'wgPageName' ) + '&safemode=1&limit=100'; + app.$content.load( url + ' #mw-content-text ul', checkForErrors ); + openPanel( '/w/index.php?title=Special:Log&wpfilters%5B%5D=patrol&page=' + mw.config.get( 'wgPageName' ) ); + break; + case app.links.notice: + url = app.$articleElement.find( 'a' )[ 0 ].href.replace( '/wiki/', '/w/index.php?action=render&title=Template:Editnotices/Page/' ); + app.$content.load( url, checkForErrors ); + openPanel( url.replace( 'action=render&', '' ) ); + break; + case app.links.contribs: + url = '/w/index.php?title=Special:Contributions/' + app.relevantUser + '&safemode=1&limit=100'; + app.$content.load( url + ' #mw-content-text ul.mw-contributions-list', checkForErrors ); + openPanel( '/w/index.php?title=Special:Contributions/' + app.relevantUser ); + break; + case app.links.deleted: + url = '/w/index.php?title=Special:DeletedContributions/' + app.relevantUser + '&safemode=1&limit=100'; + app.$content.load( url + ' #mw-content-text > section', checkForErrors ); + openPanel( '/w/index.php?title=Special:DeletedContributions/' + app.relevantUser ); + break; + case app.links.actions: + url = '/wiki/Special:Log/' + app.relevantUser; + app.$content.load( url + ' #mw-content-text ul', checkForErrors ); + openPanel( '/wiki/Special:Log/' + app.relevantUser ); + break; + case app.links.blocklog: + url = '/w/index.php?title=Special:Log/block&page=' + app.relevantUser + '&safemode=1'; + app.$content.load( url + ' #mw-log-deleterevision-submit ul', checkForErrors ); + openPanel( '/w/index.php?title=Special:Log/block&page=' + app.relevantUser ); + break; + case app.links.filter: + url = '/w/index.php?title=Special:AbuseLog&wpSearchTitle=' + mw.config.get( 'wgPageName' ) + '&safemode=1'; + app.$content.load( url + ' #mw-content-text ul', checkForErrors ); + openPanel( '/w/index.php?title=Special:AbuseLog&wpSearchTitle=' + mw.config.get( 'wgPageName' ) ); + break; + case app.links.articleFilter: + url = app.$articleElement.find( 'a' )[ 0 ].href.replace( '/wiki/', '/wiki/Special:AbuseLog?safemode=1&wpSearchTitle=' ); + app.$content.load( url + ' #mw-content-text ul', checkForErrors ); + openPanel( app.$articleElement.find( 'a' )[ 0 ].href.replace( '/wiki/', '/wiki/Special:AbuseLog?wpSearchTitle=' ) ); + break; + case app.links.userFilter: + url = '/w/index.php?title=Special:AbuseLog&wpSearchUser=' + app.relevantUser + '&safemode=1'; + app.$content.load( url + ' #mw-content-text ul', checkForErrors ); + openPanel( '/w/index.php?title=Special:AbuseLog&wpSearchUser=' + app.relevantUser ); + break; + case app.links.rights: + url = '/w/index.php?title=Special:UserRights&user=' + app.relevantUser + '&safemode=1'; + app.$content.load( url + ' #mw-content-text > ul', checkForErrors ); + openPanel( '/wiki/Special:UserRights/' + app.relevantUser ); + break; + case app.links.culog: + url = '/w/index.php?title=Special:CheckUserLog&cuSearchType=target&cuSearch=' + app.relevantUser; + app.$content.load( url + ' #mw-content-text > ul', checkForErrors ); + openPanel( '/w/index.php?title=Special:CheckUserLog&cuSearchType=target&cuSearch=' + app.relevantUser ); + break; + case app.links.nppflowchart: + $obj = $( '', { + id: 'nppsvg', + type: 'image/svg+xml', + style: 'width: 100%' + } ); + app.$content.empty().append( $obj ); + $obj.attr( 'data', 'https://upload.wikimedia.org/wikipedia/commons/f/f4/NPP_flowchart.svg' ); + break; + case app.links.afcflowchart: + app.$content.empty().append( + $( '', { + src: 'https://upload.wikimedia.org/wikipedia/en/a/a8/Flow_chart_for_AFC_3.1.png', + style: 'width: 100%' + } ) + ); + break; + case app.links.dsalerts: + openCtopAlertPanel(); + break; + case app.links.restrict: + openEditingRestrictionsPanel(); + break; + } + + app.$keyup = $( 'body' ).on( 'keyup', ( event ) => { + const escapeKey = 27; + if ( event.which == escapeKey ) { + closeAllPanels( event ); + } + } ); + } + } + + function openEditingRestrictionsPanel() { + const $editingRestrictions = $( '
' ).appendTo( app.$content ); + $.getJSON( '/w/api.php', { + action: 'query', + format: 'json', + list: 'search', + srsearch: 'User:' + app.relevantUser + ' prefix:Wikipedia:Editing restrictions/', + srnamespace: 4 + } ) + .done( ( data ) => { + app.$content.find( '#empty' ).remove(); + if ( data.query.search.length ) { + $editingRestrictions.append( $( '

' ) + .append( '', { class: 'mw-headline' } ) + .text( 'Search results: Wikipedia:Editing restrictions' ) ); + $.each( data.query.search, ( i, result ) => { + $editingRestrictions.append( $( '

' ) + .append( $( '', { + href: '/wiki/' + result.title.replace( / /g, '_' ) + '#:~:text=' + app.relevantUser, + title: result.title + } ).text( result.title ) ) ); + $editingRestrictions.append( $( '

' + result.snippet + '

' ) ); + $editingRestrictions.append( $( '
' ) ); + } ); + } else { + $editingRestrictions.remove(); + } + doFinalPanelLoadingTasks(); + } ) + .fail( () => { + putErrorMessageInPanel(); + } ); + + const $arbitrationEnforcement = $( '
' ).appendTo( app.$content ); + $.getJSON( '/w/api.php', { + action: 'query', + format: 'json', + list: 'search', + srsearch: 'User:' + app.relevantUser + ' prefix:Wikipedia:Arbitration enforcement log/', + srnamespace: 4 + } ) + .done( ( data ) => { + app.$content.find( '#empty' ).remove(); + if ( data.query.search.length ) { + $arbitrationEnforcement.append( $( '

' ) + .append( '', { class: 'mw-headline' } ) + .text( 'Search results: Wikipedia:Arbitration enforcement log' ) ); + $.each( data.query.search, ( i, result ) => { + $arbitrationEnforcement.append( $( '

' ) + .append( $( '', { + href: '/wiki/' + result.title.replace( / /g, '_' ) + '#:~:text=' + app.relevantUser, + title: result.title + } ).text( result.title ) ) ); + $arbitrationEnforcement.append( $( '

' + result.snippet + '

' ) ); + $arbitrationEnforcement.append( $( '
' ) ); + } ); + } else { + $arbitrationEnforcement.remove(); + } + doFinalPanelLoadingTasks(); + } ) + .fail( () => { + putErrorMessageInPanel(); + } ); + } + + function openCtopAlertPanel() { + openPanel( '/wiki/Special:AbuseLog?wpSearchTitle=User_talk%3A' + app.relevantUser + '&wpSearchFilter=602' ); + $.getJSON( '/w/api.php', { + action: 'query', + format: 'json', + list: 'abuselog', + afldir: 'older', + afltitle: 'User talk:' + app.relevantUser, + aflfilter: 602 + } ) + .done( ( data ) => { + app.$content.empty(); + if ( data.query.abuselog.filter( ( item ) => ( item.revid ) ).length ) { + $.each( data.query.abuselog, ( i, item ) => { + if ( item.result == 'tag' && item.revid ) { + const $item = $( '
' ).appendTo( app.$content ); + $.getJSON( '/w/api.php', { + action: 'compare', + format: 'json', + fromrev: item.revid, + torelative: 'prev', + prop: 'user|comment|diff' + } ) + .done( ( comparedata ) => { + let ts = new Date( item.timestamp ).toUTCString(); + ts = ts.slice( 5, ts.indexOf( 'GMT' ) - 1 ); + const sum = ts + ' [[User:' + comparedata.compare.touser + '|' + comparedata.compare.touser + ']] ([[Special:Diff/' + item.revid + '|diff]])\n'; + + let diff = ''; + $( comparedata.compare[ '*' ] ).find( 'td.diff-addedline > div' ).each( ( i, item ) => { + diff += $( item ).html() + '\n'; + } ); + + diff = $( ' + - + - +