User:Rich Smith/iglooTest.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
![]() | Documentation for this user script can be added at User:Rich Smith/iglooTest. |
// 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 = "<strong>N</strong> " + 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 = '<tr><td id="iglooDiffCol1" colspan="2"> </td><td id="iglooDiffCol2" colspan="2"> </td></tr>' + 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');