User:Md gilbert/vte.js

// - this prevents double left braces being misinterpreted by the MediaWiki parser

// global variables, as required

var vte_sock = true;

var action =

"" +

" " +

" Sorry, your browser does not support inline SVG." +

"";

var data_api = "https://alahele.ischool.uw.edu:8997";

var vte = {

// initialize - application constructor

initialize: function() {

// Load the external libraries

var head = document.getElementsByTagName("head")[0];

var script = document.createElement('script');

script.type = 'text/javascript';

script.src = data_api + '/static/d3.min.js';

head.appendChild(script);

script = document.createElement('script');

script.type = 'text/javascript';

script.src = data_api + '/static/socket.io-1.2.0.js';

head.appendChild(script);

script = document.createElement('script');

script.type = 'text/javascript';

script.src = data_api + '/static/wiky.js';

head.appendChild(script);

//wiky.js didn't work for what we needed, trying Pilaf's InstaView

//Grabbed from https://en.wikipedia.org/wiki/User:Pilaf/instaview.js

script = document.createElement('script');

script.type = 'text/javascript';

script.src = data_api + '/static/instaview.js';

head.appendChild(script);

// fdeb requires d3, make sure it's loaded first

var t1 = setInterval(function() {

if (typeof(d3) != 'undefined') {

clearInterval(t1);

script = document.createElement('script');

script.type = 'text/javascript';

script.src = data_api + '/static/fdeb.js';

head.appendChild(script);

}

}, 100);

// Create the VTE button

var $btn = $(

"

" +

"

VTE

" +

"

"

).attr("title", "Open the Virtual Team Explorer");

// Add the button to the left of the search box

$("#p-search").before($btn);

// Define our click action

$("#p-vte").on("click", function() {

console.log("opening vte");

vte.setCookie("vte-view", "Explorer");

vte.setCookie("vte-status", "Open");

$("#vte-window").show();

});

// Preload the vte once required variables are loaded (ie, socket.io)

var t2 = setInterval(function() {

if (typeof(vte_sock.emit) !== 'undefined') {

clearInterval(t2);

vte.renderOverlay();

}

}, 100);

}, // end initialize

// renderOverlay - draws the initial vte lightbox

renderOverlay: function() {

// Emit vte load

vte_sock.emit("vte_load", {

name: mw.config.get("wgUserName"),

time: new Date(),

page: mw.config.get("wgTitle"),

namespace: mw.config.get("wgNamespaceNumber"),

});

// If the window already exists, just display it

if ( $("#vte-window").length > 0 ) {

$("#vte-window").show();

return;

}

// Otherwise, create the vte window

var $vteWindow = $(

"

" +

"

" +

"

" +

" Online users: " +

"

" +

"

" +

"

" +

"

" +

"

" +

"

" +

"

" +

"

" +

"

" +

"

" +

"

" +

"

" +

"

" +

"

" +

"

" +

"

" +

"

" +

"

"

);

// Create and style the main vte window

$vteWindow.css(s_vteWindow);

$("#content").append($vteWindow);

$("#vte-window-left").css(s_vteWindowLeft);

$("#vte-window-left-online").css(s_vteWindowLeftOnline);

$("#vte-window-left-chat").css(s_vteWindowLeftChat);

$("#vte-window-right").css(s_vteWindowRight);

$("#vte-window-right-title").css(s_vteWindowRightTitle);

$("#vte-window-right-tool").css(s_vteWindowRightTool);

$("#vte-window-right-nav").css(s_vteWindowRightNav);

$("#vte-window-right-content").css(s_vteWindowRightContent);

// Initially hide the window (we render on page load, only display if it was previously open)

$("#vte-window").css("display", "none");

// Initially hide each of the content pages (ie, loading, explorer, summary, etc)

$(".vte-page").css("display", "none");

// Populate the loading page

$("#vte-window-loading").html(

"

" +

"Loading..." +

"

"

);

$("#vte-window-loading").css(s_vteWindowLoading);

// Fill in basic vte elements

vte.populateTitle();

vte.populateChat();

// Get project and active project information

vte.getProjectData();

// If we render the overlay from a WikiProject page, preload the summary for that project

// The general flow is:

// 1) Draw the initial VTE window and request project data

// 2) Once project data is received, we have a few options:

// 2.0) If the VTE was previously open, draw it immediately with a loading window. The steps

// below will populate the proper content.

// 2.1) Whether or not we're on a project page, if the VTE was not previously open, get the

// project data, draw the project explorer, but don't show the VTE.

// 2.2) If we're on a project page and the VTE was not previously closed, draw the VTE at the

// project summary view.

// 2.3) If we're not on a project page but the VTE was previously open to a project, draw the

// VTE with the previously opened project's summary.

// 2.4) If we're not on a project page but the VTE was previously open to the project explorer,

// open the VTE to the project explorer.

// 2.0) If the VTE was previously open, draw it immediately and show the loading page.

var stat = vte.getCookie("vte-status");

var view = vte.getCookie("vte-view");

if (stat != null && stat != "Closed") {

$("#vte-window").show();

$("#vte-window-loading").show();

}

var t1 = setInterval(function() {

if (typeof($("#vte-window").data("vte-projects")) !== "undefined") {

// Projects loaded, clear the interval and compare with the current page

clearInterval(t1);

// 2.1) Populate the project explorer page, regardless of page or whether the VTE was open

vte.populateProjectSelect();

var t2 = setInterval(function() {

if (typeof($("#vte-window").data("vte-active-projects")) !== "undefined") {

clearInterval(t2);

vte.populateProjectExplorer();

}

}, 100);

// 2.2) Iterate over projects and see if we're on a project page

var index = -1;

for (var i = 0; i < $("#vte-window").data("vte-projects").result.length; i++) {

if (mw.config.get('wgTitle').replace(/ /g, "_") == $("#vte-window").data("vte-projects").result[i].p_title) {

index = i;

break;

}

}

if (mw.config.get('wgNamespaceNumber') == 4 && index != -1) {

// We've got a match. Set data and draw the summary

console.log("vte - Loading project summary");

$("#vte-window").data("vte-project", {

title: $("#vte-window").data("vte-projects").result[index].p_title,

id: $("#vte-window").data("vte-projects").result[index].p_id,

created: $("#vte-window").data("vte-projects").result[index].p_created,

members: {},

tasks: {},

});

vte.pageTransition("vte-window-summary", function() {

vte.populateNav();

vte.populateProjectSummary();

});

} else {

// 2.3) If we're not on a project page, still render that project page if the project cookie is set

var project = vte.getCookie("vte-project");

if (project) {

console.log("vte - not on a project page but vte-project cookie set: " + project.title);

$("#vte-window").data("vte-project", project);

vte.pageTransition("vte-window-summary", function() {

vte.populateNav();

vte.populateProjectSummary();

});

} else {

// 2.4) Otherwise, switch to the project explorer page

vte.pageTransition("vte-window-explorer", function() {

$(".vte-page").hide();

$("#vte-window-explorer").show();

});

}

}

} else {

// Projects aren't loaded yet, keep waiting...

}

}, 100);

},

// getProjectData - Called when the vte is rendered on page load. Requests active and all projects.

getProjectData: function() {

// First try to load the project data from Storage variables

// (see http://www.w3schools.com/html/html5_webstorage.asp)

var vte_projects = vte.getStorage("vte-projects");

var vte_active_projects = vte.getStorage("vte-active-projects");

if (vte_projects !== null && vte_active_projects !== null) {

console.log("vte - found project data in localStorage");

$("#vte-window").data("vte-projects", vte_projects);

$("#vte-window").data("vte-active-projects", vte_active_projects);

return true;

}

console.log("vte - fetching project data from API");

// Request all projects

var url = data_api + '/api/getProjects';

$.ajax({

url: url,

dataType: "json",

success: function(data, stat, xhr) {

if (data.errorstatus != "success") {

console.error("Failed to request projects: " + data.message);

return false;

}

$("#vte-window").data("vte-projects", data);

vte.setStorage("vte-projects", data, {expires: 7});

},

error: function(xhr, stat, err) {

console.error("Failed to request project data from API: " + JSON.stringify(xhr));

},

});

// Request active projects

var url = data_api + "/api/getActiveProjects?group=project|namespace&compress=project";

$.ajax({

url: url,

dataType: "json",

success: function(data, stat, xhr) {

if (data.errorstatus != "success") {

console.error("Failed to request active projects: " + data.message);

return false;

}

// Add in the ratio

for (var i in data.result) {

data.result[i].ratio = data.result[i].total_edits / data.result[i].total_pages;

}

$("#vte-window").data("vte-active-projects", data);

vte.setStorage("vte-active-projects", data, {expires: 7});

},

error: function(xhr, stat, err) {

console.error("Failed to request active projects: " + JSON.stringify(xhr));

}

});

},

// getWikiPage - requests page content for the last revision of a wiki page

// obj can contain:

// title: The page title to request data for

// onCreate: Function called if the requested page doesn't exist

// onSuccess: Function called on successfully fetching the page

// onFailure: Function called on failing to fetch the page

getWikiPage: function(obj) {

if (typeof(obj) !== 'object') obj = {};

var title = obj.title;

if (! title) {

console.error("getWikiPage: 'title' argument is required");

return false;

}

if (! ("onSuccess" in obj)) obj.onSuccess = function() {};

if (! ("onFailure" in obj)) obj.onFailure = function() {};

if (! ("onCreate" in obj)) obj.onCreate = function() {};

$.getJSON(

mw.util.wikiScript('api'),

{

format: "json",

action: "query",

prop: "revisions",

rvprop: "content",

rvlimit: 1,

titles: title,

}

)

.done(function(data) {

var page, text;

//try {

for (page in data.query.pages) {

text = data.query.pages[page].revisions[0]["*"];

}

obj.onSuccess(text);

/*

} catch(e) {

// If the page is missing call obj.onCreate()

if ("-1" in data.query.pages && data.query.pages["-1"].missing == "") {

console.log("Requested page not found: " + obj.title);

obj.onCreate();

} else {

obj.onFailure(e);

}

}

  • /

})

.fail(function(e) {

obj.onFailure(e);

});

},

// updateWikiPage - updates a wiki page with a given string

// obj can contain:

// title: The page title to update

// text: The full text of the updated page

// summary: The summary for the revision

// onSuccess: Function called on successful updates

// onFailure: Function called on failing to update

updateWikiPage: function(obj) {

if (typeof(obj) !== 'object') obj = {};

var title = obj.title;

if (! title) {

console.error("getWikiPage: 'title' argument is required");

return false;

}

if (! ("onSuccess" in obj)) obj.onSuccess = function() {};

if (! ("onFailure" in obj)) obj.onFailure = function() {};

if (! ("summary" in obj)) obj.summary = "[VTE] Updating page contents";

// Make the request to update the page

$.ajax({

url: mw.util.wikiScript( 'api' ),

type: 'POST',

dataType: 'json',

data: {

format: 'json',

action: 'edit',

title: obj.title,

text: obj.text, // will replace entire page content

summary: obj.summary,

token: mw.user.tokens.get( 'editToken' )

}

})

.done( obj.onSuccess )

.fail( obj.onFailure );

},

// getTaskData - requests data from the Tasks page for this project, creates the page if it doesn't exist

getTaskData: function() {

var vte_project = $("#vte-window").data("vte-project");

var obj = {

title: "User:Vtebot/" + vte_project.title + "/Tasks",

onCreate: function() {

vte.updateTaskData();

vte.getTaskData();

},

onSuccess: function(text) {

var res = vte.parseTable(text, "tasks");

vte_project = $("#vte-window").data("vte-project");

vte_project.tasks = res;

$("#vte-window").data("vte-project", vte_project);

},

onFailure: function(e) {

console.error("Failed to request wiki page: " + JSON.stringify(e));

},

};

vte.getWikiPage(obj);

},

// getTaskTalkData - requests data from the Tasks Talk page for this project, create if it doesn't exist

getTaskTalkData: function() {

var vte_project = $("#vte-window").data("vte-project");

var obj = {

title: "User_talk:Vtebot/" + vte_project.title + "/Tasks",

onCreate: function() {

vte.updateTaskTalkData();

vte.getTaskTalkData();

},

onSuccess: function(text) {

var res = vte.parseTalk(text, "tasks_talk");

vte_project = $("#vte-window").data("vte-project");

vte_project.tasks_talk = res;

$("#vte-window").data("vte-project", vte_project);

},

onFailure: function(e) {

console.error("Failed to request wiki talk page: " + JSON.stringify(e));

},

};

vte.getWikiPage(obj);

},

// getMemberData - requests data from the Members page for this project, creates the page if it doesn't exist.

// This function will /also/ grab procedural members, ie, those required to build out the social network

// for the project and inform the import function.

getMemberData: function() {

var vte_project = $("#vte-window").data("vte-project");

var obj = {

title: "User:Vtebot/" + vte_project.title + "/Members",

onCreate: function() {

vte.updateMemberData();

vte.getMemberData();

},

onSuccess: function(text) {

var res = vte.parseTable(text, "members");

vte_project = $("#vte-window").data("vte-project");

vte_project.members = res;

$("#vte-window").data("vte-project", vte_project);

},

onFailure: function(e) {

console.error("Failed to request Members page: " + JSON.stringify(e));

},

};

vte.getWikiPage(obj);

/****

* Grab the procedural member data. This will require 4 data requests:

* 1) Get the top editors to the current project, subpages, and talk pages.

* 2) Get all users who have links on the project page and all sub-pages (not talk pages).

* Note - Users flagged 3 ways: user link on page, edit to page, edit to talk page

* 3) Get all pages that are under the scope of this project.

* 4) For each of those user, get the top pages each of those

* editors edited,

* Note - Pages flagged 2 ways: in-project or out-project

*

* And that's it.

* Queries 1, 2, and 3 can be run concurrently. 4 depends on 1 and 2.

****/

// object to hold the data

var res = {

editors: {},

links: {},

p_pages: {},

pages: {},

};

// Boolean to track ongoing requests

var complete = {

editors: 0,

links: 0,

p_pages: 0,

pages: 0,

};

// 1) Fetch top editors to the project page, sub-pages, and corresponding talk pages

$.ajax({

url: data_api + "/api/getEdits?",

data: {

page: vte_project.title,

//sd: , // Default range is 1 year, ending now, which should be appropriate

//ed: ,

group: "user|page|date",

subpages: 1,

namespace: "4|5",

//limit: topEditors,

excludeBots: 1,

},

dataType: "json",

error: function(xhr, stat, err) {

console.error("Failed to request data, project editors. Response: " + JSON.stringify(xhr));

},

success: function(data, stat, xhr) {

// Check for error

if (data.errorstatus == 'fail') {

console.error("Error: Failed to request data, project editors: " + data.message);

}

// Save the results

complete.editors = 1;

res.editors = data.result;

if (res.editors.length == 0) {

console.warn("No edits to the project page or subpages found for project: " + vte_project.title);

}

}

});

// 2) Get all users who have links on the project page and all sub-pages

$.ajax({

url: data_api + "/api/getProjectMembers?",

data: {

project: vte_project.title,

//sd: , // Default time span is 1 year, which is appropriate for now.

//ed: ,

},

dataType: "json",

error: function(xhr, stat, err) {

console.error("Failed to request data, member links: " + JSON.stringify(xhr));

},

success: function(data, stat, xhr) {

// Check for error

if (data.errorstatus == 'fail') {

console.error("Error: Failed to request data, project members: " + data.message);

}

// Save the results

complete.links = 1;

res.links = data.result;

if (Object.keys(res.links).length == 0) {

console.warn("No project member user links found for project: " + vte_project.title);

}

}

});

// 3) Get all pages that are under the scope of this project

$.ajax({

url: data_api + "/api/getProjectPages?",

data: {

project: vte_project.title,

},

dataType: "json",

error: function(xhr, stat, err) {

console.error("Failed to request data, project pages: " + JSON.stringify(xhr));

},

success: function(data, stat, xhr) {

// Check for error

if (data.errorstatus == 'fail') {

console.error("Error: Failed to request data, project pages: " + data.message);

}

// Save the results

complete.p_pages = 1;

res.p_pages = data.result;

if (Object.keys(res.p_pages).length == 0) {

$rlog("No pages found for project: " + vte_project.title);

}

}

});

// 4) For each of the users, get the top pages each of those users edited.

// This depends on 1 & 2 above, so wait until they're done to complete

var start = new Date().getTime();

var t = setInterval( function() {

if (complete.editors == 1 && complete.links == 1) {

clearInterval(t);

// Grab the users from editors and links and look for all pages they edited

var uids = [];

for (var u in res.links) {

for (var p in res.links[u]) {

if (res.links[u][p].link_count > 0 && res.links[u][p].pm_user_id != 0)

uids.push(res.links[u][p].pm_user_id);

}

}

for (var i in res.editors) {

if (res.editors[i].tu_id != 0)

uids.push(res.editors[i].tu_id);

}

// And then, finally, we collect the edit histories of the users who worked on

// the project page (editors) and those who placed their user links on the project

// page (links).

$.ajax({

url: data_api + "/api/getEdits?",

data: {

userid: uids.join("|"),

//sd: , // Default is to get edits for 1 year, ending now, which is fine.

//ed: ,

group: "user|page",

namespace: "0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|100|101|108|109|118|119|446|447|710|711|828|829",

limit: 2000, // Limiting mostly to reduce download size of result, usually about 3-4 Mb

excludeBots: 1,

},

dataType: "json",

error: function(xhr, stat, err) {

console.error("Failed to request data, global edits: " + JSON.stringify(xhr));

},

success: function(data, stat, xhr) {

// Check for an error

if (data.errorstatus == 'fail') {

console.error("Error, failed to request data, global edits: " + data.message);

}

// Save the results

complete.pages = 1;

res.pages = data.result;

if (res.pages.length == 0) {

console.warn("No edits found for editors and members of project: " + vte_project.title);

}

}

});

} else {

// Timeout after 60 seconds

if ( (new Date().getTime()) - start >= 60000) {

clearInterval(t);

console.error("Timed out requesting project network data: " + JSON.stringify(complete));

}

}

}, 100);

// Wait until all 4 data requests are complete

var t1 = setInterval( function() {

if (complete.editors == 1 && complete.links == 1 &&

complete.p_pages == 1 && complete.pages == 1) {

// We're all done, clear the interval and save the data

clearInterval(t1);

vte_project = $("#vte-window").data("vte-project");

vte_project.members_network = res;

$("#vte-window").data("vte-project", vte_project);

} else {

// Timeout after 60 seconds

if ( (new Date().getTime()) - start >= 60000) {

clearInterval(t1);

console.error("Timed out requesting project network data: " + JSON.stringify(complete));

}

}

}, 100);

},

// getMemberTalkData - requests data from the Talk page for this project's members, creates it if needed

getMemberTalkData: function() {

var vte_project = $("#vte-window").data("vte-project");

var obj = {

title: "User_talk:Vtebot/" + vte_project.title + "/Members",

onCreate: function() {

vte.updateMemberTalkData();

vte.getMemberTalkData();

},

onSuccess: function(text) {

var res = vte.parseTalk(text, "members_talk");

vte_project = $("#vte-window").data("vte-project");

vte_project.members_talk = res;

$("#vte-window").data("vte-project", vte_project);

},

onFailure: function(e) {

console.error("Failed to request wiki talk page: " + JSON.stringify(e));

},

};

},

// updateTaskData - will update the current task list with data from $("#vte-window").data("vte-project").tasks

// and create a corresponding talk page section, saved in $("#vte-window").data("vte-project").tasks_talk

updateTaskData: function(onSuccess, onFailure) {

if (typeof(onSuccess) === 'undefined') onSuccess = function(){};

if (typeof(onFailure) === 'undefined') onFailure = function(){};

var vte_project = $("#vte-window").data("vte-project");

var title = "User:Vtebot/" + vte_project.title + "/Tasks";

// Build the page text, if we don't currently have any tasks we're probably creating the stub page

var tasks_str = "";

if ($.isEmptyObject(vte_project.tasks)) {

// Create the page with the tasks stub, default display is task title, description, and priority

tasks_str =

"\n\n" +

"{{#invoke:ListMaster|printTable|style=table|display=title,description,priority|\n" +

"}}";

} else {

// Otherwise, build the task string from the tasks object

tasks_str = vte_project.tasks.pre +

"{{#invoke:ListMaster|printTable|style=" + vte_project.tasks.style +

"|display=" + vte_project.tasks.display + "|\n";

for (var i in vte_project.tasks.struc) {

tasks_str += " {{#";

var attribs = [];

for (var n in vte_project.tasks.struc[i]) {

// Don't save empty values

if (vte_project.tasks.struc[i][n]) attribs.push(n + "=" + vte_project.tasks.struc[i][n]);

}

tasks_str += attribs.join("|") + "}}\n";

}

tasks_str += "}}" + vte_project.tasks.post;

}

var obj = {

title: title,

text: tasks_str,

summary: "[VTE] Updating details for task: " + $("#vte-task-title").val(),

onSuccess: function() {

// Emit vte update

vte_sock.emit("update", {

name: mw.config.get("wgUserName"),

time: new Date(),

page: mw.config.get("wgTitle"),

namespace: mw.config.get("wgNamespaceNumber"),

project: $("#vte-window").data("vte-project").title,

view: "Tasks",

});

onSuccess();

},

onFailure: function(e) {

console.error("Failed to update tasks page: " + JSON.stringify(e));

onFailure();

},

};

//vte.updateWikiPage(obj);

onSuccess();

},

// updateTaskTalkData - will update the current task talk data from $("#vte-window").data("vte-project").task_talk

updateTaskTalkData: function(onSuccess, onFailure) {

if (typeof(onSuccess) === 'undefined') onSuccess = function(){};

if (typeof(onFailure) === 'undefined') onFailure = function(){};

var vte_project = $("#vte-window").data("vte-project");

var title = "User_talk:Vtebot/" + vte_project.title + "/Tasks";

// Build the Tasks Talk page string, if we don't currently have any content we're probably creating the page

var talk_str = "";

if ($.isEmptyObject(vte_project.tasks_talk)) {

// Create the page with the talk stub (empty string)

talk_str = "";

} else {

// Otherwise, build the talk page string from the tasks_talk object

for (var task in vte_project.tasks_talk) {

talk_str += "== " + task + " ==\n";

for (var i in vte_project.tasks_talk[task]) {

var obj = vte_project.tasks_talk[task][i];

talk_str += Array(obj.level + 1).join(":") + obj.msg + "\n";

}

}

}

// Finally, request the page update

var obj = {

title: title,

text: talk_str,

summary: "[VTE] Updating Tasks Talk page",

onSuccess: function() {

onSuccess();

},

onFailure: function(e) {

console.error("Failed to update Tasks Talk page: " + JSON.stringify(e));

onFailure(e);

},

};

//vte.updateWikiPage(obj);

onSuccess();

},

// updateMemberData - will update the current member list with data from

// $("#vte-window").data("vte-project").members

updateMemberData: function(onSuccess, onFailure) {

if (typeof(onSuccess) === 'undefined') onSuccess = function(){};

if (typeof(onFailure) === 'undefined') onFailure = function(){};

var vte_project = $("#vte-window").data("vte-project");

var title = "User:Vtebot/" + vte_project.title + "/Members";

// Build the page text, if we don't currently have any members we're probably creating the stub page

var members_str = "";

if ($.isEmptyObject(vte_project.members)) {

// Create the page with the members stub, default display is member name and interests

members_str =

"\n\n" +

"{{#invoke:ListMaster|printTable|style=table|display=name,interests|\n" +

"}}";

} else {

// Otherwise, build the member string from the members object

members_str = vte_project.members.pre +

"{{#invoke:ListMaster|printTable|style=" + vte_project.members.style +

"|display=" + vte_project.members.display + "|\n";

for (var i in vte_project.members.struc) {

members_str += " {{#";

var attribs = [];

for (var n in vte_project.members.struc[i]) {

attribs.push(n + "=" + vte_project.members.struc[i][n]);

}

members_str += attribs.join("|") + "}}\n";

}

members_str += vte_project.members.post;

}

var obj = {

title: title,

text: members_str,

summary: "[VTE] Updating project members",

onSuccess: function() {

// Emit vte update

vte_sock.emit("update", {

name: mw.config.get("wgUserName"),

time: new Date(),

page: mw.config.get("wgTitle"),

namespace: mw.config.get("wgNamespaceNumber"),

project: $("#vte-window").data("vte-project").title,

view: "Members",

});

onSuccess();

},

onFailure: function(e) {

console.error("Failed to update project members: " + JSON.stringify(e));

onFailure();

},

};

vte.updateWikiPage(obj);

},

// updateMemberTalkData - will update the current member talk data from

// $("#vte-window").data("vte-project").members_talk

updateMemberTalkData: function(onSuccess, onFailure) {

if (typeof(onSuccess) === 'undefined') onSuccess = function(){};

if (typeof(onFailure) === 'undefined') onFailure = function(){};

var vte_project = $("#vte-window").data("vte-project");

var title = "User_talk:Vtebot/" + vte_project.title + "/Members";

// Build the Memberss Talk page string, if we don't currently have any content we're probably creating the page

var talk_str = "";

if ($.isEmptyObject(vte_project.members_talk)) {

// Create the page with the talk stub (empty string)

talk_str = "";

} else {

// Otherwise, build the talk page string from the members_talk object

for (var member in vte_project.members_talk) {

talk_str += "== " + member + " ==\n";

for (var i in vte_project.members_talk[member]) {

var obj = vte_project.members_talk[member][i];

talk_str += Array(obj.level + 1).join(":") + obj.msg + "\n";

}

}

}

// Finally, request the page update

var obj = {

title: title,

text: talk_str,

summary: "[VTE] Updating Members Talk page",

onSuccess: function() {

onSuccess();

},

onFailure: function(e) {

console.error("Failed to update Members Talk page: " + JSON.stringify(e));

onFailure(e);

},

};

//vte.updateWikiPage(obj);

onSuccess();

},

// populateTitle - draws title bar content

populateTitle: function() {

var $vteTitle = $(

"

Virtual Team Explorer
" +

"

" +

"

" +

" " +

"

" +

"

" +

" " +

"

" +

"

" +

" " +

"

" +

"

"

);

// Attributions, via Wikimedia Commons:

// User: By GNOME icon artists (GNOME download / GNOME FTP) [GPL (http://www.gnu.org/licenses/gpl.html)]

// Gear: By MGalloway (WMF) (Own work) [CC-BY-SA-3.0 (http://creativecommons.org/licenses/by-sa/3.0)]

// Close: By MGalloway (WMF) (Own work) [CC-BY-SA-3.0 (http://creativecommons.org/licenses/by-sa/3.0)]

// Add the title and style the elements

$("#vte-window-right-title").html($vteTitle);

$("#vte-title").css(s_vteTitle);

$("#vte-title-actions").css(s_vteTitleActions);

$("#vte-title-action-user").css(s_vteTitleAction);

$("#vte-title-action-settings").css(s_vteTitleAction);

$("#vte-title-action-close").css(s_vteTitleAction);

// Add the actions

$("#vte-title-action-user").on("click", function() {

});

$("#vte-title-action-settings").on("click", function() {

});

$("#vte-title-action-close").on("click", function() {

console.log("closing the vte (will maintain current view).");

vte.setCookie("vte-status", "Closed");

$("#vte-window").hide();

});

},

// populateProjectSelect - draws the project browser content

populateProjectSelect: function() {

// Add search box and main nav

var $projectSelect = $(

"" +

"

"

);

// Add it

$("#vte-window-right-tool").html($projectSelect);

$("#vte-project-select-multi").hide();

// Style it

$("#vte-project-select-label").css(s_vteProjectSelectLabel);

$("#vte-project-select-input").css(s_vteProjectSelectInput);

// For each of the projects, add it to the dropdown

var projects = $("#vte-window").data("vte-projects").result;

$.each(projects, function(i,v) {

var project = projects[i]['p_title'].replace(/_/g, " ").toLowerCase();

var input = $("#vte-project-select-input").val().replace(/_/, " ").toLowerCase();

$("#vte-project-select-multi").append(

"

" vte-p-id='" + projects[i]['p_id'] + "' " +

" vte-p-title='" + projects[i]['p_title'] + "' vte-p-seen='0' " +

" vte-p-touched='0' vte-p-created='" + projects[i]['p_created'] + "' >" +

projects[i]['p_title'].replace(/_/g, " ") +

"

"

);

});

// Then style the things

$("#vte-project-select-multi").css(s_vteProjectSelectMulti);

$(".vte-project-select-multi-proj").css(s_vteProjectSelectMultiProj);

// Add hover color for project

$(".vte-project-select-multi-proj").hover(

function() {

$( this ).css("color", "#3B0B0B");

}, function() {

$( this ).css("color", "#000");

}

);

// Add the action to watch for keyup in the project input

vte.updateProjectSelect();

// Add click action to hide the list

$("body").on("click", function(evt) {

$("#vte-project-select-multi").hide();

});

// Add click action to load project summary

$(".vte-project-select-multi-proj").on("click", function(evt) {

var id = $(evt.currentTarget).attr("vte-p-id");

var title = $(evt.currentTarget).attr("vte-p-title");

var seen = $(evt.currentTarget).attr("vte-p-seen");

var touched = $(evt.currentTarget).attr("vte-p-touched");

var created = $(evt.currentTarget).attr("vte-p-created");

// Clear the project selection div

$("#vte-project-select-multi").remove();

// Load the project summary

console.log("loading summary for project " + title + ", id: " + id);

$("#vte-window").data("vte-project", {

title: title,

id: id,

created: created,

members: {},

tasks: {},

});

vte.pageTransition("vte-window-summary", function() {

vte.populateNav();

vte.populateProjectSummary();

});

});

},

updateProjectSelect: function() {

// Add the actions (everytime there's a key-up, update list of visible projects)

$("#vte-project-select-input").on("keyup", function() {

var input = $("#vte-project-select-input").val().replace(/ /g,"_").toLowerCase();

// FIRST, update the list of active projects

$(".vte-active-project").each(function(i, v) {

var project = $(v).attr("p_title").replace(/ /g, "_").toLowerCase();

if (project.indexOf(input) != -1) {

$(v).css("display", "block");

} else {

$(v).css("display", "none");

}

});

// SECOND, update the list from the multi-select dropdown

// Make sure we're showing the selection div

$("#vte-project-select-multi").show();

$(".vte-project-select-multi-proj").each(function(i,v) {

var project = $(v).attr("vte-p-title").replace(/ /g, "_").toLowerCase();

if (project.indexOf(input) != -1) {

$(v).css("display", "block");

} else {

$(v).css("display", "none");

}

});

// Print a message if no projects match the input

if ($(".vte-project-select-multi-proj").not(":hidden").length == 0) {

$("#vte-project-select-multi").append(

"

" +

" No matching projects found" +

"

"

);

} else {

$(".vte-project-select-multi-none").remove();

}

});

},

populateProjectExplorer: function() {

// Clear the content div, print initial greeting

$("#vte-window-explorer").html(

"

" +

" Search for a WikiProject in the box above, or select from the list of most " +

" active WikiProjects below to continue.
" +

" (Projects below represent the most active WikiProjects by edits to " +

" member pages within the last month, limited to those with at least 30 edits)" +

"

" +

"

"

);

$("#vte-summary-instructions").css(s_vteSummaryInstructions);

// Add in buttons to sort projects by edits, pages edited, or edits per page

$("#vte-summary-projects").append(

"

" +

" " +

"

" +

"

" +

" " +

"

" +

"

" +

" " +

"

" +

"

" +

" " +

"

"

);

// Add the project thumbnail for each of the most active projects

var active = $("#vte-window").data("vte-active-projects").result;

$.each(active, function(i,v) {

if (v.total_edits < 30) return true;

var proj = v;

// Mark the style as hidden if the project doesn't match the search input box

var project = proj.p_title.replace(/ /g, "_").toLowerCase();

var input = $("#vte-project-select-input").val().replace(/ /g,"_").toLowerCase();

var style = project.indexOf(input) != -1 ? " style='display: block;' " : " style='display: none;' ";

$("#vte-summary-projects").append(

"

" p_title='" + proj.p_title + "' p_created='" + proj.p_created + "' " + style + ">" +

"

" +

"

" +

"

" +

"

" +

"

" +

"

" +

"

" +

"

" +

"

" +

"

" +

"

" + proj.p_title.replace(/_/g," ") + "
Project Edits " + proj["4"] + "
Edits " + proj.total_edits + "
Pages Edited " + proj.total_pages + "
Edits per page " + (Math.round(proj.ratio * 100) / 100) + "
" +

"

"

);

// Save data on the div for sorting

$("#vte-active-project-" + proj.p_id).data("sort", {

edits: proj.total_edits,

pages: proj.total_pages,

ratio: proj.ratio,

project_edits: proj["4"],

});

// Add the hover action

$("#vte-active-project-" + proj.p_id).hover(

function() {

$(this).css("border", "solid 1px #848484");

}, function() {

$(this).css("border", "solid 1px #000000");

}

);

// Add the click action to the thumbnail

$("#vte-active-project-" + proj.p_id).click(function(e) {

// Set project attributes

$("#vte-window").data("vte-project", {

title: proj.p_title,

id: $(e.currentTarget).attr("p_id"),

title: $(e.currentTarget).attr("p_title"),

created: $(e.currentTarget).attr("p_created"),

});

// Draw the page

vte.pageTransition("vte-window-summary", function() {

vte.populateNav();

vte.populateProjectSummary();

});

});

});

// Style the thumbnails

$(".vte-active-project").css(s_vteActiveProject);

$(".vte-active-project-title").css(s_vteActiveProjectTitle);

$(".vte-active-project-label").css(s_vteActiveProjectLabel);

$(".vte-active-project-value").css(s_vteActiveProjectValue);

$(".vte-sort-summary-div").css(s_vteSortSummaryDiv);

// Add the action to sort

$(".vte-sort-summary").button().click(function(e) {

var sort_by = $(e.currentTarget).attr("vte-sort-summary-by");

var s = $("#vte-window").data("vte-active-projects-sort");

var items = $(".vte-active-project").sort(function(a,b) {

var da = $(a).data("sort")[sort_by];

var db = $(b).data("sort")[sort_by];

if (s.by == sort_by && s.direction == "desc") {

$("#vte-window").data("vte-active-projects-sort", {by: sort_by, direction: "asc"});

return (da < db) ? -1 : (da > db) ? 1 : 0;

} else {

$("#vte-window").data("vte-active-projects-sort", {by: sort_by, direction: "desc"});

return (db < da) ? -1 : (db > da) ? 1 : 0;

}

});

$("#vte-summary-projects").append(items);

});

// Trigger the initial sort action

$("#vte-window").data("vte-active-projects-sort", {by: "edits", direction: "asc"});

$("#vte-sort-summary-by-edits").click();

},

// populateProjectSummary - draws summary information for the project once it is selected

// from the vte-project-select-multi dropdown (or clicked on)

populateProjectSummary: function() {

// Update the project search input

$("#vte-project-select-input").val( $("#vte-window").data("vte-project").title.replace(/_/g," ") );

// Style the input

$("#vte-project-select-input").prop("disabled", true);

$("#vte-project-select-input").css("color", "#A4A4A4");

// Request/create the Tasks and Members pages under the vtebot user page.

vte.getTaskData();

vte.getTaskTalkData();

//vte.getMemberData();

// Set the project cookies

vte.setCookie("vte-project", $("#vte-window").data("vte-project"));

vte.setCookie("vte-view", "Summary");

var title, id, created;

title = $("#vte-window").data("vte-project").title;

id = $("#vte-window").data("vte-project").id;

created = $("#vte-window").data("vte-project").created;

// Emit vte project select

vte_sock.emit("project_load", {

name: mw.config.get("wgUserName"),

time: new Date(),

page: mw.config.get("wgTitle"),

namespace: mw.config.get("wgNamespaceNumber"),

project: $("#vte-window").data("vte-project").title,

});

// Add the close icon

$("#vte-project-select-input").after("");

$(".vte-close-project").css(s_vteCloseProject);

$(".vte-close-project").button().click(function() {

$("#vte-window").data("vte-project", false);

$(".vte-close-project").remove();

$("#vte-project-select-input").val("");

$("#vte-project-select-input").prop("disabled", false);

$("#vte-project-select-input").css("color", "#000000");

vte.removeCookie("vte-project");

vte.setCookie("vte-view", "Explorer");

vte.pageTransition("vte-window-explorer", function() {

vte.populateProjectExplorer();

vte.populateNav();

});

});

// Clear any existing data in the content window and add summary divs

$("#vte-window-summary").html(

"

" +

"

" +

"

" +

" Edits to Project (blue) and Project Talk (grey) pages" +

"

" +

"

Loading project edit data...
" +

"

" +

"

" +

"

" +

" Most active articles in the last 30 days (showing the last year)" +

"

" +

"

Loading revision history for project pages...
" +

"

" +

"

"

);

$("#vte-window-right-content-summary").css(s_vteWindowRightContentSummary);

$(".vte-loading").css(s_vteLoadingText);

$("#vte-window-right-content-summary-p-edits").css(s_vteWindowRightContentSummaryGraph);

$("#vte-window-right-content-summary-pages").css(s_vteWindowRightContentSummaryPages);

$("#vte-window-right-content-summary-new").css(s_vteWindowRightContentSummaryNew);

// Dynamically set the width so we don't get squished graphs if they're loaded too quickly.

// The graphs should be in the right-content, which is 80% of vte-window, which is 80% of the

// total width, minus padding (5px * 2 for vte-window right, 8px * 2 for the graph divs, 26px padding).

var width = (window.innerWidth * .8 * .8) - 52;

$("#vte-project-summary-graph").css("width", width + "px");

// Request summary data from our backend

var t = title.replace(/ /g, "_");

var sd = created.substr(0, 8);

var sw = vte.convertDateToWikiWeek(sd);

var url = data_api + "/api/getEdits?page=" + t + "&namespace=4|5&group=page|user|date&sd=" + sd;

$.ajax({

url: url,

dataType: "json",

success: function(data, stat, xhr) {

vte.drawProjectEdits(data, sw, "vte-project-summary-graph");

},

error: function(xhr, stat, err) {

console.error("Failed to request project edits: " + JSON.stringify(xhr));

$("#vte-window-right-content-summary").append("Failed to request project edits: " + JSON.stringify(xhr));

},

complete: function() {

$("#vte-loading-edits").remove();

},

});

// Request most active project pages

url = data_api + "/api/getActiveProjectPages?project_id=" + id;

$.ajax({

url: url,

dataType: "json",

success: function(data, stat, xhr) {

// Once we've got recent active project pages, grab edit histories for those pages

var ids = [];

for (var i in data.result) {

if (data.result[i].tp_namespace == 0 || data.result[i].tp_namespace == 1)

ids.push(data.result[i].pa_page_id);

}

// We'll want to get edits for the last year

var now = new Date();

var sd = String(now.getFullYear()-1) + String(vte.pad(now.getMonth()+1,2)) +

String(vte.pad(now.getDate(), 2));

var sw = vte.convertDateToWikiWeek(sd);

var ew = vte.convertDateToWikiWeek() - 1;

url = data_api + "/api/getEdits?pageid=" + ids.join("|") + "&limit=0&namespace=0|1&group=page|user|date&sw=" + sw + "&ew=" + ew;

$.ajax({

url: url,

dataType: "json",

success: function(data,stat,xhr) {

// Split the results by page

var pages = {};

for (var i in data.result) {

if (! pages.hasOwnProperty( data.result[i].rc_page_id )) pages[ data.result[i].rc_page_id ] = [];

pages[ data.result[i].rc_page_id ].push( data.result[i] );

}

for (var id in pages) {

//$.each(pages, function(i,v) {

// Create the graph div for each of the returned articles and draw the graph

$("#vte-window-right-content-summary-pages").append(

"

" + pages[id][0].tp_title.replace(/_/g," ") + "
" +

"

" id='vte-window-right-content-summary-page-" + id + "' />"

);

$("#vte-window-right-content-summary-page-" + id).css(s_vteWindowRightContentSummaryPage);

$("#vte-window-right-content-summary-page-" + id).css("width", width + "px");

vte.drawProjectEdits({result: pages[id]}, sw, "vte-window-right-content-summary-page-" + id);

}

$(".vte-summary-page-title").css(s_vteSummaryPageTitle);

// Add actions to article titles to set cookies and go to the page

$(".vte-summary-page-title").click(function(e) {

var title = $(e.currentTarget).html().replace(/ /g, "_");

window.location.href = "/wiki/" + title;

});

},

error: function(xhr, stat, err) {

console.error("Failed to request edits to most active articles: " + JSON.stringify(xhr));

$("#vte-window-right-content-summary").append("Failed to request active article edits: " +

JSON.stringify(xhr));

},

complete: function() {

$("#vte-loading-pages").remove();

},

});

},

error: function(xhr, stat, err) {

console.error("Failed to request active project pages: " + JSON.stringify(xhr));

$("#vte-window-right-content-summary").append("Failed to request active project pages: " +

JSON.stringify(xhr));

},

});

},

// drawProjectEdits - draws summary edit information for a project and its corresponding Talk page

drawProjectEdits: function(data, sw, div_id) {

// Structure the edits

var ew = vte.convertDateToWikiWeek(); // This should be 1 greater than what was requested.

var talk_edits = Array(ew - sw);

var page_edits = Array(ew - sw);

for (var i = 0; i < talk_edits.length; i++) talk_edits[i] = 0;

for (var i = 0; i < page_edits.length; i++) page_edits[i] = 0;

for (var i in data["result"]) {

if (data["result"][i].rc_page_namespace % 2 == 0) page_edits[ data["result"][i].rc_wikiweek - sw ] += data["result"][i].rc_edits;

if (data["result"][i].rc_page_namespace % 2 == 1) talk_edits[ data["result"][i].rc_wikiweek - sw ] += data["result"][i].rc_edits;

}

// D3 sparkline graph

// Get the width from the style of the parent element (the actual width from .width() may not be

// correct if the element is still being drawn)

var w = parseInt($("#" + div_id).css("width").replace("px", "")) - 10;

var h = $("#" + div_id).height();

var t_max = d3.max(talk_edits);

var p_max = d3.max(page_edits);

var maxy = t_max > p_max ? t_max : p_max;

var y = d3.scale.linear()

.domain([0, maxy])

.range([0, h]);

var x = d3.scale.linear()

.domain([0, page_edits.length])

.range([0, w]);

var vis = d3.select("#" + div_id)

.append("svg:svg")

.attr("width", w)

.attr("height", h);

var g1 = vis.append("svg:g").attr("transform", "translate(2, " + h + ")");

var g2 = vis.append("svg:g").attr("transform", "translate(2, " + h + ")");

var line = d3.svg.line()

.x(function(d, i) {

return x(i);

})

.y(function(d) {

return -1 * y(d);

});

g1.append("svg:path").attr("d", line(page_edits)).style({"stroke": "#0000FF", "fill": "transparent"});

g2.append("svg:path").attr("d", line(talk_edits)).style({"stroke": "#545454", "fill": "transparent"});

// Add the legend text

var count_text = [

{ "cx": 10, "cy": 12, "text": maxy + " edits" },

{ "cx": 10, "cy": h-5, "text": "0" }

];

var date_text = [

{ "cx": w / 3, "text": vte.convertWikiWeekToDate( ((ew - sw) / 3) + sw ) },

{ "cx": w * 2 / 3, "text": vte.convertWikiWeekToDate( ((ew - sw) * 2 / 3) + sw) }

];

var text_c = vis.selectAll("text.count")

.data(count_text)

.enter().append("text")

.attr("x", function(d) { return d.cx; })

.attr("y", function(d) { return d.cy; })

.text( function(d) { return d.text; })

.attr("font-family", s_wpFont)

.attr("font-size", "10px")

.attr("fill", "#000000");

var text_d = vis.selectAll("text.date")

.data(date_text)

.enter().append("text")

.attr("x", function(d) { return d.cx; })

.attr("y", function(d) { return 12; })

.text( function(d) { return d.text.substring(0,4) + "/"+d.text.substring(4,6) + "/"+d.text.substring(6,8); })

.attr("font-family", s_wpFont)

.attr("font-size", "10px")

.attr("text-anchor", "middle")

.attr("fill", "#848484");

},

// populateNav - draws the navigation content

populateNav: function() {

if ($("#vte-window").data("vte-project")) {

// Make sure we're not duplicating the nav links (there was a bug where this happened that

// I can't seem to repro)

$("#vte-window-right-nav").empty();

$("#vte-window-right-nav").append(

"

Communication
" +

"

Tasks
" +

"

Members
" +

"

Summary
" +

"

"

);

// Style it

$(".vte-content-nav").css(s_vteWindowRightContentTitle);

$("#vte-summary").css("color", "#000");

// Add the click actions for the nav links

$(".vte-content-nav").click(function(e) {

var id = $(e.currentTarget).attr("id");

if (id == "vte-summary") {

vte.pageTransition("vte-window-summary", function() {

$(".vte-content-nav").css("color", "#0B0B61");

$("#vte-summary").css("color", "#000");

vte.populateProjectSummary();

});

} else if (id == "vte-members") {

vte.pageTransition("vte-window-members", function() {

$(".vte-content-nav").css("color", "#0B0B61");

$("#vte-members").css("color", "#000");

vte.clickMembers();

});

} else if (id == "vte-tasks") {

vte.pageTransition("vte-window-tasks", function() {

$(".vte-content-nav").css("color", "#0B0B61");

$("#vte-tasks").css("color", "#000");

vte.clickTasks();

});

} else if (id == "vte-communication") {

vte.pageTransition("vte-window-communication", function() {

$(".vte-content-nav").css("color", "#0B0B61");

$("#vte-communication").css("color", "#000");

vte.clickCommunication();

});

} else {

console.error("Unknown vte action: " + id);

}

});

// And make sure it's visible

$("#vte-window-right-nav").show();

} else {

$(".vte-content-nav, .vte-content-nav-spacer").remove();

}

},

// Function to handle page transitions

pageTransition: function(page, load_function) {

// Before the transition, hide all pages and show the loading window

$(".vte-page").hide();

$("#vte-window-right-nav").hide();

$("#vte-window-loading").show();

// Add pulse animation to loading text

var i = 0;

var t = setInterval(function() {

if (i % 2 == 0) {

$("#vte-window-loading").animate({opacity: 0.3}, 1000, "linear");

} else {

$("#vte-window-loading").animate({opacity: 1.0}, 1000, "linear");

}

i++;

}, 1000);

// Then load the page

load_function();

// Then switch to it

$(".vte-page").hide();

vte.populateNav();

$("#" + page).show();

// And stop the pulse animation

clearInterval(t);

},

// Given a chunk of text, will return an object containing text before, after, and an array of

// top-level module invocations (won't parse modules in modules). Returns false if no modules found.

parseInvocation: function(data) {

//var obj = {pre: "", post: "", mods: []};

var obj = [];

var s_index = 0, e_index = 0, s_paren = 0, c_paren = 0;

for (var i = 0; i < data.length; i++) {

if ((data.slice(i, i+3) == "{{#") && (s_paren + c_paren == 0)) s_index = i;

if (data[i] == "{") s_paren += 1;

if (data[i] == "}") c_paren += 1;

if ((s_paren == c_paren) && (s_paren + c_paren > 0)) {

e_index = i + 1;

s_paren = 0, c_paren = 0;

obj.push({

pre: data.slice(0, s_index),

post: data.slice(e_index),

mod: data.slice(s_index, e_index),

});

}

}

if (s_paren > 0 || c_paren > 0) console.error("Uneven brace count, possible incorrect module declaration.");

return obj.length > 0 ? obj : false;

},

parseTable: function(data, table) {

// Grab module invocation from the page text (at this level only accepting one table)

var obj = vte.parseInvocation(data);

//s_index = data.indexOf("{{#invoke:ListMaster");

if (! obj) {

console.error("Failed to find module invocation, page contains: " + data);

return false;

}

// Then grab top-level submodule invocations from within this module

var subs = [], mod = {};

for (var i in obj) {

if (obj[i].mod.slice(0, 20) == "{{#invoke:ListMaster") {

mod = obj[i];

subs = vte.parseInvocation(obj[i].mod.slice(3, -2));

}

}

if (Object.keys(mod).length == 0) {

console.error("Module invocation on page, but not {{#invoke:ListMaster...");

return false;

}

// Break apart sub-module invocations, grabbing columns for each row

var struc = [];

for (var i in subs) {

// Strip the braces

subs[i].mod = subs[i].mod.slice(3, -2);

var attribs = subs[i].mod.split("|");

var row = {};

for (var j in attribs) {

// Split pair at first equals sign, so "=" can be used in values

var pair = attribs[j].split(/=([\s\S]+)?/);

// Don't add keys without values

if (typeof(pair[1]) !== 'undefined') row[pair[0].trim()] = pair[1].trim();

}

struc.push(row);

}

// Then, pull out the style and display values from the parent module

var re1 = new RegExp("\\|[^\\|]*style=([^\\|]+)");

var style = mod.mod.match(re1)[1].trim();

var re2 = new RegExp("\\|[^\\|]*display=([^\\|]+)");

var display = mod.mod.match(re2)[1].split(",").map(function(str) { return str.trim(); });

// And save everything

var vte_project = $("#vte-window").data("vte-project");

obj = {

pre: mod.pre,

post: mod.post,

struc: struc,

style: style,

display: display,

};

vte_project[table] = obj;

$("#vte-window").data("vte-project", vte_project);

return obj;

},

parseUser: function(text) {

var m2, m3, user, date;

// Try to grab the user from the prior post

m2 = text.match(/\[\[User:([^\|\]]+).+(\d{2}:\d{2}, \d+ \S+ \d{4} \(UTC\))/);

m3 = text.match(/\[\[User:([^\|\]]+)/);

if (m2 !== null) {

user = m2[1]; date = m2[2];

} else if (m3 !== null) {

user = m3[1]; date = "Unknown";

} else {

user = "Unknown"; date = "Unknown";

}

return {user: user, date: date};

},

parseTalkSection: function(section) {

var m1, m2, m3, o, text, posts = [], level = 0, index_to = 0;

for (var i in section) {

// We have a complete post if we're starting a new indent (":"), if we found a user

// signature, or if we're the last element of the array

m1 = section[i].match(/^(:+)(.*)/);

o = vte.parseUser(section[i]);

if (m1 !== null) {

// Strip the colon from the beginning of the string

section[i] = section[i].replace(/^(:+)/, "");

text = section.slice(index_to, (parseInt(i)+1)).join("\n");

index_to = (parseInt(i)+1);

level = m1[1].length;

o = vte.parseUser(text);

posts.push({

msg: text.trim(),

user: o.user,

date: o.date,

level: level,

});

} else if (o.user != "Unknown") {

text = section.slice(index_to, (parseInt(i)+1)).join("\n");

index_to = (parseInt(i) + 1);

posts.push({

msg: text.trim(),

user: o.user,

date: o.date,

level: 0,

});

} else if (i == section.length-1) {

text = section.slice(index_to, i+1).join("\n");

if (text == "") continue;

index_to = (parseInt(i) + 1);

o = vte.parseUser(text);

posts.push({

msg: text.trim(),

user: o.user,

date: o.date,

level: 0,

});

}

}

return posts;

},

// parseTalk - Parses a talk page, returns object where key is section heading and value

// is an array of objects. Supports nested conversations.

parseTalk: function(data, table) {

// Go through the talk page text, build each talk object by section header

var lines = data.split("\n");

var obj = {}; var section = []; var title = ""; var p_title = ""; var post = "";

for (var i in lines) {

if (! lines[i]) continue;

// If this is a new section, add the prior one to the return obj (if it exists)

var m;

m = lines[i].match(/^== ?(.+) ?== *$/);

if (m !== null && section.length == 0) {

title = m[1].trim();

} else if (m !== null && section.length > 0) {

obj[title] = vte.parseTalkSection(section);

title = m[1].trim();

section = [];

} else {

// Otherwise save the section text

section.push(lines[i]);

}

}

// And add the final section

obj[title] = vte.parseTalkSection(section);

return obj;

},

// Functions to populate the primary vte systems (ie, members, tasks, etc)

clickMembers: function() {

// Update the view cookie

vte.setCookie("vte-view", "Members");

// Clear the current content window

$("#vte-window-members").html("");

// Emit vte view

vte_sock.emit("view", {

name: mw.config.get("wgUserName"),

time: new Date(),

page: mw.config.get("wgTitle"),

namespace: mw.config.get("wgNamespaceNumber"),

project: $("#vte-window").data("vte-project").title,

view: "Members"

});

console.log("vte - drawing member content");

// We've already requested the Member content, draw page or wait for content to load

var t = setTimeout(function() {

var members = $("#vte-window").data("vte-project").members;

if (typeof(members) !== 'undefined' && ! $.isEmptyObject(members)) {

clearInterval(t);

vte.drawMembers();

}

}, 100);

},

clickTasks: function() {

// Update the view cookie

vte.setCookie("vte-view", "Tasks");

// Clear the current content window

$("#vte-window-tasks").html("");

// Emit vte view

vte_sock.emit("view", {

name: mw.config.get("wgUserName"),

time: new Date(),

page: mw.config.get("wgTitle"),

namespace: mw.config.get("wgNamespaceNumber"),

project: $("#vte-window").data("vte-project").title,

view: "Tasks"

});

console.log("vte - drawing task content");

// We've already requested the Task content, draw page or wait for content to load

var t = setInterval(function() {

var tasks = $("#vte-window").data("vte-project").tasks;

if (typeof(tasks) !== 'undefined' && ! $.isEmptyObject(tasks)) {

clearInterval(t);

vte.drawTasks();

}

}, 100);

},

clickCommunication: function() {

// Update the view cookie

vte.setCookie("vte-view", "Communication");

// Clear the current content window

$("#vte-window-communication").html("");

// Emit vte view

vte_sock.emit("view", {

name: mw.config.get("wgUserName"),

time: new Date(),

page: mw.config.get("wgTitle"),

namespace: mw.config.get("wgNamespaceNumber"),

project: $("#vte-window").data("vte-project").title,

view: "Communication"

});

console.log("vte - drawing communication content");

var project = $("#vte-window").data("vte-project").title;

// TODO: Load wiki communication

vte.drawCommunication();

},

drawMembers: function(data) {

// Clear the current content window

$("#vte-window-members").html("");

// Grab the member list data

var obj = $("#vte-window").data("vte-project").members;

var talk = $("#vte-window").data("vte-project").members_talk;

// Add the add member and import members buttons first

$("#vte-window-members").append(

"

+ Add member
" +

"

+ Import members from project network
" +

"

View:All" + action + "
" +

"

Sort:Created" + action + "
" +

"

"

);

// Style the buttons

$(".vte-members-create, .vte-members-import").css(s_vteMembersCreate);

// If we don't have any members, prompt to import from project pages

if (obj.struc.length == 0) {

$("#vte-window-members").append(

"

" +

" This project currently does not have any members listed. " +

" Add members by clicking the '+ Add member' link, or import from project-related activity " +

" by clicking the '+ Import members from project network' link." +

"

"

);

// Remove the view and sort dropdowns

$("#vte-members-view, #vte-members-sort").remove();

$(".vte-members-empty").css(s_vteMembersEmpty);

}

// Draw the members table

$("#vte-window-members").append(

"

" +

"

" +

"

" +

"

" +

"

" +

"

" +

" " +

"

" +

"

"

);

// And display all the current members

for (var i in obj.struc) {

var n = "name" in obj.struc[i] ? obj.struc[i].name : "";

var proj = "project_edits" in obj.struc[i] ? obj.struc[i].project_edits : "";

var page = "page_edits" in obj.struc[i] ? obj.struc[i].page_edits : "";

var since = "member_since" in obj.struc[i] ? obj.struc[i].member_since : "";

// Attempt to parse date

var s_date = vte.parseDateStr(since);

var s_str = vte.getMonthText(s_date.getMonth() + 1, {abbrev: 1}) + " " + s_date.getDate() + ", " +

s_date.getFullYear();

// Distinguish between explicit members and activity-based members

var explicit = ("activity" in obj.struc[i] && obj.struc[i].activity) ? "vte-member-activity" : "vte-member-explicit";

// Display the row

$(".vte-members-table-body").append(

"" +

" " + n + "

" +

" " + since + "

" +

" " + proj + "

" +

" " + page + "

" +

""

);

}

// Style the table

$(".vte-members-table").css(s_vteMembersTable);

$(".vte-m-td").css(s_vteMembersRow);

$("#vte-members-view, #vte-members-sort").css(s_vteMembersView);

$("#vte-members-view").css(s_vteMembersView);

// Action when clickinig the View or Sort links

$("#vte-members-view").click(function(e) {

vte.drawMembersView(e);

});

$("#vte-members-sort").click(function(e) {

vte.drawMembersSort(e);

});

// Highlight row on hover

$(".vte-members-row").hover(

function() {

$(this).css("background-color", "#EFF5FB");

}, function() {

$(this).css("background-color", "#FFFFFF");

}

);

// Action to add a new member

$(".vte-members-create").click(function(e) {

// Draw the lightbox

vte.drawMemberEdit();

});

// Action to edit details for an existing user

$(".vte-members-row").click(function(e) {

var index = $(e.currentTarget).attr("vte-member-index");

vte.drawMemberEdit(index);

});

// Action to import member from project/page edits

$(".vte-members-import").click(function(e) {

vte.getMemberImportData();

});

},

drawMembersView: function(e) {

e.stopPropagation();

// Draw the View window, supports choosing from All, Activity, or Explicit

$("#vte-members-sort-actions").hide();

if ($("#vte-members-view-actions").length == 0) {

$("#vte-members-view").append(

"

" +

"

All
" +

"

Activity
" +

"

Explicit
" +

"

"

);

$("#vte-members-view-actions").css(s_vteDropdownList);

$(".vte-dropdown-item").css(s_vteDropdownItem);

} else {

$("#vte-members-view-actions").show();

}

// Close the menu if clicking outside of it or hitting escape

$("body, .vte-dropdown-item").one("click", function(e) {

e.stopPropogation();

var table = $(".vte-members-table");

if (e.target.id == "vte-members-view-all") {

$(".vte-member-activity").show();

$(".vte-member-explicit").show();

} else if (e.target.id == "vte-members-view-activity") {

$(".vte-member-activity").show();

$(".vte-member-explicit").hide();

} else if (e.target.id == "vte-members-view-explicit") {

$(".vte-member-activity").hide();

$(".vte-member-explicit").show();

}

$("#vte-members-view-actions").hide();

});

$(document).on("keyup.hide_actions", function(e) {

if (e.keyCode == 27) {

$("#vte-members-view-actions").hide();

$(document).unbind("keyup.hide_actions");

}

});

},

drawMembersSort: function(e) {

e.stopPropagation();

// Draw the sort window, supports sorting by name, since, project edits, page edits, etc

$("#vte-members-view-actions").hide();

if ($("#vte-members-sort-actions").length == 0) {

$("#vte-members-sort").append(

"

" +

"

Name
" +

"

Member Since
" +

"

Project Edits
" +

"

Page Edits
" +

"

"

);

$("#vte-members-sort-actions").css(s_vteDropdownList);

$(".vte-dropdown-item").css(s_vteDropdownItem);

} else {

$("#vte-members-sort-actions").show();

}

// Close the menu if clicking outside of it or hitting escape

$("body, .vte-dropdown-item").one("click", function(e) {

e.stopPropagation();

var table = $(".vte-members-table");

if (e.target.id == "vte-members-sort-name") {

var rows = table.find('tr').toArray().sort(vte.comparer(0));

// Determine if we're ascending or descending

$(".vte-members-table").data("name", !$(".vte-members-table").data("name"));

if (!$(".vte-members-table").data("name")) rows = rows.reverse();

for (var i = 0; i < rows.length; i++) { table.append(rows[i]); }

$("#vte-members-sort").html("Sort:Name" + action);

} else if (e.target.id == "vte-members-sort-since") {

var rows = table.find('tr').toArray().sort(vte.comparer(1));

$(".vte-members-table").data("since", !$(".vte-members-table").data("since"));

if (!$(".vte-members-table").data("since")) rows = rows.reverse();

for (var i = 0; i < rows.length; i++) { table.append(rows[i]); }

$("#vte-members-sort").html("Sort:Member Since" + action);

} else if (e.target.id == "vte-members-sort-proj") {

var rows = table.find('tr').toArray().sort(vte.comparer(2));

$(".vte-members-table").data("proj", !$(".vte-members-table").data("proj"));

if (!$(".vte-members-table").data("since")) rows = rows.reverse();

for (var i = 0; i < rows.length; i++) { table.append(rows[i]); }

$("#vte-members-sort").html("Sort:Project Edits" + action);

} else if (e.target.id == "vte-members-sort-page") {

var rows = table.find('tr').toArray().sort(vte.comparer(3));

$(".vte-members-table").data("page", !$(".vte-members-table").data("page"));

if (!$(".vte-members-table").data("page")) rows = rows.reverse();

for (var i = 0; i < rows.length; i++) { table.append(rows[i]); }

$("#vte-members-sort").html("Sort:Page Edits" + action);

}

$("#vte-members-sort-actions").hide();

});

$(document).on('keyup.hide_actions', function(e) {

if (e.keyCode == 27) {

$("#vte-members-sort-actions").hide();

$(document).unbind("keyup.hide_actions");

}

});

},

drawMemberEdit: function(index) {

console.log("in drawMemberEdit");

},

getMemberImportData: function() {

console.log("in getMemberImportData");

// Grab any potential current member data

var vte_project = $("#vte-window").data("vte-project");

// Draw the import lightbox

$("#vte-window").append(

"

" +

"

" +

" " +

"

" +

" " +

"

 
" +

"

" +

"

Loading edits to project pages...
" +

"

Loading project pages...
" +

"

Loading edits by top project editors...
" +

"

" +

"

" +

"

" +

"

" +

"

" +

"

" +

"

" +

" " +

"

" +

"

" +

"

"

);

// Style the lightbox

$("#vte-member-import").css(s_vteMemberImport)

$("#vte-import-loading-links, #vte-import-loading-proj, #vte-import-loading-page, #vte-import-loading-edits").css(s_vteMemberImportLoading);

// Handle the loading text if we're still requesting the data, located in vte_project.members_network

var elapsed = 0;

var t = setInterval(function() {

if ("members_network" in vte_project) {

clearInterval(t);

vte.drawMemberImport();

} else {

// Check for timeout

if (elapsed >= 60000) {

clearInterval(t);

console.error("Timed out requesting project member data");

}

}

elapsed += 100;

}, 100);

},

drawMemberImport: function(data) {

console.log("In drawMemberImport");

// Data will contain {editors: [], links: {}, p_pages: {}, pages: []} in vte_project.members_network

var vte_project = $("#vte-window").data("vte-project");

var network = vte_project.members_network;

var members = vte_project.members.struc;

$(".vte-loading").remove();

// Structure the network data - all project edits will be included, and all users with links

// on project pages. We can ignore network.[p_pages|pages] at this point.

// Then, add a row for each potential project member showing name, project edit sparkline,

// invitation button, etc

},

drawTasks: function(data) {

// Clear the current content window

$("#vte-window-tasks").html("");

// Grab the task list data

var obj = $("#vte-window").data("vte-project").tasks;

var talk = $("#vte-window").data("vte-project").tasks_talk;

// Projects have the option to include anything in the tasks table, but for the

// VTE we'll want to display the title, created, due, priority, and owner. In

// the task details we'll additionally display subtasks, burndown, etc.

// Add the create task button first

$("#vte-window-tasks").append(

"

+ Add task
" +

"

View:All" + action + "
" +

"

Sort:Created" + action + "
" +

"

"

);

$("#vte-tasks-view, #vte-tasks-sort").css(s_vteTasksView);

// If we don't have any tasks, prompt to create a new one

if (obj.struc.length == 0) {

$("#vte-window-tasks").append(

"

" +

" This project currently does not have any tasks listed. " +

" Add tasks by clicking the '+ Add task' link." +

"

"

);

// Remove the view and sort dropdowns

$("#vte-tasks-view, #vte-members-sort").remove();

$(".vte-tasks-empty").css(s_vteTasksEmpty);

}

// Will display created date, priority, title, number of comments, and owner

// Created color will be based on date since creation

// Priority color will be based on priority (either high/medium/low or 1/2/3)

// Font color will be based on whether the task is completed

$("#vte-window-tasks").append(

"

" +

"

" +

"

" +

"

" +

"

" +

"

" +

"

" +

" " +

"

" +

"

"

);

var closed = 0;

var open = 0;

for (var i in obj.struc) {

var t = "title" in obj.struc[i] ? obj.struc[i].title : "";

var c = "created" in obj.struc[i] ? obj.struc[i].created : "";

var d = "due" in obj.struc[i] ? obj.struc[i].due : "";

var p = "priority" in obj.struc[i] ? obj.struc[i].priority : "";

var o = "owner" in obj.struc[i] ? obj.struc[i].owner : "";

var com = t in talk ? talk[t].length : 0;

// Attempt to parse dates

var c_date = vte.parseDateStr(c);

var c_str = vte.getMonthText(c_date.getMonth() + 1, {abbrev: 1}) + " " + c_date.getDate() + ", " +

c_date.getFullYear();

var n_date = new Date();

var n_str = vte.getMonthText(n_date.getMonth() + 1, {abbrev: 1}) + " " + n_date.getDate() + ", " +

n_date.getFullYear();

var d_date = vte.parseDateStr(d);

var d_str = d_date ? vte.getMonthText(d_date.getMonth() + 1, {abbrev: 1}) + " " + d_date.getDate() + ", " +

d_date.getFullYear() : d;

// Whether the task was completed

var comp = ("completed" in obj.struc[i] && obj.struc[i].completed) ? "vte-task-completed" : "vte-task-open";

// Color of the creation date will be red for older open tasks, going towards black for

// newer tasks. Color progression will be for each week going back one month (ie, tasks

// created in the last week will be black, two weeks ago will be slightly red, etc).

// If we have a due date for this task, color will still go from black to red, but color

// steps will be between the current date and creation date and due date (ie, background

// color will get more red the closer we are to the due date, split into four equal time increments).

// If the due date passed, the color will be red.

var c_color;

if (d_date) {

var inc = (d_date.getTime() - c_date.getTime()) / 4;

var spent = n_date.getTime() - c_date.getTime();

if (d_date.getTime() < n_date.getTime()) {

c_color = "#FF0400";

} else if (Math.ceil(spent / inc) == 4) {

c_color = "#FF0400";

} else if (Math.ceil(spent / inc) == 3) {

c_color = "#BA0300";

} else if (Math.ceil(spent / inc) == 2) {

c_color = "#590200";

} else {

c_color = "#000000";

}

} else {

var w = 1000 * 60 * 60 * 24 * 7;

if (n_date.getTime() - c_date.getTime() > w * 3) {

c_color = "#FF0400";

} else if (n_date.getTime() - c_date.getTime() > w * 2) {

c_color = "#BA0300";

} else if (n_date.getTime() - c_date.getTime() > w) {

c_color = "#590200";

} else {

c_color = "#000000";

}

}

// Or, if we've already completed the task created background should just be black

if (comp == "vte-task-completed") c_color = "#000000";

//c = vte.getDateStr( vte.parseDateStr(c) );

//d = vte.getDateStr( vte.parseDateStr(d) );

// Parse any wikitext in the title

//t = wiky.process( t ); // Didn't work

t = InstaView.convert( t ).slice(3); // Removing first 4 characters, InstaView adds

to everything.

// Display the row

$(".vte-tasks-table-body").append(

"" +

" " + c_str + "" +

" " + p + "" +

" " + t + "" +

" " + com + " comments" +

" " + o + "" +

""

);

}

// Style the tables

$(".vte-tasks-table").css(s_vteTasksTable);

$(".vte-t-td").css(s_vteTasksRow);

$(".vte-task-completed").css(s_vteTaskCompleted);

$(".vte-tasks-table-title").css(s_vteTasksTableTitle);

$(".vte-tasks-table-priority").css(s_vteTasksTablePriority);

$(".vte-tasks-table-created").css(s_vteTasksTableCreated);

$(".vte-tasks-table-comments").css(s_vteTasksTableComments);

$(".vte-tasks-table-owner").css(s_vteTasksTableOwner);

$(".vte-tasks-table-due").css(s_vteTasksTableDue);

$(".oh, .ch").css({ "cursor": "pointer", "padding": "4px 0px" });

$(".vte-tasks-create").css(s_vteTasksCreate);

// Action when clicking the View or Sort links

$("#vte-tasks-view").click(function(e) {

vte.drawTasksView(e);

});

$("#vte-tasks-sort").click(function(e) {

vte.drawTasksSort(e);

});

// Action to highlight row on hover

$(".vte-tasks-row").hover(

function() {

$(this).css("background-color", "#EFF5FB");

}, function() {

$(this).css("background-color", "#FFFFFF");

}

);

// Make the table sortable by clicking the headers

$('.oh, .ch').click(function() {

$(".oh, .ch").css("background-color", "#FFFFFF");

$( this ).css("background-color", "#F2F2F2");

var table = $(this).parents('table').eq(0);

var rows = table.find('tr:gt(0)').toArray().sort(vte.comparer($(this).index()));

this.asc = !this.asc;

if (!this.asc) rows = rows.reverse();

for (var i = 0; i < rows.length; i++) { table.append(rows[i]); }

});

// Action to add new task

//$(".vte-tasks-create").button().click(function(e) {

$(".vte-tasks-create").click(function(e) {

e.preventDefault();

// Draw the lightbox

vte.drawTaskEdit();

}); // END submit new task

// Action to edit an existing task

$(".vte-tasks-row").click(function(e) {

var index = $(e.currentTarget).attr("vte-task-index");

vte.drawTaskEdit(index);

});

},

drawTasksView: function(e) {

e.stopPropagation();

// Draw the View window, supports choosing from All, Open, or Closed

$("#vte-tasks-sort-actions").hide();

if ($("#vte-tasks-view-actions").length == 0) {

$("#vte-tasks-view").append(

"

" +

"

All
" +

"

Open
" +

"

Closed
" +

"

"

);

$("#vte-tasks-view-actions").css(s_vteDropdownList);

$(".vte-dropdown-item").css(s_vteDropdownItem);

} else {

$("#vte-tasks-view-actions").show();

}

// Close the menu if clicking outside of it or hitting escape

$("body, #vte-tasks-view-actions").one("click", function(e) {

e.stopPropagation();

if (e.target.id == "vte-tasks-view-all") {

$(".vte-task-open").show();

$(".vte-task-completed").show();

} else if (e.target.id == "vte-tasks-view-open") {

$(".vte-task-open").show();

$(".vte-task-completed").hide();

} else if (e.target.id == "vte-tasks-view-closed") {

$(".vte-task-open").hide();

$(".vte-task-completed").show();

}

$("#vte-tasks-view-actions").hide();

});

$(document).on('keyup.hide_actions', function(e) {

if (e.keyCode == 27) {

$("#vte-tasks-view-actions").hide();

$(document).unbind('keyup.hide_actions');

}

});

},

drawTasksSort: function(e) {

e.stopPropagation();

// Draw the Sort window, supports sorting by Created date, priority, title, comments, owner, etc

$("#vte-tasks-view-actions").hide();

if ($("#vte-tasks-sort-actions").length == 0) {

$("#vte-tasks-sort").append(

"

" +

"

Created
" +

"

Priority
" +

"

Title
" +

"

Comments
" +

"

Owner
" +

"

"

);

$("#vte-tasks-sort-actions").css(s_vteDropdownList);

$(".vte-dropdown-item").css(s_vteDropdownItem);

} else {

$("#vte-tasks-sort-actions").show();

}

// Close the menu if clicking outside of it or hitting escape

$("body, .vte-dropdown-item").one("click", function(e) {

e.stopPropagation();

var table = $(".vte-tasks-table");

if (e.target.id == "vte-tasks-sort-created") {

console.log("Sorting created");

var rows = table.find('tr').toArray().sort(vte.comparer(0));

// Determine if we're ascending/descending

$(".vte-tasks-table").data("created", !$(".vte-tasks-table").data("created"));

if (!$(".vte-tasks-table").data("created")) rows = rows.reverse();

for (var i = 0; i < rows.length; i++) { table.append(rows[i]); }

$("#vte-tasks-sort").html("Sort:Created" + action);

} else if (e.target.id == "vte-tasks-sort-priority") {

console.log("Sorting priority");

var rows = table.find('tr').toArray().sort(vte.comparer(1));

$(".vte-tasks-table").data("priority", !$(".vte-tasks-table").data("priority"));

if (!$(".vte-tasks-table").data("priority")) rows = rows.reverse();

for (var i = 0; i < rows.length; i++) { table.append(rows[i]); }

$("#vte-tasks-sort").html("Sort:Priority" + action);

} else if (e.target.id == "vte-tasks-sort-title") {

console.log("Sorting title");

var rows = table.find('tr').toArray().sort(vte.comparer(2));

$(".vte-tasks-table").data("title", !$(".vte-tasks-table").data("title"));

if (!$(".vte-tasks-table").data("title")) rows = rows.reverse();

for (var i = 0; i < rows.length; i++) { table.append(rows[i]); }

$("#vte-tasks-sort").html("Sort:Title" + action);

} else if (e.target.id == "vte-tasks-sort-comments") {

console.log("Sorting comments");

var rows = table.find('tr').toArray().sort(vte.comparer(3));

$(".vte-tasks-table").data("comments", !$(".vte-tasks-table").data("comments"));

if (!$(".vte-tasks-table").data("comments")) rows = rows.reverse();

for (var i = 0; i < rows.length; i++) { table.append(rows[i]); }

$("#vte-tasks-sort").html("Sort:Comments" + action);

} else if (e.target.id == "vte-tasks-sort-owner") {

console.log("Sorting owner");

var rows = table.find('tr').toArray().sort(vte.comparer(4));

$(".vte-tasks-table").data("owner", !$(".vte-tasks-table").data("owner"));

if (!$(".vte-tasks-table").data("owner")) rows = rows.reverse();

for (var i = 0; i < rows.length; i++) { table.append(rows[i]); }

$("#vte-tasks-sort").html("Sort:Owner" + action);

}

$("#vte-tasks-sort-actions").hide();

});

$(document).on('keyup.hide_actions', function(e) {

if (e.keyCode == 27) {

$("#vte-tasks-sort-actions").hide();

$(document).unbind('keyup.hide_actions');

}

});

},

populateChat: function() {

var project = typeof($("#vte-window").data("vte-project")) !== 'undefined' ?

$("#vte-window").data("vte-project").title : "";

// Draw the chat form

$("#vte-window-left-chat").append(

"

" +

"

    " +

    "

    " +

    "

    " +

    " " +

    " " +

    "

    " +

    "

    " +

    "

"

);

// Style the chat window

$(".vte-communication-chat-send").button().css(s_vteCommunicationChatSend);

$("#vte-communication-chat").css(s_vteCommunicationChat);

$("#vte-communication-chat").css("width", $("#vte-window-left-chat").width() + "px");

$("#vte-communication-chat-messages").css("max-height", ($("#vte-window-left-chat").height() / 2) + "px");

$("#vte-communication-chat-input").css(s_vteCommunicationChatInput);

$("#vte-communication-chat-messages").css(s_vteCommunicationChatMessages);

// Load the chat client

$("#vte-communication-chat-form").submit(function() {

if ($("#vte-communication-chat-input").val()) {

vte_sock.emit("chat", {

name: mw.config.get("wgUserName"),

time: new Date(),

project: project,

message: $("#vte-communication-chat-input").val(),

});

}

$("#vte-communication-chat-input").val("");

return false;

});

vte_sock.on("chat", function(obj) {

// TODO: Potentially only show chat messages from users in this project??

$("#vte-communication-chat-messages").append(

"

  • " +

    "

    " + obj.name + ":
    " +

    "

    " + obj.message + "
    " +

    "

  • "

    );

    // Make sure we're scrolled to the bottom

    $("#vte-communication-chat-messages").scrollTop( $("#vte-communication-chat-messages")[0].scrollHeight );

    // Style the message

    $(".vte-communication-chat-line").css(s_vteCommunicationChatLine);

    $(".vte-communication-chat-user").css(s_vteCommunicationChatUser);

    $(".vte-communication-chat-message").css(s_vteCommunicationChatMessage);

    });

    },

    drawCommunication: function(data) {

    var project = $("#vte-window").data("vte-project").title;

    // Clear the current content window and draw the chat form

    $("#vte-window-communication").html("WIP - Communication system");

    },

    // drawTaskEdit: Draws the task edit lightbox. Will prepopulate with task info if an existing

    // task was clicked, otherwise will draw the empty box to create a new task.

    drawTaskEdit: function(index) {

    var obj = $("#vte-window").data("vte-project").tasks;

    var task = {};

    var complete_button = "";

    // If we're given an index, pull out the data for that task

    if (typeof(index) !== 'undefined') {

    task = obj.struc[index];

    complete_button = "";

    }

    // Make sure task has required fields

    if (!("title" in task)) task.title = "";

    if (!("page" in task)) task.page = "";

    if (!("priority" in task)) task.priority = "";

    if (!("remaining" in task)) task.remaining = "";

    if (!("due" in task)) task.due = "";

    if (!("notes" in task)) task.notes = "";

    if (!("owner" in task)) task.owner = "";

    // Draw the lightbox

    $("#vte-window").append(

    "

    " +

    "

    " +

    " " +

    "

    " +

    " " +

    complete_button +

    "

     
    " +

    "

    " +

    "

    " +

    "

    " +

    "

    " +

    "

    " +

    "

    " +

    "

    " +

    "

    " +

    "

    " +

    "

    " +

    "

    " +

    "

    " +

    "

    " +

    "

    Task Title:
    " +

    "

    " +

    "

    " +

    " " +

    "

    " +

    "

    " +

    "

    Related Page:
    " +

    "

    " +

    "

    " +

    " " +

    "

    " +

    "

    " +

    "

    " +

    "

    Priority:
    " +

    " " +

    "

    " +

    "

    Time Remaining:
    " +

    "

    " +

    " " +

    "

    " +

    "

    " +

    "

    Due date (YYYY-mm-dd):
    " +

    "

    " +

    " " +

    "

    " +

    "

    " +

    "

    " +

    "

     
    " +

    "

    " +

    "

    Assigned To:
    " +

    "

    " +

    "

    Sub Tasks:
    " +

    "

    " +

    "

    " +

    "

    " +

    "

    " +

    "

    Comments/Details
    " +

    "

    " +

    " " +

    "

    " +

    "

    " +

    "

    "

    );

    // Style inputs

    var t = setTimeout(function() {

    $("#vte-task-title").width(($("#vte-task-edit").width() - $("#vte-task-page-label").width() - 100) + "px");

    $("#vte-task-page").width(($("#vte-task-edit").width() - $("#vte-task-page-label").width() - 100) + "px");

    }, 50);

    // Add in subtasks, owners, notes, etc, if they exist

    // Owners -

    var owners = "owner" in task ? task.owner.split(",").map( function(str) { return str.trim(); } ) : [];

    for (var i in owners) {

    if (owners[i] == "") continue;

    $(".vte-task-edit-owners").append(

    "

    " +

    " " +

    "

    " + owners[i] + "
    " +

    "

    "

    );

    }

    // And then add the owner's edit field

    $(".vte-task-edit-owners").append(

    "

    " +

    " " +

    "

    " +

    ""

    );

    // And check for any/all subtasks

    var i = 0;

    while ("subtask" + i in task) {

    $(".vte-task-edit-subtasks").append(

    "

    " +

    " " +

    "

    " + task["subtask" + i] + "
    " +

    "

    "

    );

    if ("subcomplete" + i in task && task["subcomplete" + i])

    $("#vte-task-edit-subtask-" + i).prop("checked", true);

    i += 1;

    }

    // And then add the subtasks edit field

    $(".vte-task-edit-subtasks").append(

    "

    " +

    " " +

    "

    " +

    ""

    );

    // Draw the burndown graph (if we have "remaining" updates) or user edit graph (if we have "owners")

    // TODO: This will require getting multiple revisions of the Tasks page

    // Style the box

    $("#vte-task-edit").css(s_vteTaskEdit);

    $(".vte-task-mark-complete").css(s_vteTaskMarkComplete);

    $(".vte-task-save").css(s_vteTaskSave);

    $("#vte-task-close").css(s_vteTaskClose);

    $(".vte-task-edit-label").css(s_vteTaskEditLabel);

    $(".vte-task-edit-input").css(s_vteTaskEditInput);

    $(".vte-task-edit-left").css(s_vteTaskEditLeft);

    $(".vte-task-edit-right").css(s_vteTaskEditRight);

    $(".vte-task-edit-owners").css(s_vteTaskEditOwners);

    $(".vte-task-edit-owner").css(s_vteTaskEditOwner);

    $(".vte-task-edit-remove-owner").css(s_vteTaskEditRemoveOwner);

    $(".vte-task-edit-add-owner").css(s_vteTaskEditAddOwner);

    $(".vte-task-edit-subtasks").css(s_vteTaskEditSubtasks);

    $(".vte-task-edit-add-subtask").css(s_vteTaskEditAddSubtask);

    $(".vte-task-edit-subtask").css(s_vteTaskEditSubtask);

    $(".vte-task-edit-subcomplete").css(s_vteTaskEditSubcomplete);

    $(".vte-task-edit-graph").css(s_vteTaskEditGraph);

    $(".vte-task-edit-notes").css(s_vteTaskEditNotes);

    $(".vte-owner-row").css(s_vteOwnerRow);

    $(".vte-subtask-row").css(s_vteSubtaskRow);

    $("#vte-task-edit input[type='submit']").css("font-size", "10px");

    // All the actions (not using closures so we have access to variables in calling scope -

    // see http://stackoverflow.com/questions/10204420/define-function-within-another-function-in-javascript)

    function addOwner() {

    var index = "owner" in task ? task.owner.split(",").length : 0;

    var $html = $(

    "

    " +

    " " +

    "

    " +

    $("#vte-task-owner").val() +

    "

    "+

    "

    "

    );

    $("#vte-task-owner").val("");

    $("#vte-task-edit-owner-input").before($html);

    $(".vte-task-edit-remove-owner").button().click(removeOwner);

    $(".vte-task-edit-owner").css(s_vteTaskEditOwner);

    $(".vte-task-edit-remove-owner").css(s_vteTaskEditRemoveOwner);

    $(".vte-owner-row").css(s_vteOwnerRow);

    $("#vte-task-edit input[type='submit']").css("font-size", "10px");

    }

    function removeOwner(e) {

    var index = $(e.currentTarget).attr("vte-owner-index");

    task.owner.split(",").splice(index, 1);

    $("[vte-owner-index='" + index + "']").remove();

    }

    function addSubtask() {

    // find the next subtask index

    var index = 0;

    while ("subtask" + index in task) index += 1;

    var $html = $(

    "

    " +

    " " +

    "

    " + $("#vte-task-subtask").val() + "
    " +

    "

    "

    );

    task["subtask" + index] = $("#vte-task-subtask").val();

    $("#vte-task-subtask").val("");

    $("#vte-task-edit-subtask-input").before($html);

    $(".vte-task-edit-subtask").css(s_vteTaskEditSubtask);

    $(".vte-task-edit-subcomplete").css(s_vteTaskEditSubcomplete);

    $(".vte-subtask-row").css(s_vteSubtaskRow);

    $("#vte-task-edit input[type='submit']").css("font-size", "10px");

    }

    // Handle the add owner action

    $(".vte-task-edit-add-owner").button().click(addOwner);

    // Handle the remove owner action

    $(".vte-task-edit-remove-owner").button().click(removeOwner);

    // Handle the add subtask action

    $(".vte-task-edit-add-subtask").button().click(addSubtask);

    // Nothing needed to complete the subtask - we'll check the checkbox for each task on save

    // Handle the mark complete and save actions

    $(".vte-task-mark-complete, .vte-task-save").button().click(function(e) {

    e.preventDefault();

    // Task title is required

    if (! $("#vte-task-title").val()) {

    mw.notify("You must enter a Task Title before saving the task.");

    console.warn("vte: You must enter a Task Title before saving the task.");

    return false;

    }

    var obj = $("#vte-window").data("vte-project").tasks;

    // Update the completed date if we've clicked Mark Complete

    var index = $(e.currentTarget).attr("index");

    if ($(e.currentTarget).attr("value") == "Mark Complete") {

    var d = new Date();

    obj.struc[index].completed = vte.getDateStr();

    }

    // Add the created time if this is a new task

    if (typeof(index) === 'undefined') {

    task.created = vte.getWikiDateStr();

    obj.struc.push(task);

    index = obj.struc.length-1;

    }

    // Update the task object with the other values

    obj.struc[index].title = $("#vte-task-title").val();

    obj.struc[index].page = $("#vte-task-page").val();

    obj.struc[index].priority = $("#vte-task-priority").val();

    obj.struc[index].remaining = $("#vte-task-remaining").val();

    obj.struc[index].due = $("#vte-task-due").val();

    obj.struc[index].notes = $("#vte-task-notes").val();

    var owner = [];

    $(".vte-task-edit-owner").each(function() {

    owner.push($(this).html().trim());

    });

    obj.struc[index].owner = owner.join(",");

    $(".vte-task-edit-subtask").each(function() {

    obj.struc[index]["subtask" + $(this).attr("index")] = $(this).html();

    });

    $(".vte-task-edit-subcomplete").each(function() {

    obj.struc[index]["subcomplete" + $(this).attr("index")] = $(this).prop("checked") == true ? 1 : 0;

    });

    // Save the struc and call the update function for both the tasks page and the tasks talk page

    var vte_project = $("#vte-window").data("vte-project");

    vte_project.tasks = obj;

    vte_project.tasks_talk[ $("#vte-task-title").val() ] = [];

    var complete = {task: 0, talk: 0};

    vte.updateTaskData(function() {

    complete.task = 1;

    console.log("Successfully updated details for task: " + $("#vte-task-title").val());

    mw.notify( "Successfully updated task: " + $("#vte-task-title").val() + "." );

    }, function(xhr) {

    complete.task = 1;

    console.error("Failed to update details for task: " + JSON.stringify(xhr));

    mw.notify( "Failed to update details for task: " + JSON.stringify(xhr));

    });

    vte.updateTaskTalkData(function() {

    complete.talk = 1;

    console.log("Successfully updated talk page for task: " + $("#vte-task-title").val());

    }, function(xhr) {

    complete.talk = 1;

    console.error("Failed to update talk page for task: " + JSON.stringify(xhr));

    });

    var timeout = 0;

    var t1 = setInterval(function() {

    timeout += 100;

    if (complete.task == 1 && complete.talk == 1) {

    clearInterval(t1);

    $("#vte-tasks").click();

    $("#vte-task-edit").remove();

    }

    if (timeout >= 10000) {

    clearInterval(t1);

    console.error("Timed out attempting to save Tasks and Tasks Talk pages: " + JSON.stringify(complete));

    }

    }, 100);

    });

    // Handle the close action

    $("#vte-task-close").click(function() {

    $("#vte-task-edit").remove();

    });

    },

    // drawUserEdits: Will structure and graph user edits over time, separated by namespace

    drawUserEdits: function(data) {

    // Structure the data for the graph

    var sw = ew = vte.convertDateToWikiWeek();

    for (var i in data["result"]) if (data["result"][i].rc_wikiweek < sw) sw = data["result"][i].rc_wikiweek;

    var edits = Array(ew - sw);

    for (var i = 0; i < edits.length; i++) edits[i] = {

    date: vte.convertWikiWeekToDate(i), 0:0, 1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0,

    8:0, 9:0, 10:0, 11:0, 12:0, 13:0, 14:0, 15:0

    };

    var y_max = 0;

    for (var i in data["result"]) {

    edits[ data["result"][i].rc_wikiweek - sw ][ data["result"][i].rc_page_namespace ] +=

    data["result"][i]["rc_edits"];

    if (data["result"][i].rc_edits > y_max) y_max = data["result"][i].rc_edits;

    }

    // Draw with d3

    var margin = {top: 20, right: 80, bottom: 50, left: 50};

    var w = $("#vte-members-contribution").width() - margin.left - margin.right - 20,

    h = 230 - margin.top - margin.bottom;

    var parseDate = d3.time.format("%Y%m%d").parse;

    var x = d3.time.scale()

    .range([0, w]);

    var y = d3.scale.linear()

    .range([h, 0]);

    var color = d3.scale.category10();

    var xAxis = d3.svg.axis()

    .scale(x)

    .orient("bottom");

    var yAxis = d3.svg.axis()

    .scale(y)

    .orient("left");

    var line = d3.svg.line()

    .interpolate("basis")

    .x(function(d) { return x(d.date); })

    .y(function(d) { return y(d.count); });

    //.attr("shape-rendering", "crispEdges");

    var svg = d3.select("#vte-members-contribution-edits").append("svg")

    .attr("width", w + margin.left + margin.right)

    .attr("height", h + margin.top + margin.bottom)

    .append("g")

    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    color.domain(d3.keys(edits[0]).filter( function(key) { return key !== "date"; }));

    edits.forEach(function(d) {

    d.date = parseDate(d.date);

    });

    var namespaces = color.domain().map(function(ns) {

    return {

    namespace: vte.convertIdToNamespace(ns),

    values: edits.map(function(d) {

    return {date: d.date, count: +d[ns]};

    })

    };

    });

    x.domain(d3.extent(edits, function(d) { return d.date; }));

    y.domain([

    d3.min(namespaces, function(c) { return d3.min(c.values, function(v) { return v.count; }); }),

    d3.max(namespaces, function(c) { return d3.max(c.values, function(v) { return v.count; }); })

    ]);

    svg.append("g")

    .style("fill", "none")

    .style("stroke", "#000")

    .style("shape-rendering", "crispEdges")

    .attr("transform", "translate(0," + h + ")")

    .call(xAxis);

    svg.append("g")

    .style("fill", "none")

    .style("stroke", "#000")

    .style("shape-rendering", "crispEdges")

    .call(yAxis)

    .append("text")

    .attr("transform", "rotate(-90)")

    .attr("y", 6)

    .attr("dy", ".70em")

    .style("text-anchor", "end")

    .text("Edits");

    var ns = svg.selectAll(".ns")

    .data(namespaces)

    .enter().append("g")

    .attr("class", "ns");

    ns.append("path")

    .style("fill", "none")

    .style("stroke", "steelblue")

    .style("stroke-width", "1.5px")

    .attr("d", function(d) { return line(d.values); })

    .style("stroke", function(d) { return color(d.namespace); });

    ns.append("text")

    .datum(function(d) {

    // Return null string if the last value was 0 (to avoid overlap)

    if (d.values[d.values.length -1].count == 0) {

    return { namespace: "", value: d.values[d.values.length - 1] };

    } else {

    return { namespace: d.namespace, value: d.values[d.values.length - 1]};

    }

    })

    .attr("transform", function(d) {

    return "translate(" + x(d.value.date) + "," + y(d.value.count) + ")";

    })

    .attr("x", 3)

    .attr("dy", ".35em")

    .text(function(d) { return d.namespace; });

    // And then fix the labels on the axes (needed since the ticks and text are defined at the same time above)

    $("#vte-members-contribution-edits > svg text").css({"stroke": "none", "fill": "#000"});

    },

    //

    // HELPER FUNCTIONS

    //

    // processWikiText - Converts WikiText to valid HTML - This could be done in a call to

    // the MediaWiki API, but that seems like it would be less efficient for the many

    // small cases we would require it for (i.e., making an API request for every Title

    // and Description field for each Task for a given project).

    // Ie, Mediawiki API - https://www.mediawiki.org/wiki/API:Parsing_wikitext

    // Currently using InstaView as Wiky didn't suit what we needed, so this may be unnecessary.

    processWikiText: function(str) {

    },

    // Helper functions to sort table by clicking on the header

    // (see http://stackoverflow.com/questions/3160277/jquery-table-sort)

    comparer: function(index) {

    return function(a, b) {

    var valA = vte.getCellValue(a, index), valB = vte.getCellValue(b, index)

    return $.isNumeric(valA) && $.isNumeric(valB) ? valA - valB : valA.localeCompare(valB)

    }

    },

    getCellValue: function(row, index) {

    return $(row).children('td').eq(index).html();

    },

    // parseDateStr - Given a string, will attempt to parse and create a date object

    parseDateStr: function(str) {

    if (typeof(str) === 'undefined') return new Date();

    var m=null;

    m = str.match(/(\d+):(\d+), (\d+) (\S+) (\d+)/);

    if (m !== null) return new Date(m[5] + "-" + vte.getMonth(m[4]) + "-" + m[3] + " " + m[1] + ":" + m[2] + ":00");

    // Any additional string formats we want to check?

    // Try and parse the string

    var d = new Date(str);

    if (isNaN(d.getTime())) {

    return str;

    } else {

    return d;

    }

    },

    // Checks to see if the supplied argument is a valid date string

    isValidDate: function(d) {

    if ( Object.prototype.toString.call(d) !== "[object Date]" )

    return false;

    return !isNaN(d.getTime());

    },

    // getDateStr - Given a date object, returns a string like YYYY/mm/dd hh:mm:ss. If no date

    // is given will return the string for the current time. If the date object isn't valid,

    // just returns the supplied argument.

    getDateStr: function(d) {

    if (typeof(d) === 'undefined') {

    d = new Date();

    }

    if (! vte.isValidDate(d)) return d;

    return String(d.getFullYear()) + "/" + String(vte.pad( parseInt(d.getMonth()) + 1, 2)) + "/" + String(vte.pad(d.getDate(), 2)) + " " + String(vte.pad(d.getHours(), 2)) + ":" + String(vte.pad(d.getMinutes(), 2)) + ":" + String(vte.pad(d.getSeconds(), 2));

    },

    // getWikiDateStr - Given a date object, returns a wiki-fied date string (the same

    // format that is saved if users enter ~~~~~, ie, "13:15, 14 October 2014 (UTC)")

    getWikiDateStr: function(d) {

    if (typeof(d) === 'undefined') {

    d = new Date();

    }

    return String(vte.pad(d.getUTCHours(), 2)) + ":" + String(vte.pad(d.getUTCMinutes(), 2)) + ", " + String(d.getUTCDate()) + " " + vte.getMonthText(d.getUTCMonth() + 1) + " " + String(d.getUTCFullYear()) + " (UTC)";

    },

    getMonth: function(m) {

    var months = { "January": 1, "February": 2, "March": 3, "April": 4,

    "May": 5, "June": 6, "July": 7, "August": 8, "September": 9,

    "October": 10, "November": 11, "December": 12

    };

    if (! (m in months)) {

    console.error("Invalid month: " + m);

    }

    return months[m];

    },

    getMonthText: function(m, opt) {

    if (typeof opt === 'undefined') opt = {};

    var months = {};

    if ("abbrev" in opt && opt.abbrev) {

    months = {1: "Jan", 2: "Feb", 3: "Mar", 4: "Apr",

    5: "May", 6: "Jun", 7: "Jul", 8: "Aug", 9: "Sept",

    10: "Oct", 11: "Nov", 12: "Dec"

    };

    } else {

    months = {1: "January", 2: "February", 3: "March", 4: "April",

    5: "May", 6: "June", 7: "July", 8: "August", 9: "September",

    10: "October", 11: "November", 12: "December"

    };

    }

    if (! (m in months)) {

    console.error("Invalid month number: " + m);

    }

    return months[m];

    },

    // convertDateToWikiWeek - helper function to convert a date of the form YYYYmmdd to wikiweek

    convertDateToWikiWeek: function(d) {

    if (typeof(d) === 'undefined') {

    var date = new Date();

    d = String(date.getFullYear()) + String(vte.pad( parseInt(date.getMonth()) + 1, 2)) + String(vte.pad(date.getDate(), 2));

    }

    var ms = new Date(d.substring(0,4) + '/' + d.substring(4,6) + '/' + d.substring(6,8) + ' 00:00:00').getTime();

    var originMs = new Date('2001/01/01 00:00:00').getTime();

    var msDiff = ms - originMs;

    // milliseconds in a week

    var week = 7 * 24 * 60 * 60 * 1000;

    // weeks in the millisecond range

    return Math.floor(msDiff / week);

    },

    pad: function(n, width, z) {

    z = z || '0';

    n = n + '';

    return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n;

    },

    convertWikiWeekToDate: function(ww) {

    // milliseconds in wiki weeks

    var ms = ww * 7 * 24 * 60 * 60 * 1000;

    // Add milliseconds since the epoch to week ms value

    var mil = new Date('2001/01/01 00:00:00').getTime() + ms;

    var date = new Date(mil);

    // Date will be of form YYYYmmdd

    return String(date.getFullYear()) + String(vte.pad( parseInt(date.getMonth()) + 1, 2)) + String(vte.pad(date.getDate(), 2));

    },

    convertIdToNamespace: function(id) {

    var ns = {

    0: "Article", 1: "Article talk", 2: "User", 3: "User talk",

    4: "Wikipedia", 5: "Wikipedia talk", 6: "File", 7: "File talk",

    8: "MediaWiki", 9: "MediaWiki talk", 10: "Template", 11: "Template talk",

    12: "Help", 13: "Help talk", 14: "Category", 15: "Category talk",

    100: "Portal", 101: "Portal talk", 108: "Book", 109: "Book talk",

    118: "Draft", 119: "Draft talk"

    };

    return ns[id];

    },

    getNamespaceColor: function(ns) {

    // If the namespace is an int convert it to text

    if (! isNaN(ns)) {

    ns = vte.convertIdToNamespace(ns);

    }

    var un = "#424242";

    var ns_color = {

    "Article": "#CC0000", "Article talk": "#F7B7B7", "User": "#5C8D20", "User talk": "#85ED82",

    "Wikipedia": "#2E97E0", "Wikipedia talk": "#B9E3F9", "File": "#E1711D", "File talk": "#FFC04C",

    "MediaWiki": un, "MediaWiki talk": "#5555FF", "Template": "#55FFFF", "Template talk": "#0000C0",

    "Help": "#008800", "Help talk": "#00C0C0", "Category": "#FFAFAF", "Category talk": "#808080",

    "Portal": "#75A3D1", "Portal talk": "#A679D2", "Book": "#94EF2B", "Book talk": un,

    "Draft": "#99FFFF", "Draft talk": "#99BBFF"

    };

    return ns_color[ns];

    },

    isJson: function(str) {

    try {

    JSON.parse(str);

    } catch (e) {

    return false;

    }

    return true;

    },

    setCookie: function(key, value, options) {

    if (typeof(options) === 'undefined') options = {};

    // Set defaults

    if (! ("expires" in options)) options.expires = 7;

    if (! ("path" in options)) options.path = "/";

    // Then set the cookie

    value = typeof(value) === 'object' ? JSON.stringify(value) : value;

    $.cookie(key, value, options);

    },

    getCookie: function(key) {

    var value = $.cookie(key);

    return vte.isJson(value) ? JSON.parse(value) : value;

    },

    removeCookie: function(key) {

    $.cookie(key, null, { path: '/'});

    },

    setStorage: function(key, value, options) {

    // If the browser doesn't support storage, return null

    if (typeof(Storage) === 'undefined') return null;

    if (typeof(options) === 'undefined') options = {};

    // Set defaults

    if (! ("expires" in options)) options.expires = 7;

    // Convert expires option to seconds from the current time

    options.expires = (new Date().getTime() / 1000) + (options.expires * 60 * 60 * 24);

    // Then set the localStorage, add the options

    var obj = {data: value, options: options};

    localStorage.setItem(key, JSON.stringify(obj));

    },

    getStorage: function(key) {

    // If the browser doesn't support storage, return null

    if (typeof(Storage) === 'undefined') return null;

    var value = localStorage.getItem(key);

    // If the key doesn't exist, return null

    if (value === null) return null;

    value = JSON.parse(value);

    // If the value is expired, return null

    if (value.options.expires < new Date().getTime() / 1000) {

    return null;

    } else {

    return value.data;

    }

    },

    removeStorage: function(key) {

    // If the browser doesn't support storage, return null

    if (typeof(Storage) === 'undefined') return null;

    localStorage.removeItem(key);

    },

    };

    /**** Styles ****/

    var s_wpFont = 'Verdana, "Verdana Ref", Corbel, "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", "DejaVu Sans", "Bitstream Vera Sans", "Liberation Sans", sans-serif';

    var s_vteNavLink = {

    "margin": "5px 0px 0px 10px",

    "cursor": "pointer",

    "color": "#0B0B61"

    };

    var s_vteWindow = {

    "position": "fixed",

    "width": "80%",

    "height": "80%",

    "background-color": "#FFFFFF",

    "top": "50px",

    "left": "10%",

    "box-shadow": "0 1px 6px rgba(0, 0, 0, 0.2)",

    "-moz-box-shadow": "0 1px 6px rgba(0, 0, 0, 0.2)",

    "-webkit-box-shadow": "0 1px 6px rgba(0, 0, 0, 0.2)",

    "z-index": "5"

    };

    var s_vteSummaryInstructions = {

    "font-family": s_wpFont,

    "font-size": "11px",

    "font-style": "italic",

    "padding": "10px",

    "text-align": "center",

    };

    var s_vteActiveProject = {

    "font-family": s_wpFont,

    "font-size": "10px",

    "border": "1px solid #000",

    "border-radius": "10px",

    "-moz-border-radius": "10px",

    "width": "30%",

    "float": "left",

    "background-color": "#EEE",

    "margin": "4px 5px",

    "padding": "3px 5px",

    "cursor": "pointer",

    };

    var s_vteActiveProjectTitle = {

    "font-size": "11px",

    "font-weight": "bold",

    "height": "28px",

    };

    var s_vteActiveProjectLabel = {

    "padding-left": "20px"

    };

    var s_vteActiveProjectValue = {

    "font-style": "italic",

    "padding-left": "10px",

    "color": "#424242",

    };

    var s_vteCloseProject = {

    "font-family": s_wpFont,

    "font-size": "10px",

    "color": "#424242",

    "float": "right",

    "padding": "1px 5px",

    "margin": "-26px 2px 0px 0px",

    };

    var s_vteSortSummaryDiv = {

    "float": "left",

    "font-family": s_wpFont,

    "font-size": "10px",

    "margin": "4px 5px",

    "padding": "3px 5px",

    "width": "20%",

    "text-align": "center",

    "font-color": "#424242",

    };

    var s_vteMemberImport = s_vteTaskEdit = s_vteMembersContribution = {

    "font-family": s_wpFont,

    "font-size": "12px",

    "position": "absolute",

    "top": "5%",

    "left": "5%",

    "width": "80%",

    "height": "80%",

    "background-color": "rgba(255,255,255,.98)",

    "padding": "20px",

    "box-shadow": "0 1px 6px rgba(0, 0, 0, 0.2)",

    "-moz-box-shadow": "0 1px 6px rgba(0, 0, 0, 0.2)",

    "-webkit-box-shadow": "0 1px 6px rgba(0, 0, 0, 0.2)",

    "border-radius": "10px",

    "-moz-border-radius": "10px",

    "overflow-y": "auto",

    };

    var s_vteWindowRightContentTitle = {

    "font-family": s_wpFont,

    "font-size": "13px",

    "padding": "10px 10px 3px 10px",

    "margin": "0px 5px",

    "color": "#0B0B61",

    "float": "right",

    "font-weight": "bold",

    "font-style": "italic",

    "border": "1px solid #5882FA", // was #eee (light gray), now blue

    "border-top-left-radius": "10px",

    "border-top-right-radius": "10px",

    "-moz-border-top-left-radius": "10px",

    "-moz-border-top-right-radius": "10px",

    "cursor": "pointer",

    };

    var s_vteMembersActionsMessage = {

    "font-family": s_wpFont,

    "position": "absolute",

    "top": "100px",

    "left": "25%",

    "background-color": "rgba(255,255,255,.95)",

    "padding": "20px",

    "box-shadow": "0 1px 6px rgba(0, 0, 0, 0.2)",

    "-moz-box-shadow": "0 1px 6px rgba(0, 0, 0, 0.2)",

    "-webkit-box-shadow": "0 1px 6px rgba(0, 0, 0, 0.2)",

    };

    var s_vteMembersActionsDiv = {

    "position": "absolute",

    "background-color": "#F5DA81", // yellow-orange

    "padding": "10px",

    "border-radius": "10px",

    "-moz-border-radius": "10px",

    };

    var s_vteMembersActionsAction = {

    "font-family": s_wpFont,

    "font-size": "10px",

    "color": "#424242",

    "text-align": "left",

    "cursor": "pointer",

    "margin": "3px 0px 3px 5px",

    };

    var s_vteWindowRightContentTasksAdd = s_vteWindowRightContentMembersAdd = {

    "font-family": s_wpFont,

    "font-size": "12px",

    "padding": "10px 0px 20px 0px",

    "color": "#424242",

    };

    var s_vteMembersCreate = {

    "font-family": "'Trebuchet MS', Helvetica, sans-serif",

    "font-size": "16px",

    "margin": "15px 0px 15px 15px",

    "color": "#424242",

    "cursor": "pointer",

    "float": "left",

    };

    var s_vteMembersEmpty = s_vteTasksEmpty = {

    "font-family": s_wpFont,

    "font-size": "12px",

    "padding": "0px 50px",

    "color": "#424242",

    };

    // Communication view styles

    var s_vteCommunicationChatSend = {

    "font-family": s_wpFont,

    "font-size": "10px",

    "padding": "3px",

    "float": "right",

    };

    var s_vteCommunicationChat = {

    "bottom": "5px",

    "position": "absolute",

    };

    var s_vteCommunicationChatInput = {

    "width": "-moz-calc(100% - 4px)", // Firefox

    "width": "-webkit-calc(100% - 4px)", // Webkit

    "width": "-o-calc(100% - 4px)", // Opera

    "width": "calc(100% - 4px)", // Standard

    "margin": "0px 0px 2px 0px",

    "font-family": s_wpFont,

    "font-size": "10px",

    };

    var s_vteCommunicationChatMessages = {

    "list-style-type": "none",

    "margin": "0",

    "padding": "0",

    "overflow-y": "auto",

    };

    var s_vteCommunicationChatLine = {

    "padding": "0px 1px",

    "list-style": "none",

    "font-family": s_wpFont,

    "font-size": "10px",

    };

    var s_vteCommunicationChatUser = {

    "color": "#424242",

    "display": "inline",

    };

    var s_vteCommunicationChatMessage = {

    "display": "inline",

    };

    // Task view styles

    var s_vteTasksCreate = {

    "font-family": "'Trebuchet MS', Helvetica, sans-serif",

    "font-size": "16px",

    "margin": "15px 0px 15px 15px",

    "color": "#424242",

    "cursor": "pointer",

    "float": "left",

    };

    var s_vteMembersView = s_vteTasksView = {

    "font-family": "'Trebuchet MS', Helvetica, sans-serif",

    "font-size": "13px",

    "color": "#424242",

    "cursor": "pointer",

    "display": "inline",

    "margin": "18px 0px 5px 30px",

    "float": "left",

    };

    var s_vteDropdownList = {

    "position": "absolute",

    "background-color": "#EEE",

    "font-family": "'Trebuchet MS', Helvetica, sans-serif",

    "min-width": "85px",

    "border-bottom-left-radius": "5px",

    "border-bottom-right-radius": "5px",

    "-moz-border-bottom-left-radius": "5px",

    "-moz-border-bottom-right-radius": "5px",

    };

    var s_vteDropdownItem = {

    "padding": "2px 4px",

    };

    var s_vteMembersTable = s_vteTasksTable = {

    "font-family": s_wpFont,

    "font-size": "12px",

    "color": "#424242",

    "border-collapse": "collapse",

    "width": "100%",

    };

    var s_vteMembersRow = s_vteTasksRow = {

    "border-bottom": "1px solid #EEE",

    "cursor": "pointer",

    "padding": "3px 0px",

    };

    var s_vteTasksTableTitle = {

    "width": "40%",

    };

    var s_vteTasksTablePriority = s_vteTasksTableCreated = s_vteTasksTableDue = s_vteTasksTableOwner = {

    "text-align": "center",

    "min-width": "50px",

    };

    var s_vteTasksTableComments = {

    "text-align": "center",

    "background-color": "#C9C9C9",

    };

    var s_vteTaskCompleted = {

    "text-decoration": "line-through",

    };

    // End Task view styles

    // Task edit styles

    var s_vteTaskMarkComplete = {

    "float": "right",

    "margin": "0px"

    };

    var s_vteTaskSave = {

    "float": "right",

    "margin": "0px 10px",

    };

    var s_vteTaskClose = {

    "float": "right",

    "padding": "1px",

    "cursor": "pointer",

    };

    var s_vteTaskEditLabel = {

    "display": "inline",

    "font-weight": "bold",

    };

    var s_vteTaskEditInput = {

    "display": "inline",

    };

    var s_vteTaskEditLeft = {

    "width": "45%",

    "float": "left",

    "margin": "10px 0px 10px 20px",

    };

    var s_vteTaskEditRight = {

    "width": "45%",

    "float": "right",

    "margin": "10px 0px 10px 0px",

    };

    var s_vteTaskEditOwners = {

    "color": "#424242",

    "margin-bottom": "20px",

    };

    var s_vteOwnerRow = s_vteSubtaskRow = {

    "padding": "5px 0px",

    };

    var s_vteTaskEditOwner = {

    "display": "inline",

    };

    var s_vteTaskEditRemoveOwner = {

    "display": "inline",

    "padding": "0px 5px",

    "margin": "2px 5px 0px 5px",

    };

    var s_vteTaskEditAddOwner = {

    "display": "inline",

    };

    var s_vteTaskEditSubtasks = {

    "color": "#424242",

    };

    var s_vteTaskEditSubtask = {

    "display": "inline",

    };

    var s_vteTaskEditAddSubtask = {

    "display": "inline",

    };

    var s_vteTaskEditSubcomplete = {

    "display": "inline",

    };

    var s_vteTaskEditGraph = {

    "float": "right",

    "height": "100px",

    "width": "100%",

    "border": "1px solid #000",

    "margin-bottom": "20px",

    };

    var s_vteTaskEditNotes = {

    };

    // End Task edit styles

    var s_vteMembersName = {

    "display": "inline",

    "font-family": s_wpFont,

    "font-size": "12px",

    "color": "#424242",

    "float": "left",

    "width": "20%",

    "padding-top": "5px",

    "border-bottom": "1px solid #EEE",

    };

    var s_vteMembersDate = {

    "display": "inline",

    "font-family": s_wpFont,

    "font-size": "12px",

    "color": "#424242",

    "float": "left",

    "width": "20%",

    "padding-top": "5px",

    "border-bottom": "1px solid #EEE",

    };

    var s_vteMembersComment = {

    "display": "inline",

    "font-family": s_wpFont,

    "font-size": "12px",

    "color": "#424242",

    "float": "left",

    "width": "-moz-calc(60% - 90px)", // Firefox

    "width": "-webkit-calc(60% - 90px)", // Webkit

    "width": "-o-calc(60% - 90px)", // Opera

    "width": "calc(60% - 90px)", // Standard

    "text-align": "center",

    "padding-top": "5px",

    "border-bottom": "1px solid #EEE",

    };

    var s_vteMemberImportLoading = {

    "font-family": s_wpFont,

    "font-size": "11px",

    "padding": "5px 0px 0px 10px",

    "color": "#424242",

    };

    var s_vteTasksAction = s_vteMembersAction = {

    "display": "inline",

    "font-family": s_wpFont,

    "font-size": "12px",

    "color": "#424242",

    "float": "left",

    "width": "80px",

    "text-align": "center",

    "cursor": "pointer",

    "padding-top": "5px",

    "border-bottom": "1px solid #EEE",

    };

    var s_vteTasksComplete = s_vteMembersInactive = {

    "color": "#A4A4A4",

    };

    var s_vteWindowLeft = {

    "font-family": s_wpFont,

    "font-size": "10px",

    "float": "left",

    "padding": "5px 5px 5px 5px",

    "border-right": "1px solid #EEE",

    // Set width as 20% minus padding, borders, etc

    "width": "-moz-calc(20% - 11px)", // Firefox

    "width": "-webkit-calc(20% - 11px)", // Webkit

    "width": "-o-calc(20% - 11px)", // Opera

    "width": "calc(20% - 11px)", // Standard

    // Same as width, height is 100% minus border, padding, etc

    "height": "-moz-calc(100% - 11px)",

    "height": "-webkit-calc(100% - 11px)",

    "height": "-o-calc(100% - 11px)",

    "height": "calc(100% - 11px)",

    };

    var s_vteWindowRight = {

    "float": "right",

    "padding": "5px 5px 5px 5px",

    // Set width as 80% minus padding, borders, etc

    "width": "79%",

    "width": "-moz-calc(80% - 10px)", // Firefox

    "width": "-webkit-calc(80% - 10px)", // Webkit

    "width": "-o-calc(80% - 10px)", // Opera

    "width": "calc(80% - 10px)", // Standard

    // Same as width, height is 100% minus border, padding, etc

    "height": "-moz-calc(100% - 21px)",

    "height": "-webkit-calc(100% - 21px)",

    "height": "-o-calc(100% - 21px)",

    "height": "calc(100% - 21px)",

    };

    var s_vteWindowLeftOnline = {

    "float": "left",

    "width": "100%",

    "height": "15%",

    "background-color": "#F9F9F9"

    };

    var s_vteWindowLeftChat = {

    "float": "left",

    "width": "100%",

    "height": "80%",

    "background-color": "#FFFFFF",

    "font-family": s_wpFont,

    "font-size": "12px",

    "padding": "10px 0px",

    };

    var s_vteWindowRightTitle = {

    "float": "left",

    "width": "100%",

    "min-height": "10px",

    "background-color": "#F9F9F9",

    "border-bottom": "1px solid #EEE"

    };

    var s_vteWindowRightTool = {

    "width": "100%",

    /*

    "background-color": "#F9F9F9",

    "border-bottom": "1px solid #000"

    • /

    };

    var s_vteWindowLoading = {

    "width": "100%",

    "float": "left",

    "text-align": "center",

    "font-family": s_wpFont,

    "font-size": "10px",

    "color": "#6E6E6E",

    "margin-top": "10%",

    };

    var s_vteWindowRightNav = {

    "width": "-moz-calc(100% - 20px)", // Firefox

    "width": "-webkit-calc(100% - 20px)", // Webkit

    "width": "-o-calc(100% - 20px)", // Opera

    "width": "calc(100% - 20px)", // Standard

    "border-bottom": "1px solid #EEE",

    "margin": "0px 10px",

    };

    var s_vteWindowRightContent = {

    "float": "left",

    "width": "100%",

    "height": "88%",

    "background-color": "#FFFFFF",

    "overflow-y": "auto",

    };

    var s_vteWindowRightContentSummary = {

    "font-family": s_wpFont,

    "font-size": "13px",

    };

    var s_vteWindowRightContentSummaryGraph = {

    "font-family": s_wpFont,

    "margin": "8px",

    "border": "1px solid #EEEEEE",

    "height": "112px",

    };

    var s_vteWindowRightContentSummaryPages = {

    "font-family": s_wpFont,

    "margin": "8px",

    "border": "1px solid #EEEEEE",

    };

    var s_vteWindowRightContentSummaryPage = {

    "height": "50px",

    };

    var s_vteWindowRightContentSummaryNew = {

    "font-family": s_wpFont,

    "margin": "8px",

    "border": "1px solid #EEEEEE",

    };

    var s_vteSummaryPageTitle = {

    "font-family": s_wpFont,

    "color": "#0B0080",

    "font-size": "10px",

    "font-style": "italic",

    "border-bottom": "solid 1px #EEE",

    "padding-top": "10px",

    "cursor": "pointer",

    };

    var s_vteMembersContributionEdits = {

    "font-family": s_wpFont,

    "font-size": "10px",

    "margin": "8px",

    "border": "1px solid #EEEEEE",

    "height": "250px",

    };

    var s_vteLoadingText = {

    "font-family": s_wpFont,

    "font-size": "10px",

    "margin-top": "20px",

    "color": "#848484",

    "-webkit-animation-name": "glow",

    "-webkit-animation-duration": "1s",

    "-webkit-animation-iteration-count": "infinite",

    "-webkit-animation-direction": "alternate",

    "-webkit-animation-timing-function": "ease-in-out",

    "-moz-animation-name": "glow",

    "-moz-animation-duration": "1s",

    "-moz-animation-iteration-count": "infinite",

    "-moz-animation-direction": "alternate",

    "-moz-animation-timing-function": "ease-in-out",

    "-o-animation-name": "glow",

    "-o-animation-duration": "1s",

    "-o-animation-iteration-count": "infinite",

    "-o-animation-direction": "alternate",

    "-o-animation-timing-function": "ease-in-out",

    "animation-name": "glow",

    "animation-duration": "1s",

    "animation-iteration-count": "infinite",

    "animation-direction": "alternate",

    "animation-timing-function": "ease-in-out"

    };

    var s_vteTitle = {

    "font-family": s_wpFont,

    "font-size": "20px",

    "font-weight": "normal",

    "float": "left",

    "padding": "5px 0px 5px 10px"

    };

    var s_vteTitleActions = {

    "float": "right",

    "padding": "0px 10px 0px 0px",

    };

    var s_vteTitleAction = {

    "float": "left",

    "font-family": s_wpFont,

    "font-size": "12px",

    "cursor": "pointer",

    "padding": "5px 0px 0px 5px",

    };

    var s_vteProjectSelectLabel = {

    "font-family": s_wpFont,

    "font-size": "12px",

    "padding": "5px 0px 0px 7px",

    };

    var s_vteProjectSelectInput = {

    "margin": "5px 0px",

    "height": "1.4em",

    "background-color": "transparent",

    "color": "#000",

    "outline": "none",

    "font-family": s_wpFont,

    "font-size": "12px",

    "padding": "2px 5px",

    "width": "-moz-calc(100% - 12px)", // Firefox

    "width": "-webkit-calc(100% - 12px)", // Webkit

    "width": "-o-calc(100% - 12px)", // Opera

    "width": "calc(100% - 12px)", // Standard

    };

    var s_vteProjectSelectMulti = {

    "position": "absolute",

    "width": "50%",

    "max-height": "20%",

    "overflow-y": "auto",

    "margin-top": "-4px",

    "padding": "7px 10px",

    "background-color": "#EEE",

    "border-bottom-left-radius": "10px",

    "border-bottom-right-radius": "10px",

    "-moz-border-bottom-left-radius": "10px",

    "-moz-border-bottom-right-radius": "10px",

    "border": "1px solid #000",

    "z-index": "2",

    };

    var s_vteProjectSelectMultiProj = {

    "font-family": s_wpFont,

    "font-size": "12px",

    "cursor": "pointer",

    "padding-left": "1em",

    "text-indent": "-1em",

    "padding-bottom": "4px",

    };

    // Only load vte on the Wikipedia namespace (may limit to project pages later)

    window.onload = function() {

    //if (mw.config.get('wgNamespaceNumber') === '4') {

    console.log("Loading VTE");

    mw.loader.using( ["mediawiki.api"], function() {

    $(vte.initialize);

    // And create the websocket

    var t = setInterval(function() {

    if (typeof(io) !== 'undefined') {

    clearInterval(t);

    vte_sock = io(data_api, {secure: true});

    // Emit vte initialize

    vte_sock.emit("vte_init", {

    name: mw.config.get("wgUserName"),

    time: new Date(),

    page: mw.config.get("wgTitle"),

    namespace: mw.config.get("wgNamespaceNumber"),

    });

    // And handle updates to who's online

    vte_sock.on("online", function(obj) {

    if ($("#vte-window-left-online-num").length > 0) {

    $("#vte-window-left-online-num").html(Object.keys(obj).length);

    }

    });

    }

    }, 100);

    });

    //}

    }

    //