User:PigeonIP/DiffLists.js
Jump to navigation
Jump to search
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.
// Changes appearance of Recent Changes, Watchlist, Contributions, History
// pages, and Related Changes.
// Also adds filter options.
// This script is intended to demonstrate my proposal for watchlist design, proposed in T121361.
//
// Intended browser support: Chrome, FF, MS Edge, Safari.
// Allowed ES6 bits: Rest, spread, object computed/shorthand props, fancy array/string/dom, for of, arrow functions.
// Not allowed: Destructuring, let, class.
// Update 2016-05-11: Changed my mind. Destructuring is allowed. MS Edge no longer supported.
// Multilingual support was added very late, so there's a lot of mess there.
// TODO: Maintain all non-autogenerated edit summary bits. #autolist is being removed.
// TODO: More advanced filtering options. Things like Wikiproject prop groups.
// ** Allow whatever caps
// TODO: Default (when no stored settings) to Babel settings (not in links), if possible. Investigate.
function DiffLists() {
if ( window.DiffListsLoaded ) {
return;
}
window.DiffListsLoaded = true;
var isWD = mw.config.get( 'wgDBname' ) === 'wikidatawiki',
wdDomain = 'https://www.wikidata.org',
api = isWD ? new mw.Api() : new mw.ForeignApi( wdDomain + '/w/api.php' );
(
( [ "Recentchanges", "Watchlist", "Recentchangeslinked", "Contributions" ] )
.indexOf( mw.config.get( "wgCanonicalSpecialPageName" ) ) !== -1 ||
( mw.config.get( "wgAction" ) === "history" && [ 0, 120 ].indexOf( mw.config.get( "wgNamespaceNumber" ) ) !== -1 )
) && api.get( {
action: "query",
meta: "allmessages",
amlang: mw.config.get( 'wgUserLanguage' ),
ammessages: [
// Changing the order shouldn't break things. TODO: Fix.
// Diff view stuff.
"wikibase-diffview-label",
"wikibase-diffview-description",
"wikibase-diffview-alias",
"wikibase-diffview-link",
"wikibase-entity-property",
"wikibase-diffview-qualifier",
"wikibase-diffview-reference",
"wikibase-diffview-rank",
// Extras
"wikibase-entitytermsforlanguagelistview-label",
"wikibase-entitytermsforlanguagelistview-description",
"wikibase-statementsection-statements",
"wikibase-diffview-rank-preferred",
"wikibase-diffview-rank-normal",
"wikibase-diffview-rank-deprecated"
].join( "|" ),
maxage: 60 * 60 * 24 * 30,
smaxage: 60 * 60 * 24 * 30
} ).done( function ( msgData ) {
$( function () {
//console.log( "msgs", msgData );
mw.util.addCSS(
".YR-cl-added { background-color: #d8ecff; margin: 0 1px; padding: 0 1px; }" +
".YR-cl-removed { background-color: #feeec8; margin: 0 1px; padding: 0 1px; text-decoration: line-through; }" +
".YR-cl-hidden { display: none !important; }" + // Yeah, this is misusing !important, but I need to override jquery's straight style.display.
".YR-cl-summary ~ .YR-cl-summary:before { content: \", \"; }" +
".YR-cl-hiddensummary { display: none; }"
);
var changeBlock = ( function () {
var addedTemplate = $( "<span>" ).addClass( "YR-cl-added" )[ 0 ],
removedTemplate = $( "<span>" ).addClass( "YR-cl-removed" )[ 0 ];
return function ( type, elem ) {
return ( type === "added" ? addedTemplate : removedTemplate ).cloneNode( true ).appendChild( elem ).parentNode;
};
})(),
msgs = {},
msgNameArray = msgData.query.allmessages.map( msg => msg.name.split( "-" )[ 2 ] ),
msgArray = msgData.query.allmessages.map( msg => msg[ "*" ] ),
typeIndexes = { label: 0, description: 1, alias: 2, link: 3, property: 4 },
allDiffs = [],
nestedParents = new Map(),
lang = mw.config.get( 'wgUserLanguage' );
msgData.query.allmessages.forEach( function ( msg, i ) {
msgs[ i < 8 ? msg.name.split( "-" )[ 2 ] : msg.name ] = msg[ "*" ];
} );
// Settings stuff.
var filterData = {
dbTypes: [ '', 'voyage', 'books', 'quote', 'news', 'source', 'versity' ].map( x => 'wiki' + x ),
specialwikis: [ 'commons', 'meta', 'mediawiki', 'wikidata', 'species' ],
propGroups: {
// These can be functions or arrays.
identifiers: function ( x ) {
var v = x.change.values, node;
for ( var i in v ) {
node = v[ i ].node;
if ( node && ( node.className === 'wb-external-id' || node.firstChild && node.firstChild.className === 'wb-external-id' ) ) {
return true;
}
}
},
other: function ( x ) {
return filterData.specialwikis.some( y => y + 'wiki' === x.data );
}
}
},
simpleSettings = getSettings(),
processedSettings = processSettings( simpleSettings ),
allPropTypes = {
[ msgs.label ]: "Labels",
[ msgs.description ]: "Descriptions",
[ msgs.link ]: "Sitelinks",
[ msgs.alias ]: "Aliases",
[ msgs.property ]: msgs[ "wikibase-statementsection-statements" ]
},
listType = mw.config.get( "wgAction" ) === "history" ? 'history' :
( mw.config.get( "wgCanonicalSpecialPageName" ) === "Contributions" ?
"contribs" : 'normal' ),
changeSelector =
isWD ?
listType === 'history' ?
// History
".mw-history-histlinks a:last-child"
:
// Normal watchlists/RC
".mw-changeslist .special > li > a:first-child, " +
// Enhanced watchlists/RC
".mw-enhanced-rc .mw-title + a, " +
// Enhanced, with nested
".mw-enhanced-rc-nested .mw-enhanced-rc-time + a + a, " +
// Contributions
".mw-contributions-list a"
:
'.mw-changeslist .wikibase-edit a[tabindex]';
//allPropTypes = [ "Labels", "Descriptions", "Sitelinks", "Aliases", "Statements" ];
function buildOptionsBox() {
function setSettings() {
var data = {};
$.each( checkboxes, function ( i, checkbox ) {
data[ i ] = checkbox.checked && inputs[ i ].value;
} );
localStorage[ "YR-cl-settings" ] = JSON.stringify( simpleSettings = data );
processedSettings = processSettings( simpleSettings );
}
var optionsContainer,
optionsDiv = document.createElement( "div" ),
checkboxes = {},
inputs = {};
if ( listType === "normal" ) {
optionsContainer = document.querySelector( "#mw-watchlist-options, .rcoptions" );
// Add a divider.
optionsContainer.appendChild( document.createElement( "hr" ) ).style.margin = "8px 0";
} else {
var l = document.querySelector( listType === "contribs" ? ".mw-contributions-form" : "#mw-history-searchform" );
optionsContainer = l.parentNode.insertBefore( document.createElement( "fieldset" ), l.nextSibling );
optionsContainer
.appendChild( document.createElement( "legend" ) )
.appendChild( document.createTextNode( "Filter options" ) );
}
// Build filter options menu
$.each( allPropTypes, function ( type, label ) {
var optionDiv = document.createElement( "div" );
optionDiv.style.display = "inline-block";
optionsDiv.appendChild( optionDiv );
var checkbox = checkboxes[ type ] = document.createElement( "input" );
checkbox.type = "checkbox";
checkbox.checked = ( !simpleSettings || simpleSettings[ type ] !== false ) ? "checked" : "";
optionDiv.appendChild( checkbox );
optionDiv.appendChild( document.createTextNode( label + ": " ) );
var input = inputs[ type ] = optionDiv.appendChild( document.createElement( "input" ) );
if ( type === msgs.label || type === msgs.description || type === msgs.alias ) {
input.title =
'Show only languages with these languages codes, ' +
'separated by |, e.g. "en|fr". Leave blank to show all.';
} else if ( type === msgs.link ) {
input.title =
'Show only links to projects with these database ' +
'codes, separated by |, e.g. "enwiki|frwikivoyage". ' +
'Leave blank to show all.\n' +
'You can use a language code to show links to all ' +
'projects in that language, or a project database ' +
'suffix to show links to all of those projects ' +
'regardless of language. Use "other" to show links ' +
'to Commons, Meta, Mediawiki, Wikispecies, and ' +
'Wikidata.';
} else {
input.title =
'Show only changes to statements using these ' +
'properties, separated by |, e.g. "P40|P569". ' +
'Leave blank to show all.\n' +
'You can use "identifiers" to select all identifier ' +
'properties, and prefix a property or group of ' +
'properties with "^" to not show the property. ' +
'(E.g. Input just "^identifiers" to show all ' +
'properties except identifiers.)';
}
input.style.width = "50px";
input.value = simpleSettings && simpleSettings[ type ] || "";
if ( simpleSettings && simpleSettings[ type ] === false ) {
input.disabled = true;
}
checkbox.onchange = input.onchange = function () {
input.disabled = !checkbox.checked;
setSettings();
updateDisplay( allDiffs );
};
input.onkeydown = function ( e ) {
if ( e.keyCode === 13 ) {
setSettings();
updateDisplay( allDiffs );
return false;
}
};
} );
// Add the menu to the DOM
if ( optionsContainer ) {
optionsContainer.appendChild( optionsDiv );
}
}
// Settings will be stored in localStorage, in the following format:
// { "label": "..." | "" | false, ... }
// Multiple situations possible here:
// * Checking to see if any members of a group match show=true
// ** 'changes' obj, check to see if there's any of x type, etc
// * Check if a particular prop/value should be shown
// Among this, there can be single setting prop or multi-setting prop.
// Is it actually necessary to special-case the first check?
//
// Checks if a value should be shown under relevant setting.
function checkSetting( value ) {
// setting > false || '' || [ { additive: true || false, type: '... (eg plain)', content: '... (eg en)' }, ... ]
// value > { type: property, data: Property:P31 }
var setting = processedSettings[ value.type ];
if ( setting === false ) {
return false;
} else if ( setting === '' || setting === undefined ) {
return true;
} else {
var r = !setting[ 0 ].additive,
propGroups = filterData.propGroups;
setting.forEach( settingV => {
// if ( settingV.additive === r ) { return }
if ( settingV.type === 'plain' ) {
value.data === settingV.content && ( r = settingV.additive );
} else if ( settingV.type === 'group' ) {
// Probably statements only
if ( typeof propGroups[ settingV.content ] === 'function' ?
propGroups[ settingV.content ]( value ) :
propGroups[ settingV.content ].indexOf( value.data ) !== -1
) {
r = settingV.additive;
}
} else if ( settingV.type === 'lang' ) {
// Links only
// I think this needs access to dbtypes
if ( value.data.startsWith( settingV.content ) ) {
if ( filterData.dbTypes.some( x => settingV.content + x === value.data ) ) {
r = settingV.additive;
}
}
} else if ( settingV.type === 'wiki' ) {
// Links only
if ( value.data.endsWith( settingV.content ) ) {
r = settingV.additive;
}
} else if ( settingV.type === 'specialwiki' ) {
// Links to "other" wikis, eg Commons.
if ( value.data === settingV.content + 'wiki' ) {
r = settingV.additive;
}
}
} );
return r;
}
}
// Result can be false
function getSettings() {
var urlArgs = mw.util.getParamValue( 'difflists' ),
storage = localStorage[ "YR-cl-settings" ];
//
if ( !urlArgs && !storage ) {
return false;
} else {
return $.extend( {},
storage && JSON.parse( storage ),
urlArgs && JSON.parse( urlArgs ) );
}
}
function processSettings() {
var settings = {},
{ dbTypes, propGroups, specialwikis } = filterData;
if ( simpleSettings === false ) {
return false;
}
$.each( simpleSettings, function ( type, input ) {
var setting = settings[ type ] = input && input.split( /\s*[\|\,]\s*/ );
if ( setting ) {
settings[ type ] = setting.map( x => {
var isProp = type === msgs.property,
isLink = type === msgs.link,
additive = x[ 0 ] !== '^',
content = additive ? x : x.substr( 1 );
if ( isProp && !propGroups[ content ] ) {
content = 'Property:' + content;
}
return {
additive,
type:
( propGroups.hasOwnProperty( content ) && 'group' ) ||
( isLink && (
( specialwikis.indexOf( x ) !== -1 && 'specialwiki' ) ||
( dbTypes.indexOf( x ) !== -1 && 'wiki' ) ||
( !dbTypes.some( y => x.endsWith( y ) ) && 'lang' )
) ) ||
'plain',
content
};
} );
}
// Values:?
// false
// { additive: true, type: plain, content: en }
// { additive: true, type: lang, content: en }
// { additive: true, type: wiki, content: wiki }
// { additive: false, type: group, content: identifiers }
// { additive: true, type: specialwiki, content: meta }
// { additive: false, type: all } (no)
// Should there be a special case for commonswiki etc? or just 'wiki'?
} );
return settings;
}
// Show or hide diff, depending on filter preferences.
function updateDisplay( diffs ) {
var isHistory = listType === "history";
simpleSettings && diffs.forEach( function ( diff ) {
var displayDiff = false;
// If there's some not filtered, or we're on a history page
// where all diffs are shown, hide individual filtered segments.
$.each( diff.chunkDetails, function ( i, details ) {
var display = false;
display = checkSetting( details );
displayDiff = displayDiff || display;
details.wrapper.className = display ? "YR-cl-summary" : "YR-cl-hiddensummary";
} );
// If all elements are filtered, hide the whole listing, unless
// we're on a history page.
if ( !isHistory ) {
diff.elem.classList.toggle( "YR-cl-hidden", !displayDiff );
}
} );
}
function buildChunkDetails( allChanges, chunkDetails ) {
// Use allChanges to build chunkDetails, array of DOM for later use.
// First non-statement changes.
$.each( [ msgs.label, msgs.description, msgs.link, msgs.alias ], function ( i, type ) {
$.each( allChanges[ msgArray.indexOf( type ) ], function ( langOrWiki, change ) {
var chunk = document.createElement( "span" ),
text = ( {
[ msgs.label ]: msgs[ "wikibase-entitytermsforlanguagelistview-label" ],
[ msgs.description ]: msgs[ "wikibase-entitytermsforlanguagelistview-description" ],
[ msgs.link ]: "Link",
[ msgs.alias ]: "Aliases"
} )[ type ];
chunkDetails.push( change.details = { chunk, type, data: langOrWiki, change } );
chunk.appendChild( document.createTextNode( text + " [" + langOrWiki + "]: " ) );
$.each( type === msgs.alias ? change : [ change ], function( i, a ) {
[ "added", "removed" ].forEach( function ( type ) {
a[ type ] && chunk.appendChild( changeBlock( type, a[ type ] ) );
} );
} );
// For links, add badge DOM.
change.badges && change.badges.forEach( function ( a, i ) {
$.each( a, function ( i, a ) {
// We're using the original element. Should this
// be cloned? If not, this might cause problems
// for multiple diffs using the same changes.
a.insertBefore( document.createTextNode( "(badge: "), a.firstChild );
a.appendChild( document.createTextNode( ")"), a.firstChild );
chunk.appendChild( changeBlock( i, a ) );
} );
} );
} );
} );
// Then statement changes.
$.each( allChanges[ typeIndexes.property ], function ( i, change ) {
var chunk = document.createElement( "span" ),
keys = Object.keys( change.values ),
encase = keys.length === 1 && change.values[ keys[ 0 ] ].change;
chunk.appendChild( change.link );
chunk.appendChild( document.createTextNode( ": " ) );
$.each( change.values, function ( index, statement ) {
if ( statement.change ) {
if ( !encase ) {
chunk
.appendChild( changeBlock( statement.change, statement.node ) );
} else {
chunk.appendChild( statement.node );
chunk = changeBlock( statement.change, chunk );
}
} else {
chunk.appendChild( statement.node );
}
if ( statement.qualifiers ) {
//console.log( "has qual", a );
var first = true;
// The diff system does something really weird with multis.
// Deletes and recreates all the same qualifiers.
$.each( statement.qualifiers, function ( i, a ) {
chunk.appendChild( document.createTextNode( first ? " / " : ", " ) );
var qualHolder = a.length === 1 ?
chunk.appendChild( changeBlock( a[ 0 ].change, a.link ) ) :
chunk;
first = false;
qualHolder.appendChild( a.link );
qualHolder.appendChild( document.createTextNode( ": " ) );
$.each( a, function ( i, a ) {
chunk.appendChild( changeBlock( a.change, a.value ) );
} );
} );
}
if ( statement.references ) {
//console.log( "has ref", a );
$.each( statement.references, function ( i, a ) {
var ref = changeBlock( a.change, document.createTextNode( "Reference: " ) );
chunk.appendChild( document.createTextNode( i ? ", " : " / " ) );
$.each( a, function ( i, a ) {
if ( i ) {
ref.appendChild( document.createTextNode( ", " ) );
}
ref.appendChild( a.propLink );
ref.appendChild( document.createTextNode( ": " ) );
ref.appendChild( a.value );
} );
chunk.appendChild( ref );
} );
}
if ( statement.rank ) {
$.each( statement.rank, function ( i, a ) {
var rankDot = document.createElement( [ "sup", "span", "sub" ][ a.rank ] );
rankDot.appendChild( document.createTextNode( "·" ) );
rankDot.title = a.rankName;
statement.node.parentNode.insertBefore(
changeBlock( i, rankDot ),
statement.node
);
} );
}
} );
if ( !isWD ) {
$( chunk ).find( 'a[ href ]:not( [ href ^= "http" ] )' ).each( function () {
this.href = wdDomain + this.getAttribute( 'href' );
} );
}
chunkDetails.push( change.details = { chunk, type: msgArray[ typeIndexes.property ], data: i, change } );
} );
}
function addChunkDetails( chunkDetails, diffLinkParent, nestedCDList ) {
// TODO: Reuse summary nodes in nesteds where this'll run multiple times.
var summaryNode = document.createElement( "span" ),
bitmarker = diffLinkParent.querySelector( ".mw-plusminus-pos, .mw-plusminus-neg, .mw-plusminus-null" ),
oldComment = diffLinkParent.querySelector( ".comment" ),
beforePoint,
oldSummary;
// For parents of nested diffs, add new chunks to old summary.
if ( nestedCDList ) {
oldSummary = nestedCDList.length !== 0;
for ( var i = 0; i < nestedCDList.length + 1; i++ ) {
if ( i === nestedCDList.length ) {
// At the end. None found.
nestedCDList.push( chunkDetails );
if ( i > 0 ) {
summaryNode = nestedCDList[ 0 ][ 0 ].wrapper.parentNode;
}
break;
} else if ( chunkDetails.timestamp < nestedCDList[ i ].timestamp ) {
if ( !nestedCDList[ i ][ 0 ] ) {
// This shouldn't happen.
// Might be related to redirects.
//console.log( 434, chunkDetails, nestedCDList, i );
break;
}
// Found a place to put new diffs.
beforePoint = nestedCDList[ i ][ 0 ].wrapper;
nestedCDList.splice( i, 0, chunkDetails );
//console.log( 'beforePoint=', beforePoint );
break;
}
}
}
chunkDetails.forEach( function ( details, i ) {
var chunk = details.chunk,
wrapper = details.wrapper = document.createElement( "span" );
wrapper.className = "YR-cl-summary";
i === 0 && !oldSummary && summaryNode.appendChild( document.createTextNode( " - " ) );
if ( beforePoint ) {
beforePoint.parentNode.insertBefore( wrapper, beforePoint );
} else {
summaryNode.appendChild( wrapper );
}
wrapper.appendChild( chunk );
} );
if ( !nestedCDList || nestedCDList.length === 1 ) {
// Fiddle with the DOM, remove bit markers
if ( bitmarker ) {
bitmarker.parentNode.removeChild( bitmarker.previousSibling );
bitmarker.parentNode.removeChild( bitmarker.nextSibling );
bitmarker.parentNode.removeChild( bitmarker );
}
// Replace old summary with new summary.
if ( oldComment ) {
diffLinkParent.insertBefore( summaryNode, oldComment );
if ( oldComment.querySelector( ".autocomment" ) ) {
// This should leave things like "#autolist2", if possible.
for ( ; oldComment.firstChild; ) {
oldComment.removeChild( oldComment.firstChild );
}
//oldComment.parentNode.removeChild( oldComment );
}
} else {
diffLinkParent.appendChild( summaryNode );
}
}
}
function processDiff( diffLinkParent, oldid, pageid, diffid, title ) {
api.get( {
action: "query",
prop: "revisions",
//rvprop: "timestamp|user|comment",
//pageids: pageid,
titles: title,
rvstartid: oldid,
//rvdiffto: "next",
rvdiffto: diffid,
rvlimit: 1,
uselang: lang,
maxage: 60 * 60 * 24 * 3,
smaxage: 60 * 60 * 24 * 3
} ).done( function ( result ) {
if ( !result || !result.query || !result.query.pages || result.query.pages[ -1 ] ) {
console.error( 'DiffLists: Diff not found.', result );
return;
}
// TODO: getStuff stuff.
// TODO: Figure out blank changes.
// TODO: Grab localizations for everything.
//
for ( var pageid in result.query.pages );
result = result.query.pages[ pageid ];
var revision = result.revisions[ 0 ],
$x = $( revision.diff[ "*" ] ),
timestamp = revision.timestamp,
allChanges = [ {}, {}, {}, {}, {} ],
chunkDetails = [],
// Get the element that holds the change, for hiding purposes. Three different situations:
// Regular watchlist, enhanced watchlist, and enhanced nested.
// Enhanced lists are lists of tables, the first row of each being main, others nested.
// Currently, hiding the parent hides the subs. Not sure if that's the best behaviour.
enhanced = diffLinkParent.nodeName === "TD",
nested = enhanced && diffLinkParent.className === "mw-enhanced-rc-nested",
outer = enhanced ?
nested ?
diffLinkParent.parentNode :
diffLinkParent.parentNode.parentNode.parentNode :
diffLinkParent,
diff = { elem: outer, chunkDetails, allChanges };
allDiffs.push( diff );
// Build allChanges from result data.
$x.each( function ( trIndex, tr ) {
if ( tr.nodeName === "TR" && tr.nextSibling && tr.childNodes.length === 2 && tr.firstChild.className !== "diff-marker" ) {
// We're dealing with a heading line
var node = tr.firstChild.firstChild ? tr.firstChild : tr.lastChild,
propData = node.textContent.split( " / " ),
//mainType = propData[ 0 ],
mainType = msgArray.indexOf( propData[ 0 ] ),
trValueBlock = tr.nextSibling,
valueTds = {
removed: trValueBlock.firstChild.className === "diff-marker" && trValueBlock.childNodes[ 1 ],
added: trValueBlock.lastChild.className === "diff-addedline" && trValueBlock.lastChild
};
switch ( mainType ) {
case typeIndexes.property:
// Bunch of different situations here:
// If a brand new statement, highlight all.
// If removing full statement, highlight and strike all.
// If changing a statement, don't highlight prop, highlight values.
// If just adding/removing/changing qualifiers/sources, don't highlight values either.
// Mixing these has complex results.
// Note: Depending on data type, value might not be a link.
// Overall structure: allChanges = [ TODO ]
var propLink = node.firstChild.nextSibling,
prop = propLink.title,
propChangeGroup =
( allChanges[ mainType ][ prop ] = allChanges[ mainType ][ prop ] || { values: {}, link: propLink } );
if ( !propLink ) {
//console.log( "no proplink", node );
}
$.each( valueTds, function ( changeType, td ) {
var valNode;
if ( !td.firstChild || !td.firstChild.firstChild ) {
//console.log( "break", td );
return;
}
$( [ node, td ] ).find( ".wb-language-fallback-indicator" ).remove();
// Assuming that only two nodes means it's a simple statement.
if ( node.childNodes.length === 2 ) {
// Simple statement
var val = td.firstChild.firstChild.firstChild,
valName,
valGroup;
if ( val.firstChild && val.firstChild.nodeName === "A" ) {
// We have a straight link.
valNode = val.firstChild;
// Images and whatnot don't have titles.
valName = valNode.title || valNode.textContent;
} else if ( val.firstChild && val.firstChild.nodeName === "H4" ) {
// Table filled with stuff, probably quantity.
valName = val.firstChild.textContent;
valNode = document.createElement( "span" )
.appendChild( document.createTextNode( valName ) )
.parentNode;
} else {
// Straight string
valName = val.textContent;
valNode = val;
}
valGroup = propChangeGroup.values[ valName ] =
propChangeGroup.values[ valName ] || {};
valGroup.change = changeType;
} else {
// Qualifier, Source, or Rank statement
// The value is sometimes a string, sometimes a link.
var subtype = propData[ propData.length - 1 ],
hasValueLink = node.childNodes[ 3 ] && node.childNodes[ 3 ].nodeName === "A",
valName = hasValueLink ?
node.childNodes[ 3 ].title || node.childNodes[ 3 ].textContent :
propData[ 1 ].split( ": " )[ 1 ],
valGroup = propChangeGroup.values[ valName ] =
propChangeGroup.values[ valName ] || {};
valNode = hasValueLink ?
node.childNodes[ 3 ] :
document.createTextNode( valName );
function getStuff( t ) {
// Get the value of the qualifier/reference part.
// There are 3 options: ": <a>content</a>", ": <h4>content</h4><table />", and ": content"
// Only copy the element itself if it's a link.
// Occasionally there's actually a span element indicating an error.
// If so, try for the previous element.
return t.nodeName === "A" ?
t :
document.createTextNode(
( t.nodeName === "TABLE" || t.nodeName === "SPAN" ) ?
t.previousSibling.textContent :
// This breaks sometimes. TODO.
t.nodeValue.substr( 2 )
//t.nodeValue.split( ":" )[ 1 ]
);
}
// TODO: Try to merge these, if possible.
switch ( subtype ) {
case msgs.reference:
// A statement can have any number of references.
// References have a list of statements, but are one block each.
var subGroup = valGroup.references = valGroup.references || [],
ref = [];
td && subGroup.push( ref );
ref.change = changeType;
// Series of spans, separated by brs.
$( ">div>.diffchange>span", td ).each( function () {
ref.push( {
propLink: this.firstChild,
value: getStuff( this.lastChild )
} );
} );
break;
case msgs.qualifier:
// Should this be an array or object containing arrays? There can be multiple
// qualifiers of the same type, +added/removed values, ...
// Still, changed values need to be shown...
var subGroup = valGroup.qualifiers = valGroup.qualifiers || {},
qualSpan = td.firstChild.firstChild.firstChild,
// The value will always start with a link to the property.
subPropLink = qualSpan.firstChild,
subPropName = subPropLink.title,
qualList = subGroup[ subPropName ] = subGroup[ subPropName ] || [],
qual = {};
qualList.push( qual );
qualList.link = subPropLink;
qual.change = changeType;
qual.value = getStuff( qualSpan.lastChild );
// Clear duplicates
qualList.forEach( function ( a, i ) {
if ( ( a.value.title || a.value.nodeValue ) === ( qual.value.title || qual.value.nodeValue ) && a.change !== qual.change ) {
qualList.splice( qualList.length - 1, 1 );
qualList.splice( i, 1 );
if ( qualList.length === 0 ) {
delete subGroup[ subPropName ];
}
return false;
}
} );
// After, there's a ": ", but I don't know what happens to strings.
break;
case msgs.rank:
var subGroup = valGroup.rank = valGroup.rank || {},
rankSpan = td.firstChild.firstChild.firstChild,
rankName = rankSpan.textContent,
rank = ( [
msgs[ "wikibase-diffview-rank-preferred" ],
msgs[ "wikibase-diffview-rank-normal" ],
msgs[ "wikibase-diffview-rank-deprecated" ]
] ).indexOf( rankName );
subGroup[ changeType ] = { rank, rankName };
break;
}
}
valGroup.node = valGroup.node || valNode;
} );
break;
case typeIndexes.label:
case typeIndexes.description:
case typeIndexes.link:
case typeIndexes.alias:
var langOrWiki = propData[ 1 ],
isAlias = mainType === typeIndexes.alias,
newBlock = allChanges[ mainType ][ langOrWiki ] =
allChanges[ mainType ][ langOrWiki ] || ( isAlias ? [] : {} ),
isBadge = propData[ 2 ] === "badges"; // "badges" for some reason isn't translated.
$.each( valueTds, function ( i, td ) {
if ( !td.firstChild || !td.firstChild.firstChild ) {
//console.log( 777, td, diffLink.parentNode, $x );
}
if ( td ) {
if ( !isBadge ) {
// Normal label, description, alias, or sitelink
if ( td ) {
if ( isAlias ) {
newBlock.push( newBlock = {} );
}
newBlock[ i ] = td.firstChild.firstChild.firstChild;
}
} else {
// Badge.
newBlock.badges = newBlock.badges || [];
var newBadge = {};
newBadge[ i ] = td.firstChild.firstChild.firstChild;
newBlock.badges.push( newBadge );
}
}
} );
break;
}
}
} );
// Use allChanges to build chunkDetails, array of DOM to be added.
buildChunkDetails( allChanges, chunkDetails );
//
if ( nested ) {
//var dupChunkDetails = ( chunkDetails.map( x => ( { chunk: x.chunk.cloneNode( true ), data: x.data, type: x.type, wrapper: x.wrapper, change: x.change } ) ) ),
var dupChunkDetails = ( chunkDetails.map( x => ( $.extend( {}, x, { chunk: x.chunk.cloneNode( true ) } ) ) ) ),
// These lines are the Map way of saying nP[ tb ] = p = nP[ tb ] || { ... };
nesteds,
tbody = outer.parentNode,
cdList;
/*
nestedParents.set( tbody, ( nesteds =
nestedParents.get( tbody ) ||
{ chunkDetails: [], allChanges: [ {}, {}, {}, {}, {} ], elem: tbody, chunkDetailsList: [] }
) );
*/
if ( !( nesteds = nestedParents.get( tbody ) ) ) {
nesteds = { chunkDetails: [], allChanges: [ {}, {}, {}, {}, {} ], elem: tbody, chunkDetailsList: [] };
nestedParents.set( tbody, nesteds );
allDiffs.push( nesteds );
}
cdList = nesteds.chunkDetailsList;
dupChunkDetails.timestamp = timestamp;
nesteds.chunkDetails.push( ...dupChunkDetails );
nesteds.allChanges.forEach( ( x, i ) => $.extend( x, allChanges[ i ] ) );
//p.chunkDetailsList.push( chunkDetails );
//console.log( nestedParents, p, p.chunkDetails, outer.parentNode.firstElementChild.lastElementChild );
addChunkDetails( dupChunkDetails, tbody.firstElementChild.lastElementChild, cdList );
if ( simpleSettings ) {
updateDisplay( [ nesteds ] );
}
}
// Add to the visible DOM.
addChunkDetails( chunkDetails, diffLinkParent );
if ( simpleSettings ) {
updateDisplay( [ diff ] );
}
} );
}
// Run through every diff link.
$( changeSelector ).each( function( i, diffLink ) {
// The diff API will only give one at a time...
var href = diffLink.href,
oldid = mw.util.getParamValue( "oldid", href ), //result.old_revid,
pageid = mw.util.getParamValue( "curid", href ),
diffid = mw.util.getParamValue( "diff", href ),
title = mw.util.getParamValue( "title", href ),
diffLinkParent = listType === 'history' ?
diffLink.parentNode.parentNode :
diffLink.parentNode,
enhanced = diffLinkParent.nodeName === "TD";
if (
// Only well-formed diff links
diffid && title && oldid &&
// On history pages, don't show earliest diff.
// Don't show earliest diff on page, even if there's a diff
// link, because rvstartid doesn't play nice with "prev".
// TODO: Fix.
( listType !== "history" || diffLinkParent.nextElementSibling /* || diffLink.previousSibling.previousSibling */ ) &&
// Check namespace, only show mainspace and Property: NS.
( !title.includes( ":" ) || title.startsWith( "Property:" ) ) &&
// Suppress diffs with nested sub-listings.
( !enhanced || !diffLinkParent.parentNode.nextElementSibling || diffLinkParent.parentNode.previousElementSibling )
) {
processDiff( diffLinkParent, oldid, pageid, diffid, title );
}
} );
buildOptionsBox();
});
});
}
mw.loader.using( mw.config.get( 'wgDBname' ) === 'wikidatawiki' ? 'mediawiki.api' : 'mediawiki.ForeignApi', DiffLists );
/*
$.get( api, {
format: "json",
action: "query",
list: "recentchanges",
rcnamespace: 0,
rcprop: "ids"
}, function ( r ) {
//console.log( r );
//var results = r.query.recentchanges; // array
results.forEach( function ( result ) {
var oldid = result.old_revid,
pageid = result.pageid;
*/