MediaWiki:Gadget-ArticleReview.js
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
( function () {
'use strict';
var rootId = 'shalombux-article-review';
function element( name, className, text ) {
var node = document.createElement( name );
if ( className ) {
node.className = className;
}
if ( text !== undefined ) {
node.textContent = String( text );
}
return node;
}
function safeLink( label, address, newWindow ) {
var url;
try {
url = new URL( address, window.location.href );
} catch ( error ) {
return null;
}
if ( url.protocol !== 'https:' && url.protocol !== 'http:' ) {
return null;
}
var link = element( 'a', '', label );
link.href = url.href;
if ( newWindow ) {
link.target = '_blank';
link.rel = 'noopener noreferrer';
}
return link;
}
function initialize() {
var data = window.shalombuxArticleReviewData;
var query = new URLSearchParams( window.location.search );
var pageId = Number( mw.config.get( 'wgArticleId' ) );
var viewedRevision = Number( mw.config.get( 'wgRevisionId' ) );
if ( document.getElementById( rootId ) || !data || !data.pages ||
mw.config.get( 'wgNamespaceNumber' ) !== 0 || pageId <= 0 || viewedRevision <= 0 ||
mw.config.get( 'wgAction' ) !== 'view' || query.has( 'diff' ) || query.has( 'veaction' ) ||
mw.config.get( 'wgDiffOldId' ) || mw.config.get( 'wgDiffNewId' ) ) {
return;
}
var review = data.pages[ String( pageId ) ];
if ( !review || !Number( review.reviewedRevision ) ) {
return;
}
var reviewedRevision = Number( review.reviewedRevision );
var currentRevision = Number( mw.config.get( 'wgCurRevisionId' ) ) || viewedRevision;
var storageKey = 'shalombux-article-review:' + String( data.version ) + ':' +
pageId + ':' + reviewedRevision;
var dismissed = false;
try {
dismissed = window.localStorage.getItem( storageKey ) === 'dismissed';
} catch ( error ) {
// The card remains usable when browser storage is unavailable.
}
var root = element( 'div', 'sb-article-review' );
root.id = rootId;
var badge = element( 'button', 'sb-article-review__badge', 'Article review' );
badge.type = 'button';
badge.setAttribute( 'aria-controls', rootId + '-panel' );
badge.setAttribute( 'aria-expanded', 'false' );
badge.title = 'Show the article review';
var panel = element( 'section', 'sb-article-review__panel' );
panel.id = rootId + '-panel';
panel.setAttribute( 'aria-labelledby', rootId + '-heading' );
var header = element( 'div', 'sb-article-review__header' );
var heading = element( 'h2', 'sb-article-review__heading', 'Article review' );
heading.id = rootId + '-heading';
var close = element( 'button', 'sb-article-review__close', '\u00d7' );
close.type = 'button';
close.setAttribute( 'aria-label', 'Close article review' );
close.title = 'Close article review (Escape)';
header.appendChild( heading );
header.appendChild( close );
panel.appendChild( header );
var body = element( 'div', 'sb-article-review__body' );
body.appendChild( element( 'p', 'sb-article-review__date', 'Reviewed ' + String( data.reviewDate ) ) );
if ( viewedRevision !== reviewedRevision || currentRevision !== reviewedRevision ) {
var stale = viewedRevision !== reviewedRevision ?
'This review covers a different revision. The text you are viewing has not been rechecked; the findings below may no longer apply.' :
'You are viewing the reviewed revision. A newer revision exists and has not been rechecked.';
body.appendChild( element( 'p', 'sb-article-review__stale', stale ) );
}
var revisionLink = safeLink( 'View reviewed revision ' + reviewedRevision,
mw.util.getUrl( review.title || mw.config.get( 'wgPageName' ), { oldid: reviewedRevision } ), false );
if ( revisionLink ) {
var revisionParagraph = element( 'p', 'sb-article-review__revision' );
revisionParagraph.appendChild( revisionLink );
body.appendChild( revisionParagraph );
}
var issues = Array.isArray( review.issues ) ? review.issues : [];
if ( issues.length ) {
var list = element( 'ul', 'sb-article-review__issues' );
issues.forEach( function ( issue ) {
if ( !issue || typeof issue !== 'object' ) {
return;
}
var kind = [ 'error', 'review', 'note' ].indexOf( issue.kind ) !== -1 ? issue.kind : 'note';
var labels = { error: 'Verified error', review: 'Review needed', note: 'Note' };
var item = element( 'li', 'sb-article-review__issue sb-article-review__issue--' + kind );
item.appendChild( element( 'span', 'sb-article-review__kind', labels[ kind ] ) );
item.appendChild( element( 'strong', 'sb-article-review__issue-title', issue.title || 'Review note' ) );
item.appendChild( element( 'p', 'sb-article-review__detail', issue.detail || '' ) );
if ( Array.isArray( issue.links ) ) {
var links = element( 'div', 'sb-article-review__links' );
issue.links.forEach( function ( source ) {
if ( !source || typeof source.url !== 'string' ) {
return;
}
var link = safeLink( source.label || 'Source', source.url, true );
if ( link ) {
links.appendChild( link );
}
} );
if ( links.childNodes.length ) {
item.appendChild( links );
}
}
list.appendChild( item );
} );
body.appendChild( list );
} else {
body.appendChild( element( 'p', 'sb-article-review__no-issues',
'No article-specific issue recorded in this limited review.' ) );
}
body.appendChild( element( 'p', 'sb-article-review__ai',
'AI indicators: none found in reviewed text/links; authorship unverified.' ) );
var scope = element( 'details', 'sb-article-review__scope' );
scope.appendChild( element( 'summary', '', 'Review scope' ) );
scope.appendChild( element( 'p', 'sb-article-review__unicode', review.unicodeNote ||
'Unicode scan: no flagged hidden characters found in the reviewed source.' ) );
scope.appendChild( element( 'p', 'sb-article-review__coverage',
review.sourceReview === 'spot-checked' ?
'Selected citations were spot-checked; other sources remain unverified.' :
'Citation destinations were not checked for this article.' ) );
scope.appendChild( element( 'p', 'sb-article-review__coverage',
'Wikipedia policy links are review guidance; no complete policy assessment.' ) );
body.appendChild( scope );
panel.appendChild( body );
root.appendChild( badge );
root.appendChild( panel );
var isOpen = false;
var hoverBlocked = false;
var hoverMayOpenAfter = 0;
var suppressFocus = false;
var suspended = false;
function rememberDismissal( value ) {
try {
if ( value ) {
window.localStorage.setItem( storageKey, 'dismissed' );
} else {
window.localStorage.removeItem( storageKey );
}
} catch ( error ) {
// Dismissal still works for the current page without storage.
}
}
function openPanel( remember ) {
if ( suspended ) {
return;
}
isOpen = true;
panel.hidden = false;
badge.setAttribute( 'aria-expanded', 'true' );
if ( remember ) {
rememberDismissal( false );
}
}
function closePanel( returnFocus ) {
isOpen = false;
panel.hidden = true;
badge.setAttribute( 'aria-expanded', 'false' );
hoverBlocked = true;
hoverMayOpenAfter = Date.now() + 800;
rememberDismissal( true );
if ( returnFocus ) {
suppressFocus = true;
badge.focus( { preventScroll: true } );
suppressFocus = false;
}
}
close.addEventListener( 'click', function () {
closePanel( true );
} );
badge.addEventListener( 'click', function () {
openPanel( true );
} );
badge.addEventListener( 'focus', function () {
if ( !suppressFocus ) {
openPanel( false );
}
} );
badge.addEventListener( 'mouseenter', function () {
if ( !hoverBlocked && Date.now() >= hoverMayOpenAfter &&
window.matchMedia( '(hover: hover)' ).matches ) {
openPanel( false );
}
} );
root.addEventListener( 'mouseleave', function () {
hoverBlocked = false;
} );
document.addEventListener( 'keydown', function ( event ) {
if ( event.key === 'Escape' && isOpen && !suspended ) {
var focusInside = root.contains( document.activeElement );
closePanel( focusInside );
if ( focusInside ) {
event.preventDefault();
}
}
} );
function updateEditorVisibility() {
var editorClasses = [ 've-active', 've-activated', 've-activating', 've-deactivating' ];
suspended = editorClasses.some( function ( name ) {
return document.documentElement.classList.contains( name ) || document.body.classList.contains( name );
} );
root.hidden = suspended;
}
mw.hook( 've.activationStart' ).add( function () {
suspended = true;
root.hidden = true;
} );
mw.hook( 've.deactivationComplete' ).add( updateEditorVisibility );
if ( window.MutationObserver ) {
var observer = new MutationObserver( updateEditorVisibility );
observer.observe( document.documentElement, { attributes: true, attributeFilter: [ 'class' ] } );
observer.observe( document.body, { attributes: true, attributeFilter: [ 'class' ] } );
}
// Keep review UI outside article content and VisualEditor serialization.
document.body.appendChild( root );
panel.hidden = dismissed;
if ( !dismissed ) {
openPanel( false );
}
updateEditorVisibility();
}
mw.loader.using( 'mediawiki.util' ).then( function () {
if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', initialize, { once: true } );
} else {
initialize();
}
} );
}() );