User:Splarka/ajaxrecentchanges.js

Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/* Ajax recent changes and patrolling framework, version [0.0.5a]
Originally from: http://en.wikipedia.org/wiki/User:Splarka/ajaxrecentchanges.js

Note:
* Patrol flags/links will sometimes show up where patrolling is not enabled.
** This was a bug in the API that generated patrol tokens too often.
** Fixed in http://svn.wikimedia.org/viewvc/mediawiki?view=rev&revision=49000

Todo:
* Checkbox for batch patrolling

Wontdo:
* Parse comments
* Add (talk|contribs|block) links, click their name, lazy
* Localize error messages or focus on the log message 'logaction' (wrong tense, but it works fine).
*/

if(!window.arc_i18n) {
  var arc_i18n = { 
  'title'	:'Ajax recent changes',
  'desc'	:'Paginated enhanced ajax recent changes and patrolling.',
  'mypatrol'	:'My patrol log',
  'startstamp'	:'Start timestamp (8601)',
  'limit'	:'Limit',
  'showapb'	:'Show ajax patrol buttons',
  'filterflag'	:'Filter by flag',
  'minor'	:'Minor',
  'bot'		:'Bot',
  'anon'	:'Anon',
  'redirect'	:'Redirect',
  'patrolled'	:'Patrolled',
  'all'		:'All',
  'filtertype'	:'Filter by type',
  'edit'	:'Edits',
  'new'		:'New pages',
  'log'		:'Logs',
  'filterns'	:'Filter by namespace',
  'fetch'	:'Fetch',
  'noresults'	:'Nothing found.',
  'diff'	:'diff',
  'hist'	:'hist',
  'patrolbtn'	:'Patrol',
  'logsuffix'	:' log',
  'patroldone'	:'done',
  'nsmain'	:'MAIN'
  }
}

$(function() {
  mw.util.addPortletLink('p-tb','/wiki/Special:BlankPage?blankspecial=ajaxrc',arc_i18n['title'],'t-ajax-rc',arc_i18n['desc']);
});

if(wgCanonicalSpecialPageName && wgCanonicalSpecialPageName.toLowerCase() == 'blankpage' && queryString('blankspecial') == 'ajaxrc') {
  document.title = arc_i18n['title'];
  addOnloadHook(ajaxRcForm);
}

