MediaWiki:Gadget-ArticleReview.js: Difference between revisions
Add dismissible article review interface |
Add local writing review with passage evidence and optional draft checks |
||
| Line 3: | Line 3: | ||
var rootId = 'shalombux-article-review'; | var rootId = 'shalombux-article-review'; | ||
var editorId = 'mayapedia-writing-review-editor'; | |||
var guideUrl = 'https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing'; | |||
var viewCard, editorCard, activeTarget, sourceInput, activeDocument; | |||
var editorGeneration = 0, requestSerial = 0; | |||
var maxFindings = 80; | |||
function element( name, className, text ) { | function element( name, className, text ) { | ||
| Line 34: | Line 39: | ||
} | } | ||
function | function paragraphLink( parent, label, address ) { | ||
var | var link = safeLink( label, address, true ); | ||
if ( link ) { | |||
parent.appendChild( link ); | |||
if ( | |||
} | } | ||
} | |||
var root = element( 'div', 'sb-article-review' ); | function makeCard( id, label, storageKey, editor ) { | ||
root.id = | var root = element( 'div', 'sb-article-review' + ( editor ? ' sb-article-review--editor' : '' ) ); | ||
var badge = element( 'button', 'sb-article-review__badge', | root.id = id; | ||
var badge = element( 'button', 'sb-article-review__badge', label ); | |||
badge.type = 'button'; | badge.type = 'button'; | ||
badge.setAttribute( 'aria-controls', | badge.setAttribute( 'aria-controls', id + '-panel' ); | ||
badge.setAttribute( 'aria-expanded', 'false' ); | badge.setAttribute( 'aria-expanded', 'false' ); | ||
badge.title = 'Show the article review'; | badge.title = editor ? 'Check the current unsaved writing' : 'Show the article review'; | ||
var panel = element( 'section', 'sb-article-review__panel' ); | var panel = element( 'section', 'sb-article-review__panel' ); | ||
panel.id = | panel.id = id + '-panel'; | ||
panel.setAttribute( 'aria-labelledby', | panel.hidden = true; | ||
panel.setAttribute( 'aria-labelledby', id + '-heading' ); | |||
var header = element( 'div', 'sb-article-review__header' ); | var header = element( 'div', 'sb-article-review__header' ); | ||
var heading = element( 'h2', 'sb-article-review__heading', 'Article review' ); | var heading = element( 'h2', 'sb-article-review__heading', editor ? 'Writing review' : 'Article review' ); | ||
heading.id = | heading.id = id + '-heading'; | ||
var close = element( 'button', 'sb-article-review__close', '\u00d7' ); | var close = element( 'button', 'sb-article-review__close', '\u00d7' ); | ||
close.type = 'button'; | close.type = 'button'; | ||
close.setAttribute( 'aria-label', 'Close | close.setAttribute( 'aria-label', 'Close ' + heading.textContent.toLowerCase() ); | ||
close.title = 'Close | close.title = 'Close review (Escape)'; | ||
header.appendChild( heading ); | header.appendChild( heading ); | ||
header.appendChild( close ); | header.appendChild( close ); | ||
panel.appendChild( header ); | panel.appendChild( header ); | ||
var body = element( 'div', 'sb-article-review__body' ); | var body = element( 'div', 'sb-article-review__body' ); | ||
body.appendChild( | panel.appendChild( body ); | ||
root.appendChild( badge ); | |||
root.appendChild( panel ); | |||
var dismissed = false, hoverBlocked = false, hoverAfter = 0, suppressFocus = false; | |||
try { | |||
dismissed = storageKey && window.localStorage.getItem( storageKey ) === 'dismissed'; | |||
} catch ( error ) { | |||
// The card also works when browser storage is unavailable. | |||
} | |||
function remember( value ) { | |||
if ( !storageKey ) { | |||
return; | |||
} | |||
try { | |||
if ( value ) { | |||
window.localStorage.setItem( storageKey, 'dismissed' ); | |||
} else { | |||
window.localStorage.removeItem( storageKey ); | |||
} | |||
} catch ( error ) { | |||
// Dismissal still works until this page is closed. | |||
} | |||
} | |||
function open( rememberOpen ) { | |||
if ( root.hidden ) { | |||
return; | |||
} | |||
panel.hidden = false; | |||
badge.setAttribute( 'aria-expanded', 'true' ); | |||
if ( rememberOpen ) { | |||
remember( false ); | |||
} | |||
} | |||
function closePanel( returnFocus ) { | |||
panel.hidden = true; | |||
badge.setAttribute( 'aria-expanded', 'false' ); | |||
hoverBlocked = true; | |||
hoverAfter = Date.now() + 800; | |||
remember( true ); | |||
if ( returnFocus ) { | |||
suppressFocus = true; | |||
badge.focus( { preventScroll: true } ); | |||
suppressFocus = false; | |||
} | |||
} | } | ||
close.addEventListener( 'click', function () { closePanel( true ); } ); | |||
badge.addEventListener( 'click', function () { open( true ); } ); | |||
if ( | if ( !editor ) { | ||
badge.addEventListener( 'focus', function () { | |||
if ( !suppressFocus ) { | |||
open( false ); | |||
} | |||
} ); | |||
badge.addEventListener( 'mouseenter', function () { | |||
if ( !hoverBlocked && Date.now() >= hoverAfter && window.matchMedia( '(hover: hover)' ).matches ) { | |||
open( false ); | |||
} | |||
} ); | |||
} | } | ||
root.addEventListener( 'mouseleave', function () { hoverBlocked = false; } ); | |||
document.addEventListener( 'keydown', function ( event ) { | |||
if ( event.key === 'Escape' && !root.hidden && !panel.hidden ) { | |||
var focusInside = root.contains( document.activeElement ); | |||
closePanel( focusInside ); | |||
if ( focusInside ) { | |||
event.preventDefault(); | |||
} | |||
} | |||
} ); | |||
// The controls are outside article content and editor serialization. | |||
document.body.appendChild( root ); | |||
return { root: root, body: body, badge: badge, panel: panel, open: open, dismissed: dismissed }; | |||
} | |||
function renderManual( body, review, data, viewedRevision ) { | |||
if ( !review || !Number( review.reviewedRevision ) ) { | |||
return; | |||
} | |||
var revision = Number( review.reviewedRevision ); | |||
body.appendChild( element( 'h3', 'sb-article-review__section', 'Recorded article review' ) ); | |||
body.appendChild( element( 'p', 'sb-article-review__date', 'Reviewed ' + String( data.reviewDate ) + ' \u00b7 revision ' + revision ) ); | |||
if ( viewedRevision !== revision || Number( mw.config.get( 'wgCurRevisionId' ) ) > revision ) { | |||
body.appendChild( element( 'p', 'sb-article-review__stale', viewedRevision !== revision ? | |||
'These recorded findings cover another revision and may no longer apply. Writing checks below cover the revision identified in that section.' : | |||
'A newer revision exists. These recorded findings cover the older revision shown here.' ) ); | |||
} | |||
var revisionParagraph = element( 'p', 'sb-article-review__revision' ); | |||
paragraphLink( revisionParagraph, 'View reviewed revision ' + revision, | |||
mw.util.getUrl( review.title || mw.config.get( 'wgPageName' ), { oldid: revision } ) ); | |||
body.appendChild( revisionParagraph ); | |||
var issues = Array.isArray( review.issues ) ? review.issues : []; | var issues = Array.isArray( review.issues ) ? review.issues : []; | ||
if ( issues.length ) { | if ( issues.length ) { | ||
var list = element( 'ul', 'sb-article-review__issues' ); | var list = element( 'ul', 'sb-article-review__issues' ); | ||
issues.forEach( function ( issue ) { | issues.slice( 0, maxFindings ).forEach( function ( issue ) { | ||
if ( !issue || typeof issue !== 'object' ) { | if ( !issue || typeof issue !== 'object' ) { | ||
return; | return; | ||
} | } | ||
var kind = [ 'error', 'review', 'note' ].indexOf( issue.kind ) !== -1 ? issue.kind : 'note'; | var kind = [ 'error', 'review', 'note' ].indexOf( issue.kind ) !== -1 ? issue.kind : 'note'; | ||
var item = element( 'li', 'sb-article-review__issue sb-article-review__issue--' + kind ); | var item = element( 'li', 'sb-article-review__issue sb-article-review__issue--' + kind ); | ||
item.appendChild( element( 'span', 'sb-article-review__kind', | item.appendChild( element( 'span', 'sb-article-review__kind', { error: 'Verified error', review: 'Review needed', note: 'Note' }[ kind ] ) ); | ||
item.appendChild( element( 'strong', 'sb-article-review__issue-title', issue.title || 'Review note' ) ); | item.appendChild( element( 'strong', 'sb-article-review__issue-title', issue.title || 'Review note' ) ); | ||
item.appendChild( element( 'p', 'sb-article-review__detail', issue.detail || '' ) ); | item.appendChild( element( 'p', 'sb-article-review__detail', issue.detail || '' ) ); | ||
| Line 115: | Line 174: | ||
var links = element( 'div', 'sb-article-review__links' ); | var links = element( 'div', 'sb-article-review__links' ); | ||
issue.links.forEach( function ( source ) { | issue.links.forEach( function ( source ) { | ||
if ( | if ( source && typeof source.url === 'string' ) { | ||
paragraphLink( links, source.label || 'Source', source.url ); | |||
} | } | ||
} ); | } ); | ||
item.appendChild( links ); | |||
} | } | ||
list.appendChild( item ); | list.appendChild( item ); | ||
| Line 131: | Line 184: | ||
body.appendChild( list ); | body.appendChild( list ); | ||
} else { | } else { | ||
body.appendChild( element( 'p', 'sb-article- | body.appendChild( element( 'p', '', 'No article-specific issue was recorded in this limited review.' ) ); | ||
} | |||
var scope = element( 'details', 'sb-article-review__scope' ); | |||
scope.appendChild( element( 'summary', '', 'Scope of the recorded review' ) ); | |||
scope.appendChild( element( 'p', 'sb-article-review__coverage', 'The following notes apply only to reviewed revision ' + revision + '.' ) ); | |||
if ( review.unicodeNote ) { | |||
scope.appendChild( element( 'p', 'sb-article-review__unicode', review.unicodeNote ) ); | |||
} | |||
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', 'Policy links provide review guidance; this was not a complete policy assessment.' ) ); | |||
body.appendChild( scope ); | |||
} | |||
function sourceContents() { | |||
if ( !sourceInput ) { | |||
return null; | |||
} | |||
var input = window.jQuery( sourceInput ); | |||
return typeof input.textSelection === 'function' ? input.textSelection( 'getContents' ) : sourceInput.value; | |||
} | |||
function selectFinding( finding, checkedText ) { | |||
if ( activeTarget || !sourceInput || sourceContents() !== checkedText ) { | |||
return; | |||
} | |||
var encoded = new TextEncoder().encode( checkedText ); | |||
var startByte = Math.max( 0, Math.min( encoded.length, Number( finding.offset ) || 0 ) ); | |||
var endByte = Math.max( startByte, Math.min( encoded.length, startByte + ( Number( finding.length ) || 0 ) ) ); | |||
var decoder = new TextDecoder(); | |||
var start = decoder.decode( encoded.slice( 0, startByte ) ).length; | |||
var end = decoder.decode( encoded.slice( 0, endByte ) ).length; | |||
var input = window.jQuery( sourceInput ); | |||
if ( typeof input.textSelection === 'function' ) { | |||
input.textSelection( 'setSelection', { start: start, end: end } ); | |||
input.textSelection( 'scrollToCaretPosition' ); | |||
} else { | |||
sourceInput.setSelectionRange( start, end ); | |||
} | } | ||
sourceInput.focus(); | |||
} | |||
body.appendChild( element( 'p', 'sb-article- | function renderWriting( body, result, options ) { | ||
'AI | var findings = Array.isArray( result.findings ) ? result.findings : []; | ||
body.appendChild( element( 'h3', 'sb-article-review__section', 'Writing checks' ) ); | |||
body.appendChild( element( 'p', 'sb-article-review__date', options.editor ? | |||
'Checked an unsaved snapshot. Nothing has been saved or changed.' : | |||
'Checked revision ' + Number( options.revision ) ) ); | |||
if ( options.editor ) { | |||
var stale = element( 'p', 'sb-article-review__stale sb-writing-review__stale', | |||
'The draft changed after this check. Check writing again to review the current text.' ); | |||
stale.hidden = !options.stale; | |||
body.appendChild( stale ); | |||
} | |||
body.appendChild( element( 'p', 'sb-article-review__limits', | |||
'These are prompts for human review, not proof of AI authorship or policy violations. Saving is never blocked.' ) ); | |||
if ( !findings.length ) { | |||
body.appendChild( element( 'p', 'sb-writing-review__empty', result.coverage && result.coverage.status === 'scanned' ? | |||
'No matching signs were found by these checks. This does not establish who wrote the text or whether it is accurate.' : | |||
'No findings are available from this check. See the coverage notes below.' ) ); | |||
} else { | |||
var list = element( 'ul', 'sb-article-review__issues sb-writing-review__findings' ); | |||
findings.slice( 0, maxFindings ).forEach( function ( finding ) { | |||
if ( !finding || typeof finding !== 'object' ) { | |||
return; | |||
} | |||
var kind = finding.severity === 'review' ? 'review' : 'note'; | |||
var item = element( 'li', 'sb-article-review__issue sb-article-review__issue--' + kind ); | |||
item.appendChild( element( 'span', 'sb-article-review__kind', kind === 'review' ? 'Review this passage' : 'Context check' ) ); | |||
item.appendChild( element( 'strong', 'sb-article-review__issue-title', finding.title || 'Writing sign' ) ); | |||
item.appendChild( element( 'p', 'sb-article-review__detail', finding.explanation || '' ) ); | |||
if ( typeof finding.evidence === 'string' && finding.evidence ) { | |||
item.appendChild( element( 'blockquote', 'sb-writing-review__evidence', finding.evidence.slice( 0, 1200 ) ) ); | |||
} | |||
var links = element( 'div', 'sb-article-review__links' ); | |||
if ( Number( finding.line ) > 0 ) { | |||
if ( options.editor && sourceInput && !activeTarget ) { | |||
var select = element( 'button', 'sb-writing-review__source', 'Select source line ' + Number( finding.line ) ); | |||
select.type = 'button'; | |||
select.disabled = !!options.stale; | |||
select.addEventListener( 'click', function () { selectFinding( finding, options.checkedText ); } ); | |||
links.appendChild( select ); | |||
} else if ( options.editor ) { | |||
links.appendChild( element( 'span', '', 'Draft source line ' + Number( finding.line ) ) ); | |||
} else { | |||
paragraphLink( links, 'Source line ' + Number( finding.line ), mw.util.getUrl( | |||
options.title || mw.config.get( 'wgPageName' ), { action: 'edit', oldid: Number( options.revision ) } ) ); | |||
} | |||
} | |||
paragraphLink( links, 'Why this is flagged', typeof finding.guide === 'string' ? finding.guide : guideUrl ); | |||
item.appendChild( links ); | |||
list.appendChild( item ); | |||
} ); | |||
body.appendChild( list ); | |||
} | |||
if ( result.truncated || findings.length > maxFindings ) { | |||
body.appendChild( element( 'p', 'sb-article-review__stale', 'This report is limited in size. Additional passages may need review.' ) ); | |||
} | |||
var scope = element( 'details', 'sb-article-review__scope' ); | var scope = element( 'details', 'sb-article-review__scope' ); | ||
scope.appendChild( element( 'summary', '', ' | scope.appendChild( element( 'summary', '', 'What the checks cover' ) ); | ||
scope.appendChild( element( 'p', ' | if ( result.coverage && result.coverage.reason ) { | ||
' | scope.appendChild( element( 'p', '', result.coverage.reason ) ); | ||
} | |||
var limitations = result.coverage && result.coverage.limitations; | |||
if ( Array.isArray( limitations ) ) { | |||
limitations.slice( 0, 12 ).forEach( function ( note ) { | |||
scope.appendChild( element( 'p', ' | if ( typeof note === 'string' ) { | ||
scope.appendChild( element( 'p', '', note ) ); | |||
} | |||
} ); | |||
} else if ( typeof limitations === 'string' ) { | |||
scope.appendChild( element( 'p', '', limitations ) ); | |||
} | |||
scope.appendChild( element( 'p', '', 'Checks use this wiki\u2019s text. They do not establish plagiarism, verify every source, or identify the writer.' ) ); | |||
paragraphLink( scope, 'Wikipedia: Signs of AI writing', guideUrl ); | |||
body.appendChild( scope ); | body.appendChild( scope ); | ||
} | |||
function initializeView() { | |||
var data = window.shalombuxArticleReviewData || {}; | |||
var scan = mw.config.get( 'wgMayapediaWritingReview' ); | |||
var pageId = Number( mw.config.get( 'wgArticleId' ) ); | |||
var revision = Number( mw.config.get( 'wgRevisionId' ) ); | |||
var query = new URLSearchParams( window.location.search ); | |||
if ( document.getElementById( rootId ) || pageId <= 0 || revision <= 0 || | |||
mw.config.get( 'wgAction' ) !== 'view' || query.has( 'diff' ) || | |||
mw.config.get( 'wgDiffOldId' ) || mw.config.get( 'wgDiffNewId' ) ) { | |||
return; | |||
} | |||
var review = data.pages && data.pages[ String( pageId ) ]; | |||
// Never present another page/revision's automated findings as current. | |||
if ( scan && ( Number( scan.revision ) !== revision || Number( scan.pageId ) !== pageId ) ) { | |||
scan = null; | |||
} | |||
if ( !scan && !review ) { | |||
return; | |||
} | |||
var count = scan && Array.isArray( scan.findings ) ? scan.findings.length : 0; | |||
var manualCount = review && Array.isArray( review.issues ) ? review.issues.length : 0; | |||
var storageKey = 'shalombux-article-review:writing:' + String( data.version || '' ) + ':' + | |||
String( scan && scan.version || '' ) + ':' + pageId + ':' + revision + ':' + String( review && review.reviewedRevision || '' ); | |||
viewCard = makeCard( rootId, count + manualCount ? 'Article review (' + ( count + manualCount ) + ')' : 'Article review', storageKey, false ); | |||
viewCard.revision = revision; | |||
viewCard.currentRevision = Number( mw.config.get( 'wgCurRevisionId' ) ) || revision; | |||
renderManual( viewCard.body, review, data, revision ); | |||
if ( scan ) { | |||
renderWriting( viewCard.body, scan, { revision: revision, title: scan.title } ); | |||
} else { | |||
viewCard.body.appendChild( element( 'p', 'sb-article-review__coverage', 'Automated writing checks are unavailable for this view.' ) ); | |||
} | |||
if ( ( count || manualCount ) && !viewCard.dismissed ) { | |||
viewCard.open( false ); | |||
} | |||
} | |||
function markDraftChanged() { | |||
var | editorGeneration++; | ||
if ( editorCard ) { | |||
var stale = editorCard.body.querySelector( '.sb-writing-review__stale' ); | |||
if ( stale ) { | |||
stale.hidden = false; | |||
} | |||
editorCard.body.querySelectorAll( '.sb-writing-review__source' ).forEach( function ( button ) { button.disabled = true; } ); | |||
} | |||
} | |||
function currentDraft() { | |||
if ( activeTarget ) { | |||
var doc = activeTarget.getDocToSave(); | |||
if ( typeof doc === 'string' ) { | |||
return Promise.resolve( doc ); | |||
} | |||
return Promise.resolve( activeTarget.serialize( doc ) ).then( function ( data ) { | |||
if ( !data || typeof data.content !== 'string' ) { | |||
throw new Error( 'serialization' ); | |||
} | } | ||
return data.content; | |||
} ); | |||
} | |||
} | } | ||
var text = sourceContents(); | |||
return typeof text === 'string' ? Promise.resolve( text ) : Promise.reject( new Error( 'editor' ) ); | |||
} | |||
function | function checkDraft() { | ||
if ( | if ( !editorCard || editorCard.badge.disabled ) { | ||
return; | |||
} | |||
var serial = ++requestSerial, generation = editorGeneration, checkedText; | |||
editorCard.badge.disabled = true; | |||
editorCard.badge.textContent = 'Checking writing\u2026'; | |||
editorCard.body.textContent = ''; | |||
editorCard.body.appendChild( element( 'p', 'sb-writing-review__status', 'Checking the current unsaved text on Mayapedia\u2026' ) ); | |||
editorCard.body.setAttribute( 'aria-busy', 'true' ); | |||
editorCard.open( false ); | |||
currentDraft().then( function ( text ) { | |||
checkedText = text; | |||
return new mw.Api().postWithToken( 'csrf', { action: 'writingreview', text: text, format: 'json', formatversion: 2 } ); | |||
} ).then( function ( response ) { | |||
if ( serial !== requestSerial || !editorCard ) { | |||
return; | return; | ||
} | } | ||
if ( !response || !response.writingreview ) { | |||
throw new Error( 'response' ); | |||
} | |||
if ( | editorCard.body.textContent = ''; | ||
renderWriting( editorCard.body, response.writingreview, { editor: true, checkedText: checkedText, | |||
stale: generation !== editorGeneration || ( !activeTarget && sourceContents() !== checkedText ) } ); | |||
} ).catch( function () { | |||
if ( serial === requestSerial && editorCard ) { | |||
editorCard.body.textContent = ''; | |||
editorCard.body.appendChild( element( 'p', 'sb-article-review__stale', | |||
'The writing check could not finish. Your draft is unchanged and you can still save. Try Check writing again.' ) ); | |||
} | |||
} ).then( function () { | |||
if ( serial === requestSerial && editorCard ) { | |||
editorCard.badge.disabled = false; | |||
editorCard.badge.textContent = 'Check writing'; | |||
editorCard.body.removeAttribute( 'aria-busy' ); | |||
} | } | ||
} ); | |||
} | |||
function ensureEditor() { | |||
if ( !editorCard ) { | |||
editorCard = makeCard( editorId, 'Check writing', null, true ); | |||
editorCard.badge.addEventListener( 'click', checkDraft ); | |||
} | } | ||
editorCard.root.hidden = false; | |||
} | |||
function activateVisualEditor() { | |||
var target = window.ve && ve.init && ve.init.target; | |||
if ( !target || typeof target.getSurface !== 'function' || typeof target.getDocToSave !== 'function' || typeof target.serialize !== 'function' ) { | |||
return; | |||
} | |||
var surface = target.getSurface(); | |||
if ( !surface || !surface.getModel || !surface.getModel().getDocument ) { | |||
return; | |||
} | |||
var doc = surface.getModel().getDocument(); | |||
activeTarget = target; | |||
if ( activeDocument !== doc ) { | |||
if ( activeDocument && typeof activeDocument.off === 'function' ) { | |||
activeDocument.off( 'transact', markDraftChanged ); | |||
} | } | ||
activeDocument = doc; | |||
activeDocument.on( 'transact', markDraftChanged ); | |||
markDraftChanged(); | |||
} | } | ||
ensureEditor(); | |||
if ( viewCard ) { | |||
viewCard.root.hidden = true; | |||
} | |||
} | |||
function initialize() { | |||
if ( [ 0, 118 ].indexOf( Number( mw.config.get( 'wgNamespaceNumber' ) ) ) === -1 ) { | |||
return; | |||
} | |||
initializeView(); | |||
sourceInput = document.getElementById( 'wpTextbox1' ); | |||
if ( sourceInput && !sourceInput.readOnly && !sourceInput.disabled ) { | |||
ensureEditor(); | |||
sourceInput.addEventListener( 'input', markDraftChanged ); | |||
window.jQuery( sourceInput ).on( 'change', markDraftChanged ); | |||
} | |||
mw.hook( 'ext.CodeMirror.input' ).add( markDraftChanged ); | |||
mw.hook( 've.activationStart' ).add( function () { | |||
if ( viewCard ) { | |||
viewCard.root.hidden = true; | |||
} | |||
if ( editorCard ) { | |||
editorCard.root.hidden = true; | |||
} | |||
} ); | } ); | ||
mw.hook( 've.activationComplete' ).add( activateVisualEditor ); | |||
mw.hook( 've.deactivationComplete' ).add( function () { | |||
activeTarget = null; | |||
if ( activeDocument && typeof activeDocument.off === 'function' ) { | |||
if ( | activeDocument.off( 'transact', markDraftChanged ); | ||
} | } | ||
activeDocument = null; | |||
requestSerial++; | |||
if ( editorCard ) { | |||
editorCard.root.hidden = true; | |||
editorCard.badge.disabled = false; | |||
editorCard.badge.textContent = 'Check writing'; | |||
} | } | ||
if ( viewCard ) { | |||
var revision = Number( mw.config.get( 'wgRevisionId' ) ); | |||
var currentRevision = Number( mw.config.get( 'wgCurRevisionId' ) ) || revision; | |||
if ( revision !== viewCard.revision || currentRevision !== viewCard.currentRevision ) { | |||
viewCard.body.textContent = ''; | |||
viewCard.body.appendChild( element( 'p', 'sb-article-review__stale', | |||
'The saved page changed in the editor. Reload this page to check the new saved revision.' ) ); | |||
paragraphLink( viewCard.body, 'Refresh saved-page checks', mw.util.getUrl( mw.config.get( 'wgPageName' ) ) ); | |||
viewCard.badge.textContent = 'Refresh article review'; | |||
} | } | ||
viewCard.root.hidden = false; | |||
} | } | ||
} ); | } ); | ||
if ( document.documentElement.classList.contains( 've-activated' ) || document.body.classList.contains( 've-activated' ) ) { | |||
activateVisualEditor(); | |||
} | } | ||
} | } | ||
mw.loader.using( 'mediawiki.util' ).then( function () { | mw.loader.using( [ 'mediawiki.util', 'mediawiki.api', 'jquery.textSelection' ] ).then( function () { | ||
if ( document.readyState === 'loading' ) { | if ( document.readyState === 'loading' ) { | ||
document.addEventListener( 'DOMContentLoaded', initialize, { once: true } ); | document.addEventListener( 'DOMContentLoaded', initialize, { once: true } ); | ||