User:Rich Smith/iglooTest.js

// INCLUDES function iglooViewable {

}; _iglooViewable = new iglooViewable;

// iglooMain development copy // Alex Barley // base code test only // expected jQuery 1.7.*, jin 1.04a+, Mediawiki 1.19

/*	CLASSES ========================== */ // Class iglooUserSettings /*	** iglooUserSettings is the class that holds the settings ** for a particular user. The settings for a session can ** be stored in JSON format for a particular user and then ** parsed into the program to provide saving and loading. **	** If no settings are loaded, the defauls specified in the ** class itself will simply apply. **	** It is written here in simplified object form to ensure ** it can be parsed as expected. */ var iglooConfiguration = { api: mw.config.get('wgServer') + mw.config.get('wgScriptPath') + '/api.php',

defaultContentScore: 20 }

var iglooUserSettings = { // Modules // Ticker // Requests limitRequests: 5,

// Misc maxContentSize: 50 };

function getp (obj) { if (Object.getPrototypeOf) { return Object.getPrototypeOf(obj); } else if (obj.__proto__) { return obj.__proto__; } else return false; }

function iglooQueue { var internal = [];

this.push = function (item) { internal.push(item); return this; }

this.pop = function { if (typeof arguments[0] === 'number') { var n = arguments[0]; if (n > internal.length) n = internal.length; return internal.splice(0, n); } else return internal.shift; }

this.get = function { if (internal[0]) return internal[0]; else return false; } }

// Class iglooMain /*	** iglooMain is the running class for igloo. It handles: ** - Building the core interface and starting daemons ** - Loading external modules ** - Hooking modules into the correct place */ function iglooMain { var me = this; // Define state this.canvas = null; // igloo exposes its primary canvas to modules for use. this.toolPane = null; // igloo exposes its primary toolpane to modules for use. this.content = null; // igloo exposes the content panel for convenience. this.diffContainer = null; // igloo exposes the diff container for convenience. this.ticker = null; // igloo exposes its ticker panel for convenience.

this.currentView = null; this.modules = {};

this.launch = function { if (mw.config.get('wgPageName') !== 'User:Ale_jrb/igDev') return; this.loadModules; this.buildInterface;

this.currentView = new iglooView; this.recentChanges = new iglooRecentChanges; this.contentManager = new iglooContentManager; this.recentChanges.setTickTime(2000); };

this.buildInterface = function { try { // Create drawing canvas. this.canvas = new jin.Canvas; this.canvas.setFullScreen(true); // Create base splitter. var mainPanel = new jin.SplitterPanel; mainPanel.setPosition(0, 0); mainPanel.setSize(0, 0); mainPanel.setInitialDrag(260); mainPanel.setColour(jin.Colour.DARK_GREY); mainPanel.left.setColour(jin.Colour.LIGHT_GREY); mainPanel.right.setColour(jin.Colour.LIGHT_GREY); // Expose recent changes panel. this.ticker = mainPanel.left; // Create toolbar pane. this.toolPane = new jin.Panel; this.toolPane.setPosition(0, 0); this.toolPane.setSize(0, 100); this.toolPane.setColour(jin.Colour.GREY); // Create toolbar border. var toolBorder = new jin.Panel; toolBorder.setPosition(0, 100); toolBorder.setSize(0, 1); toolBorder.setColour(jin.Colour.DARK_GREY); // Create content panel. this.content = new jin.Panel; this.content.setPosition(0, 101); this.content.setSize(0, 0); this.content.setColour(jin.Colour.WHITE); // Create diff container. this.diffContainer = new jin.Panel; this.diffContainer.setPosition(0, 0); this.diffContainer.setSize(0, 0); this.diffContainer.setColour(jin.Colour.WHITE); // Combine interface elements. this.content.add(this.diffContainer); mainPanel.right.add(this.toolPane); mainPanel.right.add(toolBorder); mainPanel.right.add(this.content); this.canvas.add(mainPanel); // Do initial render. this.canvas.render(jin.getDocument);

this.fireEvent('core','interface-rendered', true); } catch (e) { jin.handleException(e); }	};

/*		UI ====================== */	this.getCurrentView = function { return this.currentView; };

/*		EVENTS ================== */	this.announce = function (moduleName) { if (!this.modules[moduleName]) this.modules[moduleName] = {}; this.modules[moduleName]['exists'] = true; this.modules[moduleName]['ready'] = true; };

this.isModuleReady = function (moduleName) { if (!this.modules[moduleName]) return false; return this.modules[moduleName]['ready']; };

this.hookEvent = function (moduleName, hookName, func) { if (hookName === 'exists' || hookName === 'ready') return 1;

if (!this.modules[moduleName]) { this.modules[moduleName] = {}; this.modules[moduleName]['exists'] = true; this.modules[moduleName]['ready'] = false; }

if (!this.modules[moduleName][hookName]) { this.modules[moduleName][hookName] = [func]; } else { this.modules[moduleName][hookName].push(func); }

return 0; };

this.unhookEvent = function (moduleName, hookName, func) { if (this.modules[moduleName]) { if (this.modules[moduleName][hookName]) { for (i = 0; i < this.modules[moduleName][hookName].length; i++) { if (this.modules[moduleName][hookName][i] === func) this.modules[moduleName][hookName][i] = null; }			}		}	};

this.fireEvent = function (moduleName, hookName, data) { if (this.modules[moduleName]) { if (this.modules[moduleName][hookName]) { for (i = 0; i < this.modules[moduleName][hookName].length; i++) { if (this.modules[moduleName][hookName][i] !== null) this.modules[moduleName][hookName][i](data); }			}		}	};

this.loadModules = function { // do nothing

this.fireEvent('core', 'modules-loaded', true); }; };

// Class iglooContentManager /*	** iglooContentManager keeps track of iglooPage items ** that are loaded by the recent changes ticker or at ** the user request. Because igloo cannot store all ** changes for the duration of the program, it must ** decide when to discard the page item to save memory. ** The content manager uses a relevance score to track ** items. This score is created when the manager first ** sees the page and decreases when the content manager ** sees activity. When an item reaches 0, it is open ** to be discarded. If an item sees further actions, its ** score can be refreshed, preventing it from being ** discarded for longer. */ function iglooContentManager { this.contentSize = 0; this.discardable = 0; this.content = {};

this.add = function (page) { this.decrementScores; this.contentSize++; this.content[page.info.pageTitle] = { exists: true, page: page, hold: true, timeAdded: new Date, timeTouched: new Date, score: iglooConfiguration.defaultContentScore }

console.log("IGLOO: Added a page to the content manager. Size: " + this.contentSize); this.gc;

return this.content[page.info.pageTitle]; }

this.getPage = function (title) { if (this.content[title]) { return this.content[title].page; } else { return false; }	}

this.decrementScores = function { var s = "IGLOO: CSCORE: "; for (i in this.content) { if (this.content[i].score > 0) { s += this.content[i].score + ", "; if (--this.content[i].score === 0) { console.log("IGLOO: an item reached a score of 0 and is ready for discard!"); this.discardable++; }			}		}		console.log(s); }

this.gc = function { console.log("IGLOO: Running GC"); if (this.discardable === 0) return; if (this.contentSize > iglooUserSettings.maxContentSize) { console.log("IGLOO: GC removing items to fit limit (" + this.contentSize + "/" + iglooUserSettings.maxContentSize + ")") var j = 0, lastZeroScore = null, gcVal = 0.3, gcStep = 0.05; for (i in this.content) { if (this.content[i].score !== 0 || this.content[i].isRecent !== false || this.content[i].page.displaying !== false) { j++; gcVal += gcStep; continue; } else { lastZeroScore = i;				}

if (j === this.contentSize - 1) { if (lastZeroScore !== null) { console.log("IGLOO: failed to randomly select item, discarding the last one seen"); this.content[lastZeroScore] = undefined; this.contentSize--; this.discardable--; break; }				}

if (this.content[i].score === 0 					&& this.content[i].isRecent === false 					&& Math.random < gcVal					&& this.content[i].page.displaying === false) {

console.log("IGLOO: selected an item suitable for discard, discarding"); this.content[i] = undefined; this.contentSize--; this.discardable--; break; } else { j++; gcVal += gcStep; }			}		}	} }

// Class iglooRecentChanges /*	** iglooRecentChanges is the ticker class for igloo. ** With no modules loaded, igloo simply acts as a recent ** changes viewer. This class maintains the list of 	** iglooPage elements that represent wiki pages that have ** recently changed. Each pages contains many diffs. Once ** created, this class will tick in the background and ** update itself. It can be queried and then rendered at ** any point. */ function iglooRecentChanges { var me = this; console.log ( 'IGLOO: generated RC ticker' ); this.tick = null; this.loadUrl = iglooConfiguration.api; this.tickTime = 4000; this.recentChanges = [];

// Methods this.setTickTime = function (newTime) { this.tickTime = newTime; clearInterval(this.tick); this.tick = setInterval(function { me.update.apply(me); }, this.tickTime); };	// Constructor this.renderResult = document.createElement('ul'); // this is the output panel $(this.renderResult).css({		'position': 'absolute',		'top': '0px',		'left': '0px',		'padding': '0px',		'margin': '0px',		'width': '100%',		'height': '100%',		'list-style': 'none inherit none',		'overflow': 'auto'	}); $(me.renderResult).on ({		mouseover: function { $(this).css('backgroundColor', '#999999'); },		mouseout: function  { $(this).css('backgroundColor', jin.Colour.LIGHT_GREY); },		click: function  { me.show.apply(me, [$(this).data('elId')]) ; }	}, 'li'); igloo.ticker.panel.appendChild(this.renderResult); };

iglooRecentChanges.prototype.update = function { var me = this; (new iglooRequest({ url: me.loadUrl, data: { format: 'json', action: 'query', list: 'recentchanges' }, dataType: 'json', context: me, success: function (data) { me.loadChanges.apply(me, [data]); }	}, 0, false)).run; };

iglooRecentChanges.prototype.loadChanges = function (changeSet) { data = changeSet.query.recentchanges; // For each change, add it to the changeset. var l = data.length; for (var i = 0; i < l; i++) { // Check if we already have information about this page. var l2 = this.recentChanges.length, exists = false; for (var j = 0; j < l2; j++) { if (data[i].title === this.recentChanges[j]) { var p = igloo.contentManager.getPage(data[i].title); p.page.addRevision(new iglooRevision(data[i])); p.hold = true; exists = true; break; }		}		if (!exists) { var p = new iglooPage(new iglooRevision(data[i])); igloo.contentManager.add(p); this.recentChanges.push(p); }	}	this.recentChanges.sort(function (a, b) { return b.lastRevision - a.lastRevision; }); // Truncate the recent changes list to the correct length if (this.recentChanges.length > 30) { // Objects that are being removed from the recent changes list are freed in the // content manager for discard. for (var i = 30; i < this.recentChanges.length; i++) { console.log("IGLOO: Status change. " + this.recentChanges[i].title + " is no longer hold") var p = igloo.contentManager.getPage(this.recentChanges[i].title); p.hold = false; }		this.recentChanges = this.recentChanges.slice(0, 30); }	// Render the result this.render; };

// ask a diff to show its changes iglooRecentChanges.prototype.show = function (elementId) { this.recentChanges[elementId].display; return this; };

iglooRecentChanges.prototype.render = function { this.renderResult.innerHTML = ''; for (var i = 0; i < this.recentChanges.length; i++) { // Create each element var t = document.createElement('li'); // Styling $(t).css ({			'padding': '0px',			'borderBottom': '1px solid #000000',			'list-style-type': 'none',			'list-style-image': 'none',			'marker-offset': '0px',			'margin': '0px'		}); // Finish if (this.recentChanges[i].isNewPage) { t.innerHTML = " N " + this.recentChanges[i].pageTitle; } else { t.innerHTML = this.recentChanges[i].pageTitle; }		$(t).data("elId", i); this.renderResult.appendChild(t); }	console.log("Rendered " + i + " recent changes."); return this; };

// Class iglooView // iglooView represents a content view. There could be // multiple views, each showing their own bit of content. // iglooView can support viewing anything that inherits // from iglooViewable.

function iglooView { var me = this;

// State this.displaying = null; this.changedSinceDisplay = false;

// Hook to relevant events igloo.hookEvent('core', 'displayed-page-changed', function (data) {		if (me.displaying) {			if (data.page === me.displaying.page) {				this.changedSinceDisplay = true;				this.displaying = data;				this.displaying.show;			}		}	}); };

iglooView.prototype.display = function (revision) { // If a revision is being displayed, set the displaying // flag for the page to false. if (this.displaying) { this.displaying.page.displaying = false; this.displaying.page.changedSinceDisplay = false; }

// Set the new revision into the page, then show it. this.displaying = revision; this.displaying.show; }

// Class iglooPage

function iglooPage { var me = this; // Details this.info = { pageTitle: '', namespace: 0 }	this.lastRevision = 0; this.revisions = []; // State this.displaying = false; // currently displaying this.changedSinceDisplay = false; // the data of this page has changed since it was first displayed this.isNewPage = false; // whether this page currently only contains the page creation this.isRecent = false; // Methods // Revisions can be added to a page either by a history lookup, or // by the recent changes ticker. The 'diff' attached to a revision // is always the diff of this revision with the previous one, though // other diffs can be loaded as requested (as can the particular 	// content at any particular revision). // Constructor if (arguments[0]) { this.pageTitle = arguments[0].page; this.addRevision(arguments[0]); } };

iglooPage.prototype.addRevision = function (newRev) { // Check if this is a duplicate revision. for (var i = 0; i < this.revisions; i++) { if (newRev.revId === this.revisions[i].revId) return; }

if (this.isNewPage) { this.isNewPage = false; } else if (newRev.type === 'new') { this.isNewPage = true; }

newRev.page = this; this.revisions.push(newRev); this.revisions.sort(function (a, b) { return a.revId - b.revId; }); if (newRev.revId > this.lastRevision) this.lastRevision = newRev.revId; if (this.displaying) { alert('update'); igloo.fireEvent('core', 'displayed-page-changed', newRev); this.changedSinceDisplay = true; } };

iglooPage.prototype.display = function { // Calling display on a page will invoke the display // method for the current view, and pass it the relevant // revision object. var currentView = igloo.getCurrentView;

if (arguments[0]) { if (this.revisions[arguments[0]]) { currentView.display(this.revisions[arguments[0]]); } else { currentView.display(this.revisions.iglast); }	} else { currentView.display(this.revisions.iglast); }	this.displaying = true; this.changedSinceDisplay = false; };

// Class iglooRevision /*	** iglooRevision represents a revision and associated diff ** on the wiki. It may simply represent the metadata of a	** change, or it may represent the change in full. */ function iglooRevision { var me = this; // Content detail this.user = ''; // the user who made this revision this.page = ''; // the page title that this revision belongs to	this.namespace = 0; this.revId = 0; // the ID of this revision (the diff is between this and oldId) this.oldId = 0; // the ID of the revision from which this was created this.type = 'edit'; this.revisionContent = ''; // the content of the revision this.diffContent = ''; // the HTML content of the diff this.revisionRequest = null; // the content request for this revision. this.diffRequest = null; // the diff request for this revision this.revisionLoaded = false; // there is content stored for this revision this.diffLoaded = false; // there is content stored for this diff this.displayRequest = false; // diff should be displayed when its content next changes this.page = null; // the iglooPage object to which this revision belongs // Constructor if (arguments[0]) { this.setMetaData(arguments[0]); } };

iglooRevision.prototype.setMetaData = function (newData) { this.user = newData.user; this.page = newData.title; this.namespace = newData.ns; this.oldId = newData.old_revid; this.revId = newData.revid; this.type = newData.type; };

iglooRevision.prototype.loadRevision = function (newData) { var me = this;

if (this.revisionRequest === null) { this.revisionRequest = new iglooRequest({			url: iglooConfiguration.api,			data: { format: 'json', action: 'query', prop: 'revisions', revids: '' + me.revId, rvprop: 'content', rvparse: 'true' },			dataType: 'json',			context: me,			success: function (data) {				for (i in data.query.pages) {					this.revisionContent = data.query.pages[i].revisions[0]['*'];				}				this.revisionLoaded = true;				if (this.displayRequest === 'revision') this.display('revision');				this.revisionRequest = null;			}		}, 0, true); this.revisionRequest.run; } };

iglooRevision.prototype.loadDiff = function { var me = this;

if (this.diffRequest === null) { console.log('Attempted to show a diff, but we had no data so has to load it.') this.diffRequest = new iglooRequest({			url: iglooConfiguration.api,			data: { format: 'json', action: 'compare', fromrev:  + me.oldId, torev:  + me.revId },			dataType: 'json',			context: me,			success: function (data) {				this.diffContent = data.compare['*'];				this.diffLoaded = true;				if (this.displayRequest === 'diff') this.display('diff');				this.diffRequest = null;			}		}, 0, true); this.diffRequest.run; } };

iglooRevision.prototype.display = function { // Determine what should be displayed. if (!arguments[0]) { var displayWhat = 'diff'; } else { var displayWhat = arguments[0]; }

// If this was fired as a result of a display request, clear the flag. if (this.displayRequest) this.displayRequest = false; // Mark as displaying, and fire the displaying event. this.displaying = true; igloo.fireEvent('core', 'displaying-change', this); // Create display element. if (displayWhat === 'revision' || this.type === 'new') { var div = document.createElement('div'); div.innerHTML = this.revisionContent; // Style display element. $(div).find('a').each(function {			$(this).prop('target', '_blank');		}); // Clear current display. $(igloo.diffContainer.panel).find('*').remove; // Append new content. igloo.diffContainer.panel.appendChild(div); } else if (displayWhat === 'diff') { var table = document.createElement('table');

table.innerHTML = '   ' + this.diffContent; // Style display element. // TODO $(table).css({ 'width': '100%', 'overflow': 'auto' }); $(table).find('#iglooDiffCol1').css({ 'width': '50%' }); $(table).find('#iglooDiffCol2').css({ 'width': '50%' });

$(table).find('.diff-empty').css(''); $(table).find('.diff-addedline').css({ 'background-color': '#ccffcc' }); $(table).find('.diff-marker').css({ 'text-align': 'right' }); $(table).find('.diff-lineno').css({ 'font-weight': 'bold' }); $(table).find('.diff-deletedline').css({ 'background-color': '#ffffaa' }); $(table).find('.diff-context').css({ 'background-color': '#eeeeee' }); $(table).find('.diffchange').css({ 'color': 'red' }); // Clear current display. $(igloo.diffContainer.panel).find('*').remove; // Append new content. igloo.diffContainer.panel.appendChild(table); } };

iglooRevision.prototype.show = function {

// Determine what to show. if (!arguments[0]) { var displayWhat = 'diff'; } else { var displayWhat = arguments[0]; }

if (displayWhat === 'diff' && this.type === 'edit') { console.log('IGLOO: diff display requested, page: ' + this.page.pageTitle); if ((!this.diffLoaded) && (!this.diffRequest)) { this.displayRequest = 'diff'; this.loadDiff; } else { this.display('diff'); }	} else { console.log('IGLOO: revision display requested, page: ' + this.page.pageTitle); if ((!this.revisionLoaded) && (!this.revisionRequest)) { this.displayRequest = 'revision'; this.loadRevision; } else { this.display('revision'); }	} };

function iglooRequest (request, priority, important) { var me = this; // Statics getp(this).requests = []; getp(this).queuedRequests = 0; getp(this).runningRequests = 0;

// Constructor this.request = request; this.priority = priority; this.important = important; this.requestItem = null; };

iglooRequest.prototype.run = function { var me = this;

if (this.important === true) { // If important, execute immediately. this.requestItem = $.ajax(this.request); return this.requestItem; } else { // If not important, attach our callback to its complete function. if (this.request.complete) { var f = this.request['complete']; this.request['complete'] = function (data) { me.callback; f(data); }; } else { this.request['complete'] = function (data) { me.callback; }; }		// If we have enough requests, just run, otherwise hold. if (getp(this).runningRequests >= iglooUserSettings.limitRequests) { console.log('IGLOO: queuing a request because ' + getp(this).runningRequests + '/' + iglooUserSettings.limitRequests + ' are running'); getp(this).requests.push(this.request); getp(this).requests.sort(function (a, b) { return a.priority - b.priority; }); if (getp(this).queuedRequests > 20) { console.log('IGLOO: pruned an old request because the queue contains 20 items'); getp(this).requests = getp(this).requests.slice(1); } else { getp(this).queuedRequests++; }		} else { console.log ( 'IGLOO: running a request because ' + getp(this).runningRequests + '/' + iglooUserSettings.limitRequests + ' are running' ); getp(this).runningRequests++; this.requestItem = $.ajax(this.request); return this.requestItem; }	} };

iglooRequest.prototype.abort = function { if (this.requestItem !== null) { this.requestItem.abort; this.requestItem = null; } else { this.requestItem = null; } };

iglooRequest.prototype.callback = function { getp(this).runningRequests--; if (getp(this).queuedRequests > 0) { console.log('IGLOO: non-important request completed, running another request, remaining: ' + getp(this).queuedRequests); var request = null; while (request === null) { request = getp(this).requests.pop; getp(this).queuedRequests--; }

if (request !== undefined) { getp(this).runningRequests++; $.ajax(request); }	} else { console.log ( 'IGLOO: non-important request completed, but none remain queued to run' ); } };

/*	COMPLETE ========================== */ // MAIN if (!igloo) var igloo = new iglooMain; if (typeof jin === 'undefined') { tIgLa = function { if (typeof jin === 'undefined') { setTimeout(tIgLa, 1000); } else { igloo.launch; }	}	setTimeout(tIgLa, 1000); } else { igloo.launch; }

Array.prototype.iglast = function { return this[this.length - 1]; }

igloo.announce('core');