function ajaxRcForm() {
  mw.util.addPortletLink('p-tb','/wiki/Special:Log/patrol?user=' + encodeURIComponent(wgUserName),arc_i18n['mypatrol']);
  //subvert this Special: page to our own needs.
  var con = document.getElementById('content') || document.getElementById('mw_content');
  var bcon = document.getElementById('bodyContent') || document.getElementById('mw_contentholder');
  var fh = getElementsByClassName(con,'h1','firstHeading')[0];
  while(fh.firstChild) fh.removeChild(fh.firstChild)
  fh.appendChild(document.createTextNode(arc_i18n['title']));
  for(var i=0;i<bcon.childNodes.length;i++) {
    bcur = bcon.childNodes[i];
    if(bcur.id != 'siteSub' && bcur.id != 'contentSub' && bcur.className != 'visualClear') {
      while(bcur.firstChild) bcur.removeChild(bcur.firstChild)
      if(bcur.nodeType == 3) bcur.nodeValue = '';
    }
  }

  appendCSS(
    '#arc-form {border:1px solid black;padding:.5em;margin:2em;} #arc-out {border:1px solid black;padding:.5em;margin:.5em;}'
  + '#arc-fetch {padding:0 1em;margin:0 .5em;} .clear {clear:both;} .arc-box {border:1px solid #bbbbbb;padding:.2em;margin:.5em;}'
  + '.arc-cbox {display:block;float:left;width:11em;white-space:nowrap;overflow:hidden;font-size:80%;margin:0 .f2em;}'
  + '.arc-box-label {text-align:center;border-bottom:1px solid #bbbbbb;margin-bottom:.3em} .spacer {border:1px solid transparent;margin-right:.5em;}'
  + '.arc-patrol {border:2px outset #bbbbbb;background-color:#bbbbbb;color:black;padding:2px;margin:3px;text-decoration:none;}'
  );

  var form = '<form id="arc-form" action="javascript:void(0)">'
    + '<label for="arc-start">' + arc_i18n['startstamp'] + ':</label> <input type="text" name="arc-start" id="arc-start" value="" size="25" maxlength="20"/><span class="spacer"></span>'
    + '<label for="arc-limit">' + arc_i18n['limit'] + ':</label> <input type="text" name="arc-limit" id="arc-limit" value="50" size="5" maxlength="3"/><span class="spacer"></span>'
    + '<input type="checkbox" name="arc-patrol-enable" id="arc-patrol-enable" /><label for="arc-patrol-enable">' + arc_i18n['showapb'] + '</label><br/>'
    + '<div class="arc-box" id="arc-f-boxen">' + arc_i18n['filterflag'] + ': <span class="spacer"></span>'
    + '<label for="arc-f-minor">' + arc_i18n['minor'] + ':</label> <input type="button" name="arc-f-minor" id="arc-f-minor" value="' + arc_i18n['all'] + '" onclick="ajaxRcFlagChange(this)" /><span class="spacer"></span>'
    + '<label for="arc-f-bot">' + arc_i18n['bot'] + ':</label> <input type="button" name="arc-f-bot" id="arc-f-bot" value="!bot" onclick="ajaxRcFlagChange(this)" /><span class="spacer"></span>'
    + '<label for="arc-f-anon">' + arc_i18n['anon'] + ':</label> <input type="button" name="arc-f-anon" id="arc-f-anon" value="' + arc_i18n['all'] + '" onclick="ajaxRcFlagChange(this)" /><span class="spacer"></span>'
    + '<label for="arc-f-redirect">' + arc_i18n['redirect'] + ':</label> <input type="button" name="arc-f-redirect" id="arc-f-redirect" value="' + arc_i18n['all'] + '" onclick="ajaxRcFlagChange(this)" /><span class="spacer"></span>'
    + '<label for="arc-f-patrolled">' + arc_i18n['patrolled'] + ':</label> <input type="button" name="arc-f-patrolled" id="arc-f-patrolled" value="' + arc_i18n['all'] + '" onclick="ajaxRcFlagChange(this)" />'
    + '</div>'
    + '<div class="arc-box" id="arc-t-boxen">' + arc_i18n['filtertype'] + ': <span class="spacer"></span>'
    + '<input type="checkbox" name="arc-t-edit" id="arc-t-edit" checked="checked" value="edit" /><label for="arc-t-edit">' + arc_i18n['edit'] + '</label><span class="spacer"></span>'
    + '<input type="checkbox" name="arc-t-new" id="arc-t-new" checked="checked" value="new" /><label for="arc-t-new">' + arc_i18n['new'] + '</label><span class="spacer"></span>'
    + '<input type="checkbox" name="arc-t-log" id="arc-t-log" checked="checked" value="log" /><label for="arc-t-log">' + arc_i18n['log'] + '</label><span class="spacer"></span>'
    + '</div>'
    + '<div class="arc-box" id="arc-ns-boxen"><div class="arc-box-label">' + arc_i18n['filterns'] + '</div></div>'
    +  ''
    + '<input type="button" name="fetch" value="' + arc_i18n['fetch'] + '" id="arc-fetch" onclick="ajaxRcFetch()" /><div class="clear"></div>'
    + '</form>'

  bcon.innerHTML += form + '<div id="arc-out"></div>';
  mw.loader.load(wgScriptPath + '/api.php?action=query&meta=siteinfo&siprop=namespaces&format=json&callback=ajaxRcFormNamespacesCB&smaxage=2678400&maxage=2678400');
}

function ajaxRcFetch(timestamp,direction) {
  document.getElementById('arc-fetch').setAttribute('disabled','disabled');
  var nav = document.getElementById('arc-fetchnav');
  if(nav) nav.style.visibility = 'hidden'
  injectSpinner(document.getElementById('arc-fetch'),'arc-spin');

  //direction
  var rcdir = '';
  if(direction) rcdir = '&rcdir=' + direction + '&requestid=' + direction

  //start
  var rcstart = timestamp || document.getElementById('arc-start').value;
  rcstart = rcstart.replace(/[^\d]*/g,'');
  if(rcstart != '' && /^\d{14}$/.test(rcstart)) {
    rcstart = '&rcstart=' + rcstart;
  } else {
    rcstart = '';
  }

  //limit
  var rclimit = parseInt(document.getElementById('arc-limit').value);
  if(isNaN(rclimit)) rclimit = 100
  rclimit = '&rclimit=' + rclimit;

  //type
  var tb = document.getElementById('arc-t-boxen').getElementsByTagName('input');
  var rctype = [];
  for(var i=0;i<tb.length;i++) if(tb[i].checked) rctype.push(tb[i].value)
  if(rctype.length > 0) {
    rctype = '&rctype=' + rctype.join('|');
  } else {
    rctype = '';
  }

  //show (flags)
  var fb = document.getElementById('arc-f-boxen').getElementsByTagName('input');
  var rcshow = [];
  for(var i=0;i<fb.length;i++) if(fb[i].value != arc_i18n['all']) rcshow.push(fb[i].value) 
  if(rcshow.length > 0) {
    rcshow = '&rcshow=' + rcshow.join('|');
  } else {
    rcshow = '';
  }

  //namespace
  var nsb = document.getElementById('arc-ns-boxen').getElementsByTagName('input');
  var rcnamespace = [];
  for(var i=0;i<nsb.length;i++) if(nsb[i].checked) rcnamespace.push(nsb[i].value)
  if(rcnamespace.length > 0) {
    rcnamespace = '&rcnamespace=' + rcnamespace.join('|');
  } else {
    rcnamespace = '';
  }

  //prop & token
  var rcprop = '&rcprop=user|comment|flags|timestamp|title|ids|sizes|redirect|patrolled|loginfo';
  var rctoken = '&rctoken=patrol';

  var url = wgScriptPath + '/api.php?action=query&rawcontinue=&format=json&list=recentchanges' + rcdir + rcstart + rclimit + rctype + rcshow + rcnamespace + rcprop + rctoken;
  var req = sajax_init_object();
  req.open('GET', url, true);
  req.onreadystatechange = function() {
    if(req.readyState == 4 && req.status == 200) {
      eval("ajaxRcFetchHandler(" + req.responseText + ",'" + req.responseText.replace(/\'/g,"`") + "')");
    }
  }
  req.send(null);
}

function ajaxRcFetchHandler(obj,txt) {
  document.getElementById('arc-fetch').removeAttribute('disabled');
  removeSpinner('arc-spin');
  var out = document.getElementById('arc-out');
  var ajaxpatrol = document.getElementById('arc-patrol-enable').checked;
  while(out.firstChild) out.removeChild(out.firstChild)
  if(obj['error']) {
    out.appendChild(document.createTextNode('Api error: ' + obj['error']['code'] + ' - ' + obj['error']['info'] + '\n'));
    return;
  }
  if(!obj['query'] || !obj['query']['recentchanges']) {
    out.appendChild(document.createTextNode('Unexpected response: ' + txt + '\n'));
    return;
  }
  var rc = obj['query']['recentchanges'];
  if(rc.length == 0) {
    out.appendChild(document.createTextNode(arc_i18n['noresults']));
    return;
  }

  var backwards = false;
  if(obj['requestid'] && obj['requestid'] == 'newer') backwards = true

  var nav = document.createElement('div');
  nav.setAttribute('id','arc-fetchnav');
  if(obj['query-continue'] && obj['query-continue']['recentchanges'] && obj['query-continue']['recentchanges']['rcstart']) {
    var rcstart = obj['query-continue']['recentchanges']['rcstart'];
    var rcstartnewer = rcstart;
    var rcstartolder = rcstart;
    if(!backwards) {
      rcstartnewer = rc[0]['timestamp'];
    } else {
      rcstartolder = rc[0]['timestamp'];
    }
    addLinkChild(nav,'javascript:ajaxRcFetch("' + rcstartnewer + '","newer")','Newer');
    addText(nav,' | ');
    addLinkChild(nav,'javascript:ajaxRcFetch("' + rcstartolder + '","older")','Older');
  } else if(backwards) {
    addLinkChild(nav,'javascript:ajaxRcFetch()','Older');
  }
  out.appendChild(nav);

  var ul = document.createElement('ul');
  for(var i=0;i<rc.length;i++) {
    var r = rc[i];
    var li = document.createElement('li');
    if(r['type'] == 'edit') {
      var rcid = ''
      if(typeof r['patrolled'] == 'undefined' && r['rcid'] && r['patroltoken']) rcid = '&rcid=' + r['rcid']
      addText(li,'(');
      addLinkChild(li,wgScript + '?oldid=' + r['old_revid'] + '&diff=' + r['revid'] + rcid,arc_i18n['diff']);
      addText(li,') (');
      addLinkChild(li,wgScript + '?curid=' + r['pageid'] + '&action=history',arc_i18n['hist']);
      addText(li,') . . ');
      if(typeof r['bot'] != 'undefined') addText(li,'b','span','bot')
      if(typeof r['minor'] != 'undefined') addText(li,'m','span','minor')
      if(rcid != '' && r['patroltoken'])addText(li,'!','span','unpatrolled');
      addText(li,' ');
      addLinkChild(li,wgScript + '?curid=' + r['pageid'],r['title']);
      var size = '' + (parseInt(r['newlen']) - parseInt(r['oldlen']));
      if(size.substring(0,1) != '-') size = '+' + size
      addText(li,'; ' + r['timestamp'].replace(/[TZ]/ig,' ') + ' . . (' + size + ') . . ' );
      addLinkChild(li,wgScript + '?title=Special:Contributions&target=' + encodeURIComponent(r['user']),r['user']);
      if(r['comment']) addText(li,' (' + r['comment'] + ')','i')
      if(ajaxpatrol == true && rcid != '' && r['patroltoken']) addLinkChild(li,'javascript:ajaxRcDoPatrol("' + r['rcid'] + '","' + encodeURIComponent(encodeURIComponent(r['patroltoken'])) + '")',arc_i18n['patrolbtn'],'arc-patrol-' + r['rcid'],'arc-patrol');
    } else if(r['type'] == 'new') {
      var rcid = ''
      if(typeof r['patrolled'] == 'undefined' && r['rcid']) rcid = '&rcid=' + r['rcid']
      addText(li,'(' + arc_i18n['diff'] + ') (');
      addLinkChild(li,wgScript + '?curid=' + r['pageid'] + '&action=history',arc_i18n['hist']);
      addText(li,') . . ');
      addText(li,'N','span','newpage');
      if(rcid != '' && r['patroltoken']) addText(li,'!','span','unpatrolled');
      addText(li,' ');
      addLinkChild(li,wgScript + '?curid=' + r['pageid'] + rcid,r['title']);
      addText(li,'; ' + r['timestamp'].replace(/[TZ]/ig,' ') + ' . . (+' + r['newlen'] + ') . . ' );
      addLinkChild(li,wgScript + '?title=Special:Contributions&target=' + encodeURIComponent(r['user']),r['user']);
      if(r['comment']) addText(li,' (' + r['comment'] + ')','i')
      if(ajaxpatrol == true && rcid != '' && r['patroltoken']) addLinkChild(li,'javascript:ajaxRcDoPatrol("' + r['rcid'] + '","' + encodeURIComponent(encodeURIComponent(r['patroltoken'])) + '")',arc_i18n['patrolbtn'],'arc-patrol-' + r['rcid'],'arc-patrol');
    } else if(r['type'] == 'log') {
      addText(li,'(');
      addLinkChild(li,wgScript + '?title=Special:Log&type=' + r['logtype'],r['logtype'] + arc_i18n['logsuffix']);
      addText(li,'); ' + r['timestamp'].replace(/[TZ]/ig,' ') + ' . . ' );
      addLinkChild(li,wgScript + '?title=Special:Contributions&target=' + encodeURIComponent(r['user']),r['user']);
      addText(li,' ' + r['logaction'] + ' ');
      addLinkChild(li,wgScript + '?title=' + encodeURIComponent(r['title']),r['title']);
      if(r['comment']) addText(li,' (' + r['comment'] + ')','i')
    }
    if(backwards && ul.firstChild) { 
      ul.insertBefore(li,ul.firstChild);
    } else { 
      ul.appendChild(li);
    }
  }
  out.appendChild(ul);
}

function ajaxRcDoPatrol(rcid,token) {
  var params = 'action=patrol&format=json&requestid=' + rcid + '&rcid=' + rcid + '&token=' + token;
  var url = wgScriptPath + '/api.php';
  var req = sajax_init_object();
  req.open('POST', url, true);
  req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
  req.setRequestHeader('Content-length', params.length);
  req.setRequestHeader('Connection', 'close');
  req.onreadystatechange = function() {
    if(req.readyState == 4 && req.status == 200) {
      eval("ajaxRcDidPatrol(" + req.responseText + ",'" + req.responseText.replace(/\'/g,"`") + "')");
    }
  }
  req.send(params);
}

function ajaxRcDidPatrol(obj,txt) {
  if(!obj['requestid']) return;
  if(obj['error']) {
    alert('Api error in patrolling rcid=' + obj['requestid'] + ' : ' + obj['error']['code'] + '\n' + obj['error']['info']);
    return;
  }
  var button = document.getElementById('arc-patrol-' + obj['requestid']);
  if(!button || !obj['patrol']) return
  button.setAttribute('href','javascript:alert("(' + arc_i18n['patroldone'] + ')");');
  addText(button,' (' + arc_i18n['patroldone'] + ')');
  //{"requestid":"80879","error":{"code":"permissiondenied","info":"Permission denied"}}
  //{"requestid":"80871","patrol":{"rcid":80871,"ns":2,"title":"Page Title Here"}}
}

function ajaxRcFlagChange(obj) {
  var type = obj.getAttribute('id').substring(6);
  var val = obj.value;
  if(val == type) {
    obj.value = '!' + type;
  } else if(val == '!' + type) {
    obj.value = arc_i18n['all'];
  } else {
    obj.value = type;
  }
}

function ajaxRcFormNamespacesCB(obj) {
  if(!obj['query'] || !obj['query']['namespaces']) return
  var ns = obj['query']['namespaces'];
  var nsb = document.getElementById('arc-ns-boxen');
  for(var i in ns) {
    if(typeof i != 'string' || ns[i]['id'] < 0) continue
    var title = ns[i]['*'];
    if(ns[i]['id'] == '') title = arc_i18n['nsmain']
    var canon = ns[i]['canonical'] || '';
    addCheckboxChild(nsb,'arc-ns-' + ns[i]['id'],i,false,'arc-ns-' + ns[i]['id'],title,'arc-cbox',ns[i]['id'] + ' => ' + canon);
    //nsb.appendChild(document.createElement('br'));
  }
  var div = nsb.appendChild(document.createElement('div'));
  div.setAttribute('class','clear');
}

function queryString(p) {
  var re = RegExp('[&?]' + p + '=([^&]*)');
  var matches;
  if (matches = re.exec(document.location)) {
    try { 
      return decodeURI(matches[1]);
    } catch (e) {
    }
  }
  return null;
}

function addText(obj,txt,elem,classes) {
  if(elem) {
    var e = document.createElement(elem);
    e.appendChild(document.createTextNode(txt));
    if(classes) e.setAttribute('class',classes);
    obj.appendChild(e);
    return e;
  } else {
    obj.appendChild(document.createTextNode(txt));
  }
}

function addLinkChild(obj,href,text,id,classes,title) {
  if(!obj || !href || !text) return false;
  var a = document.createElement('a');
  a.setAttribute('href',href);
  a.appendChild(document.createTextNode(text));
  if(id) a.setAttribute('id',id);
  if(classes) a.setAttribute('class',classes);
  if(title) a.setAttribute('title',title);
  obj.appendChild(a);
  return a;
}

function addCheckboxChild(obj,name,value,checked,id,label,classes,title) {
  if(!obj || !name) return false;
  var span = document.createElement('span');
  var c = document.createElement('input');
  c.setAttribute('name',name);
  c.setAttribute('type','checkbox');
  if(value) c.setAttribute('value',value)
  if(checked) c.setAttribute('checked','checked')
  if(title) c.setAttribute('title',title)
  span.appendChild(c);
  if(id) {
    c.setAttribute('id',id);
    if(label) {
      var l = document.createElement('label');
      l.setAttribute('for',id);
      l.appendChild(document.createTextNode(label));
      if(title) l.setAttribute('title',title)
      span.appendChild(l);
    }
  }
  if(classes) span.setAttribute('class',classes);
  obj.appendChild(span);
  return span;
}