Nota: Depois de publicar, poderá ter de contornar a cache do seu navegador para ver as alterações.

  • Firefox / Safari: Pressione Shift enquanto clica Recarregar, ou pressione Ctrl-F5 ou Ctrl-R (⌘-R no Mac)
  • Google Chrome: Pressione Ctrl-Shift-R (⌘-Shift-R no Mac)
  • Internet Explorer / Edge: Pressione Ctrl enquanto clica Recarregar, ou pressione Ctrl-F5
  • Opera: Pressione Ctrl-F5.
//une extension de Date pour qu'il comprenne  le format ISO8601 utilisé par l'API (ya une fonction native mais seulement sur firefox 3.5+)
// http://dansnetwork.com/2008/11/01/javascript-iso8601rfc3339-date-parser/ 
Date.prototype.setISO8601 = function(dString){
  var regexp = /(\d\d\d\d)(-)?(\d\d)(-)?(\d\d)(T)?(\d\d)(:)?(\d\d)(:)?(\d\d)?(Z)/;
 
  if (dString.toString().match(new RegExp(regexp))) {
     var d = dString.match(new RegExp(regexp));
     var offset = 0;
 
     this.setUTCDate(1);
     this.setUTCFullYear(parseInt(d[1],10));
     this.setUTCMonth(parseInt(d[3],10) - 1);
     this.setUTCDate(parseInt(d[5],10));
     this.setUTCHours(parseInt(d[7],10));
     this.setUTCMinutes(parseInt(d[9],10));
     this.setUTCSeconds(parseInt(d[11],10));
  }
  else {
    this.setTime(Date.parse(dString));
  }
  return this;
};
 
 
Date.prototype.getYYYYMMDD = function(){
  var month = this.getMonth()+1
  if(month < 10) month = "0"+month
  var day = this.getDate()
  if(day < 10) day = "0"+day
 
  return ( this.getFullYear() +"-"+ month +"-"+ day  );
};
 
 
//essai d'extension du DOM Element. ça rend de facto le script incompatible avec IE7- et Safari 2
Element.prototype.createAndAppendTextNode = function(p_txt) {
   return this.appendChild( document.createTextNode( p_txt ) )
}
 
//creation d'un element + attributs + éventuels node texte dedans ; et ajout au node parent
Element.prototype.createAndAppendElement = function(p_el, p_attributs, p_textnode) {
   var xx = this.appendChild( document.createElement( p_el ) )
   for(key in p_attributs) {
       xx.setAttribute( key , p_attributs[key] )
   }
   if(p_textnode) {
       xx.createAndAppendTextNode( p_textnode )
   }
   return xx
}
 
 
//supprime tous les sous nodes d'un node (pas besoin d'être récursif).
Element.prototype.removeChildNodes =  function() {
   while (this.hasChildNodes() )  {
      this.removeChild(this.firstChild);
   }
}
 
 
 
/*
 Auteur : user:Darkoneko
 tous les id css sont préfixées de nekotb_fc (pour "NEKO ToolBox - Fusionneur de Contribs") pour eviter les conflits avec d'autres scripts
*/
 
var nekotb_fc = new Object()
 
nekotb_fc.bgcolor_list = new Array( //utilisés pour la coloration des lignes de chaque user
  "lightblue",
  "yellow",
  "orange",
  "lightgreen",
  "white",
  "#FCC",
  "#BBB",
  "lightyellow"
) //note : pas de virgule après le dernier terme ou ça plante !
 
 
 
nekotb_fc.user_list = new Array() //stocke la liste des utilisateurs fusionnés
nekotb_fc.contrib_limit_per_user = 7500; //nb max de contribs récupérées par compte
nekotb_fc.edit_clash_limit = 5; //(en minutes) espacement sous lequel des edits de 2 comptes différents sont considérés comme en collision
nekotb_fc.timeline_nb_day_per_line = 5 //nombre de jours affichés par ligne (en + du nom des comptes) en mode timeline
 
nekotb_fc.oldest_timestamp = false;
nekotb_fc.newest_timestamp = false;
nekotb_fc.user_contribs = new Array()
nekotb_fc.every_contrib_ordered = new Array()
 
 
 
nekotb_fc.addLinkToLeftBar = function() {
   var ul = document.getElementById("p-navigation").getElementsByTagName("ul")[0]
   var li = ul.createAndAppendElement("li")
   li.createAndAppendElement( "a", {href:'#', 'onclick':"return nekotb_fc.init()"}, '@Fusion contribs' )
 
}
$(nekotb_fc.addLinkToLeftBar)
 
 
 
 
nekotb_fc.init = function() {
   document.getElementById("firstHeading").firstChild.nodeValue = "Neko toolbox : fusionneur de contribs,  v1.1.beta du 12/12/2010"
 
   var content = document.getElementById("bodyContent")
   content.removeChildNodes()    //supprimer le contenu initial de la zone 'article".
 
   var root = content.createAndAppendElement("div",  {'id':"nekotb_fc_formulaire"} )
   root.createAndAppendElement("h2", {}, "Zone temporelle à inspecter")
 
      //on laisse à l'user le choix dans la date 
   curYear = new Date().getFullYear();
   root.createAndAppendTextNode("Fusionner entre le ")
   root.createAndAppendElement("input", {'id':"nekotb_fc_date_debut", 'value':curYear+"-01-01"} )
   root.createAndAppendTextNode(" et le ")
   root.createAndAppendElement("input", {'id':"nekotb_fc_date_fin", 'value':curYear+"-12-31"})
 
   root.createAndAppendElement("h2", {}, "Comptes à fusionner")
 
      //ajout des users
   var form = root.createAndAppendElement("form", {"onsubmit":"return nekotb_fc.add_user();"} )
   form.createAndAppendElement("input", {'id':"nekotb_fc_user_input"} )
   form.createAndAppendTextNode(" (appuyez sur entrée pour ajouter)")
 
   root.createAndAppendElement("ul", {'id':"nekotbfc_liste_users"} ) //la liste des users ajoutés sera affichée dans cet element
   root.createAndAppendElement("hr")
   root.createAndAppendElement( "a", { href:"#", "onclick":"return nekotb_fc.show_fusion()" }, "=> lancer la fusion <=" )
 
      //réglages divers
   root.createAndAppendElement("h2", {}, "Réglages optionnels divers")
 
   root.createAndAppendElement("h3", {}, "Nombre max de contributions récupérées par utilisateur")
   root.createAndAppendElement("input", { 'id' : "nekotb_fc_contrib_limit_per_user", "value": nekotb_fc.contrib_limit_per_user } )
   root.createAndAppendTextNode(" (sera arrondi au multiple de 500 supérieur)")
 
   root.createAndAppendElement("h3", {}, "indicateur de collisions")
   root.createAndAppendTextNode("Ecart maximal de temps pour que des éditions de 2 users différents soient considérées comme entrant en 'collision' : ")
   root.createAndAppendElement("br")
   root.createAndAppendElement("input", { 'id' : "nekotb_fc_edit_clash_limit", "value":nekotb_fc.edit_clash_limit  })
   root.createAndAppendTextNode(" minutes")
 
   root.createAndAppendElement("h3", {}, "timeline")
   root.createAndAppendTextNode("Nombre de jours par ligne")
   root.createAndAppendElement("br")
   root.createAndAppendElement("input", { 'id' : "nekotb_fc_timeline_nb_day_per_line", "value":nekotb_fc.timeline_nb_day_per_line  })
 
   return false
}
 
 
nekotb_fc.add_user = function() {
   var input = document.getElementById("nekotb_fc_user_input")
   var name = input.value
   input.value = '' //on vide le champ 
   if(name.length < 1 ) return false //si champ vide, on ignore silencieusement
 
   var ul = document.getElementById("nekotbfc_liste_users")
 
   var randomNumber = Math.floor(Math.random()*100001) //generation d'un id [a priori] unique, comme le nom peut contenir des chars non valides
   var li = ul.createAndAppendElement("li", {'id':"nekotb_fc_"+randomNumber, "onclick":"nekotb_fc.remove_user('nekotb_fc_"+randomNumber +"')"} )
   li.createAndAppendTextNode(name)
   var img = li.createAndAppendElement("img", {'src':"http://upload.wikimedia.org/wikipedia/commons/thumb/4/46/Pictogram_voting_delete.svg/15px-Pictogram_voting_delete.svg.png"} )
 
   nekotb_fc.user_list.push( name.replace(/ /g, "_") )
 
   return false
}
 
nekotb_fc.remove_user = function(idnode) {
   var node = document.getElementById(idnode)
   var name = node.firstChild.nodeValue.replace(/ /g, "_")
   node.parentNode.removeChild(node) 
 
   for(var a=0 ; a < nekotb_fc.user_list.length ; a++) {
      if(nekotb_fc.user_list[a] == name) {
         nekotb_fc.user_list.splice(a, 1);
         break;
      }
   }
   return false
}
 
 
nekotb_fc.back_to_form = function() {
   var div2 = document.getElementById("nekotb_fc_fusion")
   div2.parentNode.removeChild(div2)  //supprimer le div de resultat complement
   document.getElementById("nekotb_fc_formulaire").style.display = "block"
   return false
}
 
 
 
nekotb_fc.fetch_and_verify_parameters = function() {
   if( nekotb_fc.user_list.length < 1 )  {
       alert("aucun utilisateur selectionné")
       return false
   }
 
   var reg_int = new RegExp(/^[0-9]+$/)
 
    //recuperer les params
   nekotb_fc.contrib_limit_per_user = document.getElementById("nekotb_fc_contrib_limit_per_user").value
   if( ! reg_int.test(nekotb_fc.contrib_limit_per_user) ) {
       alert("la limite de contributions récupérées par compte doit être un nombre entier")
       return false
   }
 
   nekotb_fc.edit_clash_limit = document.getElementById("nekotb_fc_edit_clash_limit").value
   if( ! reg_int.test(nekotb_fc.edit_clash_limit ) ) {
       alert("l'écart pour l'indicateur de clash doit etre un nombre entier")
       return false
   }
 
   nekotb_fc.timeline_nb_day_per_line = document.getElementById("nekotb_fc_timeline_nb_day_per_line").value
   if( ! reg_int.test(nekotb_fc.timeline_nb_day_per_line ) ) {
       alert("le nombre de jour par ligne de la timeline doit etre un nombre entier")
       return false
   }
 
 
   var reg_date = new RegExp(/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/)
 
   var debut = document.getElementById("nekotb_fc_date_debut").value
   if( ! reg_date.test(debut) ) {
       alert("date de début est incorrect")
       return false
   }
   nekotb_fc.oldest_timestamp = debut + "T00:00:00Z" 
 
   var fin = document.getElementById("nekotb_fc_date_fin").value
   if( ! reg_date.test(fin) ) {
       alert("date de fin est incorrect")
       return false
   }
   nekotb_fc.newest_timestamp = fin + "T23:59:59Z"
 
 
   return true
}
 
 
 
 
nekotb_fc.show_fusion = function() {
    if( ! nekotb_fc.fetch_and_verify_parameters() ) return false;
 
   nekotb_fc.user_count = 0 //mise ou remise à 0 du compteur utilisé pour la colorisation
 
   document.getElementById("nekotb_fc_formulaire").style.display = "none" //masquer le div du formulaire (sans le supprimer)
 
   //creer le div qui contiendra les resultats
   var div2 = document.getElementById("bodyContent").createAndAppendElement( "div", {'id':"nekotb_fc_fusion"} )
   div2.createAndAppendElement( "a", {'href':"#", 'onclick':"return nekotb_fc.back_to_form()"}, "revenir au formulaire" )
 
   div2.createAndAppendElement( "h2", {}, "Récupération des contributions" ) 
   div2.createAndAppendElement("ul", { 'id' : "nekotb_fc_fusion_user_list" }) //utilisé pour jouer avec l'affichage en temps réel de la recup de contribs 
 
   div2.createAndAppendElement("h2", {}, "Liste des contributions fusionnées") 
   div2.createAndAppendTextNode("Note : les heures indiquées sont en UTC")
 
  //recuperation des contributions par user
   for(var a=0, len = nekotb_fc.user_list.length ; a < len ; a++) {
       nekotb_fc.user_contribs[ nekotb_fc.user_list[a] ] = nekotb_fc.get_contribution_list( nekotb_fc.user_list[a])
       nekotb_fc.every_contrib_ordered = nekotb_fc.every_contrib_ordered.concat( nekotb_fc.user_contribs[ nekotb_fc.user_list[a] ] ); 
       nekotb_fc.user_contribs[ nekotb_fc.user_list[a] ].sort( function(a, b){ return a['revid'] - b['revid'] } ) //tri chronologique ascendant
   }
   nekotb_fc.every_contrib_ordered.sort( function(a, b){ return b['revid'] - a['revid'] } ) //tri chronologique descendant 
   var len = nekotb_fc.every_contrib_ordered.length 
   var dat = new Date()
 
   if( len > 0 ) {
     //test de creation de ligne
     var first_timestamp = dat.setISO8601(nekotb_fc.oldest_timestamp).getTime() //(car antéchronologique)
     var last_timestamp = dat.setISO8601(nekotb_fc.newest_timestamp).getTime() 
 
     nekotb_fc.show_timeline(first_timestamp, last_timestamp)
   }
   return false; //TEST timeline
 
 
   var ul = div2.createAndAppendElement("ul")
 
   if( len > 0 ) {
      ul.appendChild( nekotb_fc.create_contrib_line(nekotb_fc.every_contrib_ordered[0]), false ) //car on ne peut pas comparer cette premiere ligne avec "celle d'avant"
      for(var a = 1; a < len ; a++ ) {
         var editClash = false //detecteur de clash  
         if( nekotb_fc.every_contrib_ordered[a]['bgcolor'] != nekotb_fc.every_contrib_ordered[a-1]['bgcolor'] ) {
            var ecart = dat.setISO8601(nekotb_fc.every_contrib_ordered[a-1]['timestamp']).getTime() - dat.setISO8601(nekotb_fc.every_contrib_ordered[a]['timestamp']).getTime() / 1000
            if( ecart < (nekotb_fc.edit_clash_limit * 60) ) {
               editClash = true
            }
         }   
         ul.appendChild( nekotb_fc.create_contrib_line(nekotb_fc.every_contrib_ordered[a], editClash ) )
      }
   }
   return false
}
 
 
 
 
nekotb_fc.show_timeline = function(first_timestamp, last_timestamp) {
   var div2 = document.getElementById("nekotb_fc_fusion");
   var dat = new Date();
   var final = new Array()
   var nb_users = nekotb_fc.user_list.length
 
   div2.createAndAppendElement("br")
   div2.createAndAppendTextNode("/!\\ Note : à cause des passages à l'heure d'été, le dernier dimanche de mars comporte 23 heures et le dernier dimanche d'octobre 25 heures")
 
   //partie recuperation 
   for( var a = 0 ; a < nb_users ; a++) {
      var cur_day = new Date(first_timestamp).getYYYYMMDD()
      var user = nekotb_fc.user_list[a]
      final[user] = new Array()
      var ligne = ''
 
      var length_user_list = nekotb_fc.user_contribs[user].length      
 
      for(var b=0, next_timestamp=false, cur_timestamp=first_timestamp ; cur_timestamp <= last_timestamp ; cur_timestamp=next_timestamp) {
         next_timestamp = cur_timestamp + 1000 * 60 * 60 //1H plus tard
         var edit = 0
 
         while( b < length_user_list && dat.setISO8601(nekotb_fc.user_contribs[user][b]['timestamp']).getTime() < next_timestamp ) {
             edit++
             b++
         }
 
         //caractere a afficher pour cette heure
         if(edit == 0) {         ligne += "_"   } 
         else if (edit < 10) {   ligne += edit  }
         else {                  ligne += "X"   }
 
         var new_day = new Date(cur_timestamp).getYYYYMMDD() //test changement de jour pour remplir une case
         if(cur_day != new_day) {
             final[user][cur_day] = new Array()
             final[user][cur_day] = ligne
             ligne = ''
             cur_day = new_day
         }
      } //boucle d'un user
      final[user][cur_day] = new Array()
      final[user][cur_day] = ligne //mise du dernier jour
   } //boucle users
 
   //partie affichage
 
   var compteur_jours = 0;
   for(var jour = first_timestamp ; jour < last_timestamp ; jour += (1000 * 60 * 60 * 24) ) {
       if( compteur_jours % nekotb_fc.timeline_nb_day_per_line == 0 ) {
          var table = div2.createAndAppendElement("table", {"class":"nekotb_fc_timeline", "style":"font-family: monospace; border:1px solid black; margin-top:5px;"})
          var tr = new Array();
          for(var a=0 ; a < nb_users+1 ; a++) {
            tr[a] = table.createAndAppendElement("tr")
          }
          //premiere colonne : la liste des users 
          tr[0].createAndAppendElement("td", {}, " ")
          for(var a=0 ; a < nb_users ; a++) {
            tr[a+1].createAndAppendElement("td", {}, nekotb_fc.user_list[a])
          }
       }
 
       var cur_day = new Date(jour)
       tr[0].createAndAppendElement("td", {}, cur_day.getYYYYMMDD() )
       for(var b = 0 ; b < nekotb_fc.user_list.length ; b++) {
          tr[b+1].createAndAppendElement("td", {}, final[ nekotb_fc.user_list[b] ][ cur_day.getYYYYMMDD() ] )
       }
 
       compteur_jours++
    }
}
 
 
 
 
nekotb_fc.create_contrib_line = function(tab, editClash) {
   var li = document.createElement("li")
 
   if( editClash == true) { //ligne rouge entre les 2 contribs incriminées
       li.setAttribute('style', "border-top: 5px solid red; background-color:"+tab['bgcolor']+";" );
   } else {
       li.setAttribute('style', "background-color:"+tab['bgcolor']+";" )
   }
   var timestamp = tab['timestamp'].replace("T", " à ").replace("Z", "") //mise en page du timestamp
 
   li.createAndAppendElement( "a",  {'href':"/w/index.php?title="+tab['title']+"&oldid="+tab['revid']}, timestamp )
   li.createAndAppendTextNode(" (")
   li.createAndAppendElement( "a",  {'href':"/w/index.php?title="+tab['title']+"&oldid="+tab['revid']+"&diff=prev"}, "diff" )
   li.createAndAppendTextNode(" | ")
   li.createAndAppendElement( "a",  {'href':"/w/index.php?title="+tab['title']+"&action=history"}, "hist" )
   li.createAndAppendTextNode(") ")
   if( tab['new'] != null ) {
      li.createAndAppendElement("span", {'class':"newpage"}, "N" ) 
   }
   if( tab['minor'] != null ) {
      li.createAndAppendElement("span", {'class':"minor"}, "m" ) 
   }
   li.createAndAppendTextNode(" ")
   li.createAndAppendElement( "a",  {'href':"/wiki/"+tab['title']}, tab['title'] )
 
   if( tab['comment'] != null && tab['comment'] != "" ) {
      li.createAndAppendTextNode(" (")
      li.createAndAppendElement("span", {'class':"comment"}, tab['comment'] )
      li.createAndAppendTextNode(")")
   }
   if( tab['top'] != null ) {
       li.createAndAppendTextNode(" ")
       li.createAndAppendElement("span", {'class':"mw-uctop"}, "(dernière)" ) 
   }
   return li
}
 
 
 
 
/*
  retourne un tableau a partir d'une (ou plusieurs) requetes API
*/ 
nekotb_fc.get_contribution_list = function(p_user) { 
   var bgcolor = nekotb_fc.bgcolor_list[nekotb_fc.user_count++]   // choix de la couleur de fond
 
   var ul = document.getElementById('nekotb_fc_fusion_user_list') // pour faire joujou avec l'affichage
   var li = ul.createAndAppendElement( "li", {'style':"background-color:"+bgcolor+";"}, " ")
 
   var http_request = sajax_init_object()
   http_request.overrideMimeType('text/xml');
 
   var tableau = new Array()
   var compteur_tableau = 0
 
   var newest_timestamp = nekotb_fc.newest_timestamp //variable locale car changé à chaque boucle
 
   do {
     if( tableau.length >= nekotb_fc.contrib_limit_per_user ) { 
        li.firstChild.nodeValue += " Limite atteinte - les contributions les plus anciennes n'apparaitrons pas"
        break; 
     }
 
     var continue_do_while = false
     var address = "/w/api.php?format=xml&action=query&list=usercontribs&uclimit=500&ucuser=" + p_user + "&ucend=" + nekotb_fc.oldest_timestamp + "&ucstart=" + newest_timestamp
     http_request.open('GET', address , false)
     http_request.send(null)
     var lignes = http_request.responseXML.documentElement.getElementsByTagName("item")
 
     for (var a = 0, len = lignes.length ; a < len ; a++) { 
        tableau[compteur_tableau] = new Array()
 
        for (var b = 0, len2 = lignes[a].attributes.length ; b < len2; b++) {
            tableau[compteur_tableau][ lignes[a].attributes.item(b).name ] = lignes[a].attributes.item(b).value
        }
        tableau[compteur_tableau]['bgcolor'] = bgcolor //affectée ici pour ne pas avoir à reparser le tableau plus tard
 
        compteur_tableau ++
     }
 
     li.firstChild.nodeValue = p_user + " : " + tableau.length + " contributions récupérées."
 
     query_continue = http_request.responseXML.documentElement.getElementsByTagName("query-continue")
     if( query_continue && query_continue.length > 0 )  {
        newest_timestamp = query_continue[0].getElementsByTagName("usercontribs")[0].getAttribute('ucstart')
        if( newest_timestamp && newest_timestamp != 'undefined' ) { //protection
           continue_do_while = true
        }
     }
   }  while( continue_do_while );
 
   return tableau
}