
|


The CodeThis user script runs on all Wikipedia history pages. It adds an "Animate changes" button to the history page that acts as the main entry point for the rest of the script. The animation itself is a series of calls to Wikipedia's revision history interface. We create an XMLHttpRequest object to retrieve the actual revision text and associated metadata (such as the author and revision date). The script also constructs a slider (really, a styled <div> with appropriate styling and event handlers) that tracks the current status of the animation, from the first version of the page to the current revision. Save the following user script as wikipedia-animate.user.js:
// ==UserScript==
// @name Wikipedia Animate
// @namespace http://phiffer.org/greasemonkey/
// @description Animates page modifications between two specific edit points
// @include http://*.wikipedia.tld/*action=history*
// ==/UserScript==
// based on code by Dan Phiffer
// and included here with his gracious permission
function Animation() {
if (!document.getElementById('bodyContent')) {
return;
}
var url = window.location.href;
this.base_url = url.substr(0, url.indexOf('&'));
this.hostname = url.substr(7, url.indexOf('/', 8) - 7);
this.add_buttons();
this.add_options();
this.add_css();
}
Animation.prototype.add_buttons = function() {
// Create the animate buttons
var button1 = document.createElement('input');
button1.className = 'historysubmit';
button1.style.marginLeft = '5px';
button1.setAttribute('type', 'button');
button1.value = 'Animate changes';
button1.addEventListener('click', function() { animate.start(); },
true);
button1.setAttribute('id', 'animate_button1');
var button2 = button1.cloneNode(true);
button2.addEventListener('click', function() { animate.start(); },
true);
button2.setAttribute('id', 'animate_button2');
// Add the buttons to the page
var history = document.getElementById('pagehistory');
history.parentNode.insertBefore(button1, history);
history.parentNode.appendChild(document.createTextNode(' '));
history.parentNode.appendChild(button2);
}
Animation.prototype.add_options = function() {
// Create the options box
var toolbox = document.getElementById('p-tb');
var options = document.createElement('div');
options.className = 'portlet';
options.innerHTML = '<h5>animate options</h5><div class="pBody"><ul>' +
// Range selection
'<li>Animate over:' +
'<div><input type="radio" name="animate_range" id="animate_range_
selected" value="selected" checked="checked"/> Selected</div>' +
'<div><input type="radio" name="animate_range" id="animate_range_all"
value="all"/> All versions</div>' +
'<div><input type="checkbox" id="animate_skip_minor"/> Skip minor
edits</div>' +
// Diffs
'</li><li>Highlight diffs:' +
'<div><input type="radio" name="animate_diff" id="animate_diff_yes"
value="yes" checked="checked"/> Yes</div>' +
'<div><input type="radio" name="animate_diff" id="animate_diff_no"
value="no"/> No</div>' +
// Speed
'</li><li>Animate speed:' +
'<div>Pause <input type="text" id="animate_delay" value="0.5" size="3"
style="font-size: 10px" onblur="animate.option(this);"/> sec</div>' +
// Info
'</li><li>Include info:' +
'<div><input type="checkbox" id="animate_info_date" checked="checked"/>
Date/time</div>' +
'<div><input type="checkbox" id="animate_info_author" checked="checked"/
> Author</div>' +
'<div><input type="checkbox" id="animate_info_summary"
checked="checked"/> Change summary</div>' +
'</li></ul></div>';
toolbox.parentNode.appendChild(options);
}
Animation.prototype.add_css = function( ) {
// Add some CSS formatting rules for diffs
var head = document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = 'ins.diff { display: inline; background-color: #CFC;
font-weight: bold; } ' +
'del.diff { background-color: #FFA; display: inline; } ' +
'#animate_main { width: 100%; position: relative; } ' +
'#animate_controls { position: absolute; top: 0; left: 0; background:
transparent url(data:image/
png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAA8CAYAAACuGnCAAAAABGdBTUEAANbY1E9YM
gAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAABnSURBVHjaYvz///
9NBjTAxIAFjAqOCo4KDl7Bf0D8H4rBgAWIvwExIxIGC36G6oBLgAQ/oakEC75HspAJJvgOm/
a3uAQZkSSY8KrEEES2iAnm+I/
Y3PkJ6ka4ICOwWOOA+RkmCBBgAIPPFd35TefZAAAAAElFTkSuQmCC) repeat-x; width:
100%; } ' +
'#animate_controls span.text { background: #FFF; } ' +
'#animate_main div.content { position: absolute; top: 60px; display:
none; } ' +
'#animate_button { float: left; margin-left: 0; margin-top: 5px; width:
50px; } ' +
'#animate_scrubber { position: relative; width: 402px; height: 11px;
border: 1px solid #AAA; background: #F5F5F5; float: left; margin: 8px; } ' +
'#animate_load_progress { position: absolute; top: 1px; left: 1px;
background: #E1E1E1; height: 9px; width: 5px; visibility: hidden; } ' +
'#animate_playhead { position: absolute; left: 1px; top: 1px; cursor:
pointer; background: transparent url(data:image/
gif;base64,R0lGODlhCQAJAIAAAP///
wAAACH5BAEAAAAALAAAAAAJAAkAAAIRhBGnwYrcDJxvwkplPtchVQAAOw==) no-repeat;
height: 9px; width: 9px; } ' +
'#animate_status { font: 10px verdana, sans-serif; float: left; margin-top:
8px; } ' +
'#animate_info { font-size: 10px; margin: 0 0 20px 58px; }';
head.appendChild(style);
}
Animation.prototype.start = function( ) {
// Initialize variables
this.urls = new Array( );
this.info = new Array( );
this.pages = new Array( );
this.activity = new Array( );
this.activity_max = 0;
this.num_loaded = 0;
this.pos = 0;
this.interval = -1;
this.prev = -1;
this.status = 1; /* Status codes:
0: history
1: loading
2: playing
3: paused
4: playhead scrub */
var history = document.getElementById('pagehistory');
var items = history.getElementsByTagName('li');
// Cache the current history view
var bodyContent = document.getElementById('bodyContent');
this.history_content = this.mediawiki_content(bodyContent.innerHTML);
// Check whether to animate over all article revisions
if (document.getElementById('animate_range_all').checked) {
// Check to see if the current history page already contains every
revision
var last_row = items[items.length - 1];
var last_links = last_row.getElementsByTagName('a');
var first_row = items[0];
var first_links = first_row.getElementsByTagName('a');
// The first and last list items each lack a 'last' and 'cur' link,
respectively
if (last_links[1].firstChild.nodeValue == 'last' ||
first_links[0].firstChild.nodeValue == 'cur') {
this.get_full_history( );
return;
} else {
first_row.getElementsByTagName('input')[1].checked = true;
last_row.getElementsByTagName('input')[0].checked = true;
}
}
this.parse_history( );
this.setup_markup( );
this.start_loading( );
}
Animation.prototype.get_full_history = function( ) {
// Disable the animate buttons while we load
var button1 = document.getElementById('animate_button1');
button1.value = 'Loading…';
button1.setAttribute('disabled', 'disabled');
var button2 = document.getElementById('animate_button2');
button2.value = 'Loading…';
button2.setAttribute('disabled', 'disabled');
// Load in the full history
var request = new XMLHttpRequest( );
request.open('GET', this.base_url +
'&action=history&limit=5000&offset=0', true);
request.onreadystatechange = function( ) {
if (request.readyState == 4) {
var content = animate.mediawiki_content(request.responseText);
document.getElementById('bodyContent').innerHTML = content;
var history = document.getElementById('pagehistory');
var items = history.getElementsByTagName('li');
var inputs = items[items.length - 1].
getElementsByTagName('input');
inputs[0].checked = true;
animate.parse_history( );
animate.setup_markup( );
animate.start_loading( );
}
}
request.send(null);
}
Animation.prototype.parse_history = function( ) {
var history = document.getElementById('pagehistory');
var items = history.getElementsByTagName('li');
var skip_minor = document.getElementById('animate_skip_minor').checked;
var found_start = false;
for (var i = 0; i < items.length; i++) {
var radios = items[i].getElementsByTagName('input');
var skip = false;
// Skip this revision if it's been labeled 'minor'
if (skip_minor) {
var spans = items[i].getElementsByTagName('span');
for (var j = 0; j < spans.length; j++) {
if (spans[j].className == 'minor') {
skip = true;
}
}
}
if (radios[1] && radios[1].checked) {
var links = items[i].getElementsByTagName('a');
if (links[0].firstChild.nodeValue != 'cur' && !skip) {
this.urls.unshift(links[1].getAttribute('href'));
this.info.unshift(this.parse_info(items[i], 1));
} else if (!skip) {
this.urls.unshift(links[2].getAttribute('href'));
this.info.unshift(this.parse_info(items[i], 2));
}
found_start = true;
} else if (radios[0] && radios[0].checked) {
var links = items[i].getElementsByTagName('a');
if (links[1].firstChild.nodeValue != 'last' && !skip) {
this.urls.unshift(links[1].getAttribute('href'));
this.info.unshift(this.parse_info(items[i], 1));
} else if (!skip) {
this.urls.unshift(links[2].getAttribute('href'));
this.info.unshift(this.parse_info(items[i], 2));
}
break;
} else if (found_start && !skip) {
var links = items[i].getElementsByTagName('a');
this.urls.unshift(links[2].getAttribute('href'));
this.info.unshift(this.parse_info(items[i], 2));
}
}
}
Animation.prototype.setup_markup = function( ) {
this.add_nav_link( );
var content = '<div id="animate_main">' +
'<div id="animate_controls">' +
'<input type="button" value="Pause" class="historysubmit" id="animate_
button"/> ' +
'<div id="animate_scrubber">' +
'<div id="animate_load_progress"></div>' +
'<div id="animate_playhead"></div></div>' +
'<div id="animate_status">Loading…</div>' +
'<br style="clear: both;"/>' +
'<div id="animate_info"></div>' +
'</div></div>';
document.getElementById('bodyContent').innerHTML = content;
document.getElementById('bodyContent').style.height = '250px';
this.content = 1;
var playhead = document.getElementById('animate_playhead');
playhead.addEventListener('mousedown', function(e) {
animate.playhead(e); e.preventDefault( );
}, true);
var button = document.getElementById('animate_button');
button.addEventListener('click', function( ) {
animate.button( );
}, true);
var body = document.getElementsByTagName('body').item(0);
body.addEventListener('mouseup', function(e) {
animate.mouseup(e);
}, true);
body.addEventListener('mousemove', function(e) {
animate.mousemove(e);
}, true);
var top = 0;
var curr = document.getElementById('animate_main');
while (curr.offsetParent) {
top += curr.offsetTop;
curr = curr.offsetParent;
}
this.scroll_origin = top;
window.setInterval(function( ) { animate.check_scroll( ); }, 50);
}
Animation.prototype.add_nav_link = function( ) {
var history_nav = document.getElementById('ca-history');
history_nav.className = '';
history_nav.getElementsByTagName('a').item(0).addEventListener(
'click', function(event) {
animate.status = 0;
document.getElementById('bodyContent').innerHTML = animate.history_
content;
histrowinit( );
this.parentNode.className = 'selected';
var animate_nav = document.getElementById('animate_nav');
this.parentNode.parentNode.removeChild(animate_nav);
document.getElementById('animate_button1').addEventListener(
'click', function( ) { animate.start( ); }, true);
document.getElementById('animate_button2').addEventListener(
'click', function( ) { animate.start( ); }, true);
event.preventDefault( );
}, true);
var animate_nav = document.createElement('li');
var link = animate_nav.appendChild(document.createElement('a'));
animate_nav.id = 'animate_nav';
link.appendChild(document.createTextNode('animate'));
link.setAttribute('href', '#');
link.addEventListener('click', function(event) {
event.preventDefault( );
}, true);
animate_nav.className = 'selected';
history_nav.parentNode.appendChild(animate_nav);
}
Animation.prototype.start_loading = function( ) {
var url = 'http://' + this.hostname + this.urls[0];
var request = new XMLHttpRequest( );
request.open('GET', url, true);
request.onreadystatechange = function( ) {
if (request.readyState == 4) {
animate.loaded(request);
}
}
request.send(null);
}
Animation.prototype.parse_info = function(item, l) {
var info = '';
var links = item.getElementsByTagName('a');
if (document.getElementById('animate_info_date').checked) {
var href = links.item(l).getAttribute('href');
var text = links.item(l).firstChild.nodeValue;
info += '<a href="' + href + '">' + text + '</a> ';
}
if (document.getElementById('animate_info_author').checked) {
var href = links.item(l + 1).getAttribute('href');
var text = links.item(l + 1).firstChild.nodeValue;
info += 'by <a href="' + href + '">' + text + '</a> ';
}
if (document.getElementById('animate_info_summary').checked) {
var em = item.getElementsByTagName('em');
if (em.length == 1) {
info += ' ' + em.item(0).innerHTML;
}
}
info = '<span class="text">' + info + '</span>';
return info;
}
Animation.prototype.loaded = function(details) {
var content = this.mediawiki_content(details.responseText);
this.pages[this.pages.length] = content;
if (this.num_loaded > 0 && document.getElementById('animate_diff_yes').
checked) {
content = diffString(this.pages[this.num_loaded - 1], content);
}
var frame = document.createElement('div');
var main = document.getElementById('animate_main');
var controls = document.getElementById('animate_controls');
main.insertBefore(frame, controls);
frame.innerHTML = content;
frame.className = 'content';
frame.setAttribute('id', 'frame' + this.num_loaded);
var load_progress = document.getElementById('animate_load_progress');
load_progress.style.width = 5 + (395 * this.num_loaded / (this.urls.
length - 1)) + 'px';
load_progress.style.visibility = 'visible';
if (this.num_loaded > 0 && document.getElementById('animate_diff_yes').
checked) {
var activity = 0;
// Check for added content
var b_list = frame.getElementsByTagName('b');
for (var i = 0; i < b_list.length; i++) {
if (b_list[i].className == 'diff') {
activity++;
}
}
// Check for deleted content
var s_list = frame.getElementsByTagName('s');
for (var i = 0; i < s_list.length; i++) {
if (s_list[i].className == 'diff') {
activity++;
}
}
var id = this.activity.length;
this.activity[id] = activity;
var a = document.createElement('div');
document.getElementById('animate_load_progress').appendChild(a);
a.setAttribute('id', 'animate_activity' + id);
a.style.position = 'absolute';
a.style.left = (395 * (this.num_loaded - 1) / (this.urls.length -
1)) + 'px';
a.style.width = (395 / (this.urls.length - 1)) + 'px';
if (this.num_loaded == 1) {
a.style.width = parseFloat(a.style.width) + 5 + 'px';
} else {
a.style.left = parseFloat(a.style.left) + 5 + 'px';
}
a.style.height = '9px';
a.style.top = '0px';
if (this.activity_max == 0) {
if (activity == 0) {
var digit = 225;
} else {
this.activity_max = activity;
var digit = 153;
}
a.style.background = 'rgb(' + digit + ',' + digit + ',' + digit
+ ')';
} else {
if (activity > this.activity_max) {
this.activity_max = activity;
this.normalize_activity( );
} else {
var digit = parseInt(225 - 72 * activity / this.activity_
max);
a.style.background = 'rgb(' + digit + ',' + digit + ',' +
digit + ')';
}
}
}
if (this.status == 1) {
this.swap_content(this.num_loaded);
var playhead = document.getElementById('animate_playhead');
playhead.style.left = 1 + (390 * this.num_loaded / (this.urls.length
- 1)) + 'px';
this.set_info(this.num_loaded);
this.pos = this.num_loaded;
}
this.num_loaded++;
if (this.num_loaded < this.urls.length &&
this.status != 0) {
var url = 'http://' + this.hostname + this.urls[this.num_loaded];
var request = new XMLHttpRequest( );
request.open('GET', url, true);
var _this = this;
request.onreadystatechange = function( ) {
if (request.readyState == 4) {
_this.loaded(request);
}
}
request.send(null);
} else if (this.num_loaded == this.urls.length) {
this.pause( );
}
}
Animation.prototype.normalize_activity = function( ) {
for (var i = 0; i < this.activity.length; i++) {
var a = document.getElementById('animate_activity' + i);
var digit = parseInt(225 - 72 * this.activity[i] / this.activity_
max);
a.style.background = 'rgb(' + digit + ',' + digit + ',' + digit +
')';
}
}
Animation.prototype.button = function( ) {
if (this.status == 3) {
this.play( );
} else {
this.pause( );
}
}
Animation.prototype.play = function( ) {
this.status = 2;
var button = document.getElementById('animate_button').value = 'Pause';
if (this.pos + 1 == this.urls.length) {
var playhead = document.getElementById('animate_playhead');
playhead.style.left = '1px';
this.pos = 0;
}
this.show_frame(this.pos);
var delay = Math.round(parseFloat(document.getElementById('animate_
delay').value) * 1000);
this.interval = window.setInterval(function( ) { animate.show_frame( );
}, delay);
}
Animation.prototype.pause = function( ) {
this.status = 3;
var button = document.getElementById('animate_button').value = 'Play';
if (this.interval != -1) {
clearInterval(this.interval);
}
}
Animation.prototype.show_frame = function(num) {
if (this.status == 0 || this.status == 3) {
return;
}
// If not scrubbing
if (this.status != 4) {
var num = this.pos;
var playhead = document.getElementById('animate_playhead');
playhead.style.left = 1 + (390 * num / (this.urls.length - 1)) +
'px';
}
this.swap_content(num);
this.set_info(num);
if (this.status == 2) {
if (this.pos + 1 >= this.pages.length) {
this.pause( );
} else {
this.pos++;
}
}
}
Animation.prototype.set_info = function(num) {
document.getElementById('animate_info').innerHTML = this.info[num];
var prev = (num > 0) ? '<a href="#" onclick="animate.prev_frame( );
return false;" accesskey="p">←</a> ' : '← ';
var frame = (num + 1) + ' / ' + this.urls.length;
var next = (num < this.urls.length - 1) ? ' <a href="#"
onclick="animate.next_frame( ); return false;" accesskey="nw">→ </a>' : '
→';
document.getElementById('animate_status').innerHTML = '<span
class="text">' + prev + frame + next + '</span>';
}
Animation.prototype.playhead = function(e) {
this.status = 4;
return false;
}
Animation.prototype.prev_frame = function( ) {
this.pos--;
this.show_frame(this.pos);
this.status = 3;
}
Animation.prototype.next_frame = function( ) {
if (this.pos + 1 < this.num_loaded) {
this.pos++;
this.show_frame(this.pos);
this.status = 3;
}
}
Animation.prototype.mousemove = function(e) {
// Make sure the user has clicked on the playhead
if (animate.status != 4) {
return;
}
var scrubber = document.getElementById('animate_scrubber');
var left = 0;
var curr = scrubber;
while (curr.offsetParent) {
left += curr.offsetLeft;
curr = curr.offsetParent;
}
var playhead = document.getElementById('animate_playhead');
var x = e.pageX - left - 5;
if (x > 391) {
x = 391;
} else if (x < 1) {
x = 1;
}
var load_progress = document.getElementById('animate_load_progress');
if (x > parseInt(load_progress.style.width - 5)) {
x = parseInt(load_progress.style.width - 5);
}
playhead.style.left = x + 'px';
var snap = Math.floor((x - 1) * (animate.urls.length - 1) / 390);
if (snap != animate.pos) {
animate.pos = snap;
animate.show_frame(snap);
}
}
Animation.prototype.mouseup = function(e) {
if (animate.status != 4) {
return;
}
var scrubber = document.getElementById('animate_scrubber');
var left = 0;
var curr = scrubber;
while (curr.offsetParent) {
left += curr.offsetLeft;
curr = curr.offsetParent;
}
var playhead = document.getElementById('animate_playhead');
var x = e.pageX - left - 5;
if (x > 391) {
x = 391;
} else if (x < 1) {
x = 1;
}
var load_progress = document.getElementById('animate_load_progress');
if (x > parseInt(load_progress.style.width)) {
x = parseInt(load_progress.style.width);
}
var snap = Math.floor((x - 1) * (animate.urls.length - 1) / 390);
if (snap != animate.pos) {
animate.pos = snap;
animate.show_frame(snap);
}
animate.status = 3;
}
Animation.prototype.option = function(input) {
}
Animation.prototype.swap_content = function(num) {
var frame = document.getElementById('frame' + num);
frame.style.display = 'block';
var height = parseInt(frame.offsetHeight) + 60;
document.getElementById('bodyContent').style.height = height + 'px';
if (this.prev != -1) {
var prev = document.getElementById('frame' + this.prev);
prev.style.display = 'none';
}
this.prev = num;
}
Animation.prototype.mediawiki_content = function(text) {
text = '' + text;
var start = text.indexOf('<!-- start content -->');
var end = text.indexOf('<!-- end content -->');
return text.substr(start, end - start);
}
Animation.prototype.check_scroll = function( ) {
var controls = document.getElementById('animate_controls');
if (self.pageYOffset > this.scroll_origin) {
controls.style.top = (self.pageYOffset - this.scroll_origin) + 'px';
} else {
controls.style.top = 0;
}
}
var animate = new Animation( );
// JavaScript diff code thanks to John Resig (http://ejohn.org)
// http://ejohn.org/files/jsdiff.js
function diffString( o, n ) {
var out = diff( o.split(/\s+/), n.split(/\s+/) );
var str = "";
for ( var i = 0; i < out.n.length - 1; i++ ) {
if ( out.n[i].text == null ) {
if ( out.n[i].indexOf('"') == -1 && out.n[i].indexOf('<') == -1
&& out.n[i].indexOf('=') == -1 ) {
str += "<b style='background:#E6FFE6;' class='diff'> " +
out.n[i] +"</b>";
} else {
str += " " + out.n[i];
}
} else {
var pre = "";
if ( out.n[i].text.indexOf('"') == -1 && out.n[i].text.
indexOf('<') == -1 && out.n[i].text.indexOf('=') == -1 ) {
var n = out.n[i].row + 1;
while ( n < out.o.length && out.o[n].text == null ) {
if ( out.o[n].indexOf('"') == -1 && out.o[n].
indexOf('<') == -1 && out.o[n].indexOf(':') == -1 && out.o[n].indexOf(';')
== -1 && out.o[n].indexOf('=') == -1 ) {
pre += " <s style='background:#FFE6E6;'
class='diff'>" + out.o[n] +" </s>";
}
n++;
}
}
str += " " + out.n[i].text + pre;
}
}
return str;
}
function diff( o, n ) {
var ns = new Array( );
var os = new Array( );
for ( var i = 0; i < n.length; i++ ) {
if ( ns[ n[i] ] == null ) {
ns[ n[i] ] = { rows: new Array( ), o: null };
}
if (ns[n[i]].rows) {
ns[ n[i] ].rows.push( i );
}
}
for ( var i = 0; i < o.length; i++ ) {
if ( os[ o[i] ] == null ) {
os[ o[i] ] = { rows: new Array( ), n: null };
}
if (os[o[i]].rows) {
os[ o[i] ].rows.push( i );
}
}
for ( var i in ns ) {
if ( ns[i].rows.length == 1 && typeof(os[i]) != "undefined" &&
os[i].rows.length == 1 ) {
n[ ns[i].rows[0] ] = { text: n[ ns[i].rows[0] ], row: os[i].
rows[0] };
o[ os[i].rows[0] ] = { text: o[ os[i].rows[0] ], row: ns[i].
rows[0] };
}
}
for ( var i = 0; i < n.length - 1; i++ ) {
if ( n[i].text != null && n[i+1].text == null && o[ n[i].row + 1 ].
text == null &&
n[i+1] == o[ n[i].row + 1 ] ) {
n[i+1] = { text: n[i+1], row: n[i].row + 1 };
o[n[i].row+1] = { text: o[n[i].row+1], row: i + 1 };
}
}
for ( var i = n.length - 1; i > 0; i-- ) {
if ( n[i].text != null && n[i-1].text == null && o[ n[i].row - 1 ].
text == null &&
n[i-1] == o[ n[i].row - 1 ] ) {
n[i-1] = { text: n[i-1], row: n[i].row - 1 };
o[n[i].row-1] = { text: o[n[i].row-1], row: i - 1 };
}
}
return { o: o, n: n };
}
|
O'Reilly Home | Privacy Policy

© 2007 O'Reilly Media, Inc.
Website:
| Customer Service:
| Book issues:
All trademarks and registered trademarks appearing on oreilly.com are the property of their respective owners.
|
|
|