Jump to content

User:Md gilbert/vte.js

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Md gilbert (talk | contribs) at 00:12, 18 November 2014 (Updating project explorer to separate project edits (edits in the Wikipedia and Wikipedia_Talk namespace), and allow sorting on them.). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
//<nowiki> - this prevents double left braces being misinterpreted by the MediaWiki parser

// global variables, as required
var vte_sock = true;

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 = 'https://alahele.ischool.uw.edu:8997/js/d3.min.js';
    head.appendChild(script);
    script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = 'https://alahele.ischool.uw.edu:8997/js/socket.io-1.2.0.js';
    head.appendChild(script);

    // Create the VTE button
    var $btn = $(
      "<div class='vectorMenu' id='p-vte'>" +
      "  <h3><span>VTE</span></h3>" +
      "</div>"
    ).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.renderOverlay();
    });
  }, // 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 = $(
      "<div id='vte-window'>" +
      "  <div id='vte-window-left'>" +
      "    <div id='vte-window-left-online'>" +
      "      Online users: <span id='vte-window-left-online-num'></span>" +
      "    </div>" +
      "    <div id='vte-window-left-chat' />" +
      "  </div>" +
      "  <div id='vte-window-right'> " +
      "    <div id='vte-window-right-title' />" +
      "    <div id='vte-window-right-tool' />" +
      "    <div id='vte-window-right-content' />" +
      "  </div>" +
      "  <div style='clear: both;'></div>" +
      "</div>"
    );
    // 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-content").css(s_vteWindowRightContent);
    // Fill in vte elements
    vte.populateTitle();
    vte.populateTool();
    vte.populateProject();
    vte.populateNav();
    vte.populateContent();
    vte.drawChat();
  },

  // populateTitle - draws title bar content
  populateTitle: function() {
    var $vteTitle = $(
      "<div id='vte-title'>Virtual Team Explorer</div>" +
      "<div id='vte-title-actions'>" +
      "  <div id='vte-title-action-user' title='View user information'>" +
      "    <img src='/media/wikipedia/commons/0/0a/Gnome-stock_person.svg' width='15' height='15' style='padding: 5px;'/>" +
      "  </div>" +
      "  <div id='vte-title-action-settings' title='View VTE settings'>" +
      "    <img src='/media/wikipedia/commons/7/77/Gear_icon.svg' width='25' height='25'/>" +
      "  </div>" +
      "  <div id='vte-title-action-close' title='Close the VTE'>" +
      "    <img src='/media/wikipedia/commons/6/60/Close_icon.svg' width='25' height='25'/>" +
      "  </div>" +
      "</div>"
    );
    // 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-window").hide();
    });
  },

  // populateTool - draws tool bar content
  populateTool: function(tool) {

  },

  // populateProject - draws the project browser content
  populateProject: function() {
    // Fetch all projects or load from previous request
    // TODO: allow cookies before deploying
    var projects = []; // $.cookie( 'vte-projects' );
    if (projects.length > 0) {
      vte.drawProjectSelect(projects);
    } else {
      // Add search box and main nav
      var $projectSelect = $(
        "<input id='vte-project-select-input' type='text' placeholder='Loading WikiProject data...'/>" +
        "<div id='vte-project-select-multi' />"
      );
      // Add it
      $("#vte-window-right-tool").html($projectSelect);
      // Style it
      $("#vte-project-select-label").css(s_vteProjectSelectLabel);
      $("#vte-project-select-input").css(s_vteProjectSelectInput);

      // Make the request
      //var url = 'http://tools.wmflabs.org/catscan2/catscan2.php?depth=10&categories=Active_WikiProjects&ns%5B4%5D=1&doit=1&format=json';
      //var url = 'http://tools.wmflabs.org/catscan2/catscan2.php?depth=2&categories=Active_WikiProjects&ns%5B4%5D=1&topcats_no_talk=1&format=json&doit=1';
      var url = 'https://alahele.ischool.uw.edu:8997/api/getProjects';
      $.ajax({
        url: url,
        dataType: "json",
        success: function(data, stat, xhr) {
          //var projects = data["*"][0]["*"];
          var projects = data["result"];
          // TODO: Allow cookies before deploying
          /*
          $.cookie('vte-projects', projects, {
            expires: 7, // expire in 7 days
            path: '/'   // domain-wide, entire wiki
          });
          */
          vte.drawProjectSelect(projects);
        },
        error: function(xhr, stat, err) {
          console.error("Failed to request project data from API: " + JSON.stringify(xhr));
          $("#vte-window-left-project").html("Failed to request project data from API: " + JSON.stringify(xhr));
        },
        complete: function() {
          $("#vte-project-loading").remove();
        },
      });
    }
  },

  // drawProjectSelect - called by populateProject after successfully requesting project data. 
  // Draws project select box
  drawProjectSelect: function(projects) {
    // Remove loading placeholder
    $("#vte-project-select-input").attr("placeholder", "Enter a WikiProject to explore");
    // Add the actions (everytime there's a key-up, update list of visible projects)
    $("#vte-project-select-input").on("keyup", function() {
      // FIRST, update the list of active projects
      $(".vte-active-project").each(function(i, v) {
        var project = $(v).attr("p_title").replace(/ /g, "_").toLowerCase();
        var input = $("#vte-project-select-input").val().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
      // Empty the project selection div
      $("#vte-project-select-multi").html("");
      // Then for each project add to the dropdown if it matches the input text
      $.each(projects, function(i,v) {
        // Make sure the div exists
        if ($("#vte-project-select-multi").length === 0) {
          $("#vte-project-select").after("<div id='vte-project-select-multi' />");
        }
        $("#vte-project-select-multi").show();
        var project = projects[i]['p_title'].replace(/_/g, " ").toLowerCase();
        var input = $("#vte-project-select-input").val().replace(/_/, " ").toLowerCase();
        if (project.indexOf(input) != -1) {
          $("#vte-project-select-multi").append(
            "<div class='vte-project-select-multi-proj' 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, " ") + 
            "</div>"
          );
        }
      });
      if ($("#vte-project-select-multi").html() == "") {
        $("#vte-project-select-multi").append(
          "<div class='vte-project-select-multi-proj' style='color: #848484;'>No matching projects found</div>"
        );
      }
      // Style the container and projects
      $("#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 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();
        $("#vte-project-select-input").val($(evt.currentTarget).html());
        // Load the project summary
        console.log("loading summary for project " + title + ", id: " + id);
        $("#vte-window").data("vte-project", title);
        $("#vte-window").data("vte-project-attr", {
          title: title,
          id: id,
          created: created,
        });
        vte.populateNav();
        vte.drawProjectSummary();
      });
    });
  },

  // drawProjectSummary - draws summary information for the project once it is selected
  //   from the vte-project-select-multi dropdown (or clicked on)
  drawProjectSummary: function() {
    var title, id, created;
    title = $("#vte-window").data("vte-project-attr").title;
    id = $("#vte-window").data("vte-project-attr").id;
    created = $("#vte-window").data("vte-project-attr").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"),
    });

    // Add the close icon
    $("#vte-project-select-input").after("<input type='submit' class='vte-close-project' value='X'/>");
    $(".vte-close-project").css(s_vteCloseProject);
    $(".vte-close-project").button().click(function() {
      $("#vte-window").data("vte-project", false);
      $("#vte-window").data("vte-project-attr", 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.populateContent();
      vte.populateNav();
    });

    // Clear any existing data in the content window and add summary divs
    $("#vte-window-right-content").html(
      "<div id='vte-window-right-content-summary'>" +
      "  <div id='vte-window-right-content-summary-title' />" + 
      "  <div id='vte-window-right-content-summary-p-edits'>" +
      "    Edits to Project (blue) and Project Talk (grey) pages" +
      "    <div style='height: 10px; width: 100%; border-bottom: 1px solid #000;'></div>" +
      "    <div id='vte-loading-edits' class='vte-loading'>Loading project edit data...</div>" +
      "    <div id='vte-project-summary-graph' style='height:80px' />" +
      "  </div>" +
      "  <div id='vte-window-right-content-summary-pages'>" +
      "    Most active articles in the last 30 days (showing the last year)" +
      "    <div style='height: 10px; width: 100%; border-bottom: 1px solid #000;'></div>" +
      "    <div id='vte-loading-pages' class='vte-loading'>Loading revision history for project pages...</div>" +
      "  </div>" +
      "</div>"
    );
    $("#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);

    // Request summary data from our backend
    var t = title.replace(/ /g, "_");
    var sd = created.substr(0, 8);
    var sw = vte.convertDateToWikiWeek(sd);
    var url = "https://alahele.ischool.uw.edu:8997/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 = "https://alahele.ischool.uw.edu:8997/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();
        url = "https://alahele.ischool.uw.edu:8997/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(
                "<div class='vte-summary-page-title'>" + pages[id][0].tp_title.replace(/_/g," ") + "</div>" +
                "<div class='vte-window-right-content-summary-page' " +
                "  id='vte-window-right-content-summary-page-" + id + "' />"
              );
              $("#vte-window-right-content-summary-page-" + id).css(s_vteWindowRightContentSummaryPage);
              vte.drawProjectEdits({result: pages[id]}, sw, "vte-window-right-content-summary-page-" + id);
            }
            $(".vte-summary-page-title").css(s_vteSummaryPageTitle);
          },
          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));
      },
    });
  },

  // drawProjectPages - draws a summary of pages in a project
  drawProjectPages: function(data) {
    // Structure the pages for vis
    var pages = Array(16);
    for (var i = 0; i < pages.length; i++) pages[i] = { "namespace": i, "count": 0 };
    for (var p in data["result"]) {
      for (var i in data["result"][p]) {
        if (data["result"][p][i].tp_namespace > 15) continue;
        pages[ data["result"][p][i].tp_namespace ].count += 1;
      }
    }
    // D3 bar graph
    var margin = {top: 10, right: 20, bottom: 30, left: 60},
        w = $("#vte-window-right-content").width() - 30 - margin.right - margin.left,
        h = 100 - margin.top - margin.bottom;
    var x = d3.scale.ordinal()
        .rangeRoundBands([0, w], .1);
    var y = d3.scale.linear()
        .range([h, 0]);
    var xAxis = d3.svg.axis()
        .scale(x)
        .orient("bottom")
        .ticks(16, "");
    var yAxis = d3.svg.axis()
        .scale(y)
        .orient("left")
        .ticks(4, "");
    var vis = d3.select("#vte-window-right-content-summary-pages")
        .append("svg:svg")
        .attr("width", w + margin.left + margin.right)
        .attr("height", h + margin.top + margin.bottom)
      .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    x.domain(pages.map(function(d) { return d.namespace; }));
    y.domain([0, d3.max(pages, function(d) { return d.count; })]);
    vis.append("g")
        .attr("transform", "translate(0," + h + ")")
        .call(xAxis)
      .append("text")
        .attr("y", h-10)
        .attr("x", w / 2)
        .attr("dy", ".71em")
        .style("text-anchor", "center")
        .text("Namespace");
    vis.append("g")
        .call(yAxis)
      .append("text")
        .attr("transform", "rotate(-90)")
        .attr("y", 6)
        .attr("dy", ".71em")
        .style("text-anchor", "end")
        .text("# Pages");

    vis.selectAll(".bar")
        .data(pages)
      .enter().append("rect")
        .attr("x", function(d) { return x(d.namespace); })
        .attr("width", x.rangeBand())
        .attr("y", function(d) { return y(d.count); })
        .attr("height", function(d) { return h - y(d.count); });
  },

  // 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() - 1;
    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
    var w = $("#vte-window-right-content").width() - 30,
        h = $("#" + div_id).height();
        //h = 80;
    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")) {
      $("#vte-window-right-tool").append(
        "<div class='vte-content-nav' id='vte-communication'>Communication</div>" +
        "<div class='vte-content-nav' id='vte-tasks'>Tasks</div>" +
        "<div class='vte-content-nav' id='vte-members'>Members</div>" +
        "<div class='vte-content-nav' id='vte-summary'>Summary</div>" +
        "<div class='vte-content-nav-spacer' style='clear: both;'/>"
      );
      // 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-content-nav").css("color", "#0B0B61");
          $("#vte-summary").css("color", "#000");
          vte.drawProjectSummary();
        } else if (id == "vte-members") {
          $(".vte-content-nav").css("color", "#0B0B61");
          $("#vte-members").css("color", "#000");
          vte.clickMembers();
        } else if (id == "vte-tasks") {
          $(".vte-content-nav").css("color", "#0B0B61");
          $("#vte-tasks").css("color", "#000");
          vte.clickTasks();
        } else if (id == "vte-communication") {
          $(".vte-content-nav").css("color", "#0B0B61");
          $("#vte-communication").css("color", "#000");
          vte.clickCommunication();
        } else {
          console.error("Unknown vte action: " + id);
        }
      });
    } else {
      $(".vte-content-nav").remove();
    }
  },

  // Functions to populate the primary vte systems (ie, members, tasks, etc)
  clickMembers: function() {
    // 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"),
      view: "Members"
    });

    console.log("vte - drawing member content");
    // Load the Members page for the current project (should be Wikipedia:<project name>/Members
    var project = $("#vte-window").data("vte-project");
    $.getJSON(
      mw.util.wikiScript('api'),
      {
        format: 'json',
        action: 'query',
        prop: 'revisions',
        rvprop: 'content',
        rvlimit: 1,
        titles: 'Wikipedia:' + project + "/Members",
      }
    )
    .done(function(data) {
      var page, text;
      try {
        // This should only return one page...
        for ( page in data.query.pages) {
          text = data.query.pages[page].revisions[0]['*'];
        }
      } catch( e ) {
        console.error("Failed to request members page for project " + project);
      }
      vte.drawMembers(text);
    })
    .fail(function() {
      console.error("Failed to request members page for project " + project);
    });

  },
  clickTasks: function() {
    // 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"),
      view: "Tasks"
    });

    console.log("vte - drawing task content");
    // Load the Tasks page for the current project (should be Wikipedia:<project name>/Tasks
    var project = $("#vte-window").data("vte-project");
    $.getJSON(
      mw.util.wikiScript('api'),
      {
        format: "json",
        action: "query",
        prop: "revisions",
        rvprop: "content",
        rvlimit: 1,
        titles: "Wikipedia:" + project + "/Tasks",
      }
    )
    .done(function(data) {
      var page, text;
      try {
        // This should only return one page
        for (page in data.query.pages) {
          text = data.query.pages[page].revisions[0]['*'];
        }
      } catch(e) {
        console.error("Failed to request tasks page for project " + project);
      }
      vte.drawTasks(text);
    })
    .fail(function() {
      console.error("Failed to request tasks page for project " + project);
    });
  },
  clickCommunication: function() {
    // 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"),
      view: "Communication"
    });

    console.log("vte - drawing communication content");
    var project = $("#vte-window").data("vte-project");

    // TODO: Load wiki communication
    

    vte.drawCommunication();
  },

  drawMembers: function(data) {
    // Clear the current content window
    $("#vte-window-right-content").html("");

    // Grab current project users from the Members page text
    var obj = vte.parseTable(data, "Members", "printMembersTable");

    // Projects have the option to include anything in the members table, but for
    // the VTE we'll want to display the name, member since, and project roles.
    // Possible column names given in [[Module:Members]] (although these aren't enforced).
    var c_map = {};
    for (var i in obj.cols) {
      if (obj.cols[i].slice(0,4) == "name") { c_map["name"] = i; }
      else if (obj.cols[i] == "member_since") { c_map["member_since"] = i; }
      else if (obj.cols[i] == "project_roles") { c_map["project_roles"] = i; }
      else if (obj.cols[i] == "member_status") { c_map["member_status"] = i; }
    }

    // Once we've parsed all the users from the Members page, draw the VTE members content
    var roles_input = "project_roles" in c_map ? 
      " Project Role: <input type='text' id='vte-members-add-comment' />" : "";
    $("#vte-window-right-content").append(
      "<div id='vte-window-right-content-members-add'>" +
      "  <form id='vte-member-submit' action='javascript();'>" +
      "    Add Member: <input type='text' id='vte-members-add-name' />" +
      roles_input +
      "    <input type='submit' value='Add' />" +
      "  </form>" +
      "</div>"
    );
    $("#vte-window-right-content-members-add").css(s_vteWindowRightContentMembersAdd);
    // Display existing users (header on top, only show columns that were saved on the project's Members page)
    $("#vte-window-right-content").append(
      "<div class='vte-members-actions' style='font-weight: bold; text-align: center;'>Actions</div>"
    );
    if ("name" in c_map) {
      $("#vte-window-right-content").append(
        "<div class='vte-members-name' style='font-weight: bold; text-align: center;'>User name</div>"
      );
    }
    if ("member_since" in c_map) {
      $("#vte-window-right-content").append(
        "<div class='vte-members-date' style='font-weight: bold; text-align: center;'>Member since</div>"
      );
    }
    if ("project_roles" in c_map) {
      $("#vte-window-right-content").append(
        "<div class='vte-members-comment' style='font-weight: bold; text-align: center;'>Project roles</div>"
      );
    }
    $("#vte-window-right-content").append("<div style='clear: both;'/>");

    var action = 
      "<svg height='10' width='10'>" +
      "  <polygon points='2,3 8,3 5,8' style='fill:black;stroke:black;stroke-width:1' />" +
      "  Sorry, your browser does not support inline SVG." +
      "</svg>";

    for (var i in obj.struc) {
      // Same as above, try to display name, member_since, and project_roles. Skip values that aren't there.
      var n = "name" in c_map ? obj.struc[i][c_map["name"]] : "";
      var d = "member_since" in c_map ? obj.struc[i][c_map["member_since"]] : "";
      var r = "project_roles" in c_map ? obj.struc[i][c_map["project_roles"]] : "";
      if (! n) n = "&nbsp;";
      if (! d) d = "&nbsp;";
      if (! r) r = "&nbsp;";
      if (d != "&nbsp;") {
        var m = d.match(/(\d+):(\d+), (\d+) (\S+) (\d+)/);
        var date = m === null ? "" : new Date(m[5] + "-" + vte.getMonth(m[4]) + "-" + m[3] + " " + m[1] + ":" + m[2] + ":00 UTC");
        if (date) d = vte.getDateStr(date);
      }
      // Inactive members will be grey
      var inactive = "";
      if ("member_status" in c_map && obj.struc[i][c_map["member_status"]] == "Inactive")
        inactive = " vte-members-inactive ";
      // Action icon is displayed first
      $("#vte-window-right-content").append(
        "<div class='vte-members-actions' vte-name='" + n + "' " + "  id='action_" + n + "'>" + action + "</div>"
      );
      if ("name" in c_map) {
        $("#vte-window-right-content").append(
          "<div id='vte-members-name-" + n + "' class='vte-members-name " + inactive + "'>" + n + "</div>"
        );
      }
      if ("member_since" in c_map) {
        $("#vte-window-right-content").append(
          "<div id='vte-members-date-" + n + "' class='vte-members-date " + inactive + "'>" + d + "</div>" 
        );
      }
      if ("project_roles" in c_map) {
        $("#vte-window-right-content").append(
          "<div id='vte-members-comment-" + n + "' class='vte-members-comment " + inactive + "'>" + r + "</div>"
        );
      }
      $("#vte-window-right-content").append("<div style='clear: both;' />");
      // Add action to open Actions menu on click
      $("#action_" + n).click(vte.clickMemberActions);
    }
    $(".vte-members-name").css(s_vteMembersName);
    $(".vte-members-date").css(s_vteMembersDate);
    $(".vte-members-comment").css(s_vteMembersComment);
    $(".vte-members-actions").css(s_vteMembersAction);
    $(".vte-members-inactive").css(s_vteMembersInactive);


    // Action to add new member
    $("#vte-member-submit").submit(function(e) {
      e.preventDefault();
      console.log("Adding user to members list");
      // TODO: Verify that the user actually exists, potentially?
      var d = new Date();
      var n = $("#vte-members-add-name").val();
      var member = [];
      if("name" in c_map) member[ c_map["name"] ] = $("#vte-members-add-name").val();
      if("project_roles" in c_map) member[ c_map["project_roles"] ] = $("#vte-members-add-comment").val();
      if("member_since" in c_map) member[ c_map["member_since"] ] = vte.getWikiDateStr(d);
      if("member_status" in c_map) member[ c_map["member_status"] ] = "Active";
      var obj = $("#vte-window").data("Members");
      obj.struc.push(member)

      // And update the local members object
      $("#vte-window").data("Members", obj);

      // Build the member string
      var members_str = obj.pre + "{{#invoke:" + obj.mod + "|" + obj.func + "|cols=" + obj.cols.join(",") + "\n";
      for (var i in obj.struc) {
        members_str += "|" + obj.struc[i].join("|");
        members_str += parseInt(i)+1 == obj.struc.length ? "}}" : "\n";
      }
      members_str += obj.post;

      // Make the request to update the wikitext for the Members page
      $.ajax({
        url: mw.util.wikiScript( 'api' ),
        type: 'POST',
        dataType: 'json',
        data: {
          format: 'json',
          action: 'edit',
          title: "Wikipedia:" + $("#vte-window").data("vte-project") + "/Members",
          text: members_str, // will replace entire page content
          summary: "[VTE] Adding member to project: " + $("#vte-members-add-name").val(),
          token: mw.user.tokens.get( 'editToken' )
        }
      })
      .done( function(data) {
        // 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"),
          view: "Members"
        });

        // Add the user to the members list
        console.log("Added member: " + n);
        $("#vte-window-right-content").append(
          "<div class='vte-members-actions' vte-name='" + n + "' id='action_" + n + "'>" + action + "</div>"
        );
        if ("name" in c_map) {
          $("#vte-window-right-content").append(
            "<div id='vte-members-name-" + n + "' class='vte-members-name'>" + n + "</div>"
          );
        }
        if ("member_since" in c_map) {
          $("#vte-window-right-content").append(
            "<div id='vte-members-date-" + n + "' class='vte-members-date'>" + vte.getDateStr(d) + "</div>"
          );
        }
        if ("project_roles" in c_map) {
          var roles = $("#vte-members-add-comment").val() ? $("#vte-members-add-comment").val() : "&nbsp;";
          $("#vte-window-right-content").append(
            "<div id='vte-members-comment-" + n + "' class='vte-members-comment'>" + roles + "</div>"
          );
        }
        $("#vte-window-right-content").append("<div style='clear: both;'/>");
        // Add action to open Actions menu on click
        $("#action_" + n).click(vte.clickMemberActions);
        $(".vte-members-name").css(s_vteMembersName);
        $(".vte-members-date").css(s_vteMembersDate);
        $(".vte-members-comment").css(s_vteMembersComment);
        $(".vte-members-actions").css(s_vteMembersAction);
        // And clear out the member inputs
        $("#vte-members-add-name").val("");
        $("#vte-members-add-comment").val("");
      })
      .fail( function(xhr) {
        console.error("Failed to update Members page: " + JSON.stringify(xhr));
      });
    });
  },

  parseTable: function(data, mod, func) {
    // Parse out the module text, plus everything before and after it
    var re = new RegExp("([\\s\\S]*)(\\{\\{#invoke:" + mod + "\\|" + func + "\\|[^\\}]+)\\}\\}([\\s\\S]*)");
    var m = data.match(re);
    if (m === null || m.length != 4) {
      console.error("Failed to find module invocation ({#invoke:" + mod + "|" + func + ")");
      console.error("Page contains: " + data);
      return false;
    }
    var pre = m[1];
    var post = m[3];
    // Break apart the module, grabbing columns first and then parsing arguments
    var m1 = m[2].match(/\|cols=([^\|]+)\|/);
    if (m1 === null) {
      console.error("Failed to find column declaration in module invocation: " + m[2]);
      return false;
    }
    var cols = m1[1].split(",");
    cols = cols.map( function(c) { return c.trim(); } );
    // Grab the displayed_cols argument, if it exists
    var m2 = m[2].match(/\|displayed_cols=([^\|]+)\|/);
    var displayed_cols = [];
    if (m2 !== null) {
      displayed_cols = m2[1].split(",");
    }
    displayed_cols = displayed_cols.map( function(c) { return c.trim(); } );
    // Then handle the rest of the arguments
    var args = m[2].split("|");
    // Remove the Module and function names
    args = args.slice(2);
    // Then store arguments in a 2d array representing table values
    var struc = [];
    var row = [];
    for (var i in args) {
      args[i] = args[i].trim();
      if (args[i].slice(0,5) == "cols=" || args[i].slice(0,15) == "displayed_cols=") continue
      row.push(args[i]);
      if (row.length == cols.length) {
        struc.push(row);
        row = [];
      }
    }
    var obj = {
      pre: pre,
      post: post,
      mod: mod,
      func: func,
      cols: cols,
      displayed_cols: displayed_cols,
      struc: struc,
    };
    // Save the data for later
    $("#vte-window").data(mod, obj);
    return obj;
  },

  clickMemberActions: function(e) {
    var name = $(e.currentTarget).attr("vte-name");
    console.log("opening actions for user " + name);
    $(".vte-members-action-div").remove();

    // Draw the actions window, should support posting to user talk page, viewing contributions, and removing
    var obj = $("#vte-window").data("Members");
    var roles_action = obj.cols.indexOf("project_roles") != -1 ? 
      "<div id='vte-action-role' class='vte-members-actions-action' vte-name='" + name + "'>Edit user roles</div>" :
      "";
    // Get the status of the current user
    var name_i = 0;
    var stat = "";
    var inactive_action = "";
    if (obj.cols.indexOf("member_status") != -1) {
      for (var i in obj.cols) { if (obj.cols[i].slice(0,4) == "name") name_i = i; }
      for (var i in obj.struc) {
        if (obj.struc[i][name_i] == name)
          stat = obj.struc[i][ obj.cols.indexOf("member_status") ] == "Active" ? "inactive" : "active";
      }
      inactive_action =
        "<div id='vte-action-remove' class='vte-members-actions-action' vte-name='" + name + "'>Set "+stat+"</div>";
    }
    $("#vte-window").append(
      "<div class='vte-members-actions-div'>" +
      "  <div id='vte-action-message' class='vte-members-actions-action' vte-name='" + name + "'>Post to user talk</div>" +
         roles_action +
      "  <div id='vte-action-user' class='vte-members-actions-action' vte-name='" + name + "'>View user contributions</div>" +
         inactive_action +
      "</div>"
    );
    $(".vte-members-actions-div").css(s_vteMembersActionsDiv);
    $(".vte-members-actions-action").css(s_vteMembersActionsAction);
    // Position the div by the cursor, relative to parent, considering the scroll
    var x = (e.pageX - $('#vte-window').offset().left) - 150;
    var y = (e.pageY - $('#vte-window').offset().top) + 10;
    $(".vte-members-actions-div").css({ "left": x + "px", "top": y + "px" });

    // Close the actions window on hitting escape or clicking outside the actions window
    var opening = true;
    var t = setTimeout(function() { opening = false; }, 200);
    $(document).on('keyup.hide_member_actions', function(e) {
      if (opening == false) {
        if (e.keyCode == 27) {
          console.log("Closing actions menu on escape.");
          $(".vte-members-actions-div").remove();
          $(document).unbind('keyup.hide_member_actions');
        }
      }
    });
    $(document).on('click.hide_member_actions', function(e) {
      if (opening == false) {
        console.log("Closing actions menu on click.");
        $(".vte-members-actions-div").remove();
        $(document).unbind('click.hide_member_actions');
      }
    });

    // Add the post to user talk page action
    $("#vte-action-message").on("click", function(e) {
      // Remove the actions div
      console.log("Removing the action div box");
      $(".vte-members-actions-div").remove();
      // Open the message box, should contain space for subject and the message
      var name = $(e.currentTarget).attr("vte-name");
      $("#vte-window-right-content").append(
        "<div class='vte-members-actions-message'>" +
        "  <form id='vte-member-message' action='javascript();'>" +
        "    <div class='vte-members-actions-message-subject'>" +
        "      Subject: <input type='text' id='vte-message-subject' />" +
        "    </div>" +
        "    <div class='vte-members-actions-message-text'>" +
        "      Message: <textarea rows='10' cols='50' id='vte-message-text' />" +
        "    </div>" +
        "    <input type='submit' value='Send' />" +
        "  </form>" +
        "</div>"
      );
      $(".vte-members-actions-message").css(s_vteMembersActionsMessage);
      // Close the message window on hitting escape or (TODO) clicking the close button
      $(document).on('keyup.hide_member_message', function(e) {
        if (e.keyCode == 27) {
          $(".vte-members-actions-message").remove();
          $(document).unbind('keyup.hide_member_message');
        }
      });

      // Add action to post to the talk page
      $("#vte-member-message").submit(function(e) {
        e.preventDefault();
        console.log("Posting a message to user talk page: " + name);
        // Pull the text from the message box
        var text = "\n== " + $("#vte-message-subject").val() + " ==\n\n" + $("#vte-message-text").val() + "\n";
        // Ensure the post is signed
        if (text.match(/\~\~\~\~/) == null) text += "~~~~\n";
        $.ajax({
          url: mw.util.wikiScript( 'api' ),
          type: 'POST',
          dataType: 'json',
          data: {
            format: 'json',
            action: 'edit',
            title: "User_talk:" + name,
            appendtext: text,
            summary: "[VTE] Posting directed talk page message - " + $("#vte-message-subject").val(),
            token: mw.user.tokens.get( 'editToken' )
          }
        })
        .done( function(data) {
          mw.notify( "Successfully posted to user's Talk page." );
          console.log("Successfully posted to user's talk page");
          $(".vte-members-actions-message").remove();
        })
        .fail( function(xhr) {
          console.error("Failed to post to user talk page: " + JSON.stringify(xhr));
        });
      });

    }); // End post to talk page vte-action-message click

    // Add the update user role action
    $("#vte-action-role").on("click", function(e) {
      // Remove the actions div
      console.log("Removing the action div box");
      $(".vte-members-actions-div").remove();
      // Open the role update dialogue, prepopulate with this member's current roles
      var name = $(e.currentTarget).attr("vte-name");
      var v = $("#vte-members-comment-" + name).html();
      $("#vte-window-right-content").append(
        "<div class='vte-members-actions-role'>" +
        "  <form id='vte-member-role' action='javascript();'>" +
        "    <div class='vte-role'>" +
        "      Project roles: <input type='text' id='vte-roles-input' value='" + v + "' />" +
        "    </div>" +
        "    <input type='submit' value='Update role' />" +
        "  </form>" +
        "</div>"
      );
      $(".vte-members-actions-role").css(s_vteMembersActionsMessage); // TODO: Could clean this up

      // Close the message window on hitting escape or TODO: clicking outside the window
      var t = setTimeout(function() {
        $(document).on('keyup.hide_role', function(e) {
          if (e.keyCode == 27) {
            $(".vte-members-actions-role").remove();
            $(document).unbind('keyup.hide_role');
          }
        });
/*
        $(document).on('click.hide_role', function(e) {
          $(".vte-members-actions-role").remove();
          $(document).unbind('click.hide_role');
        });
*/
      }, 100);

      $("#vte-member-role").submit(function(e) {
        e.preventDefault();
        // Update the Members page first, then the VTE if we're successful. 
        var obj = $("#vte-window").data("Members");
        var members_str = obj.pre + "{{#invoke:" + obj.mod + "|" + obj.func + "|cols=" + obj.cols.join(",") + "\n";
        var name_i = 0;
        for (var i in obj.cols) { if (obj.cols[i].slice(0,4) == "name") name_i = i; break; }
        for (var i in obj.struc) {
          if (obj.struc[i][name_i] == name) 
            obj.struc[i][ obj.cols.indexOf("project_roles") ] = $("#vte-roles-input").val();
          members_str += "|" + obj.struc[i].join("|");
          members_str += parseInt(i)+1 == obj.struc.length ? "}}" : "\n";
        }
        members_str += obj.post;
        // Make the request to update the wikitext for the Members page
        $.ajax({
          url: mw.util.wikiScript( 'api' ),
          type: 'POST',
          dataType: 'json',
          data: {
            format: 'json',
            action: 'edit',
            title: "Wikipedia:" + $("#vte-window").data("vte-project") + "/Members",
            text: members_str, // will replace entire page content
            summary: "[VTE] Updating roles for user " + name +" in project: "+ $("#vte-window").data("vte-project"),
            token: mw.user.tokens.get( 'editToken' )
          }
         })
        .done( function(data) {
          // 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"),
            view: "Members"
          });

          console.log("Successfully updated member roles for " + name);
          // Just update the roles for the current user
          $("#vte-members-comment-" + name).html($("#vte-roles-input").val());
          // And remove the roles dialogue
          $(".vte-members-actions-role").remove();
          // And save the members struc
          $("#vte-window").data("Members", obj);
        })
        .fail( function(xhr) {
          console.error("Failed to update member roles for " + name + ": " + JSON.stringify(xhr));
        });
      });
    }); // End vte-action-role click

    // Add the view user contributions action
    $("#vte-action-user").on("click", function(e) {
      e.preventDefault();
      // Draw the lightbox that user stats will be placed within
      var name = $(e.currentTarget).attr("vte-name");
      var alt_name = name.replace(/_/g, " ");
      $("#vte-window").append(
        "<div id='vte-members-contribution'>" +
        "  <div id='vte-members-contribution-title'>" + name + " - User Contributions</div>" +
        "  <div id='vte-members-contribution-close'>" +
        "    <img src='/media/wikipedia/commons/6/60/Close_icon.svg' width='25' height='25'>"+
        "  </div>" +
        "  <div style='clear: both;'/>" +
        "  <div id='vte-members-contribution-edits'>" +
        "    Weely edits by namespace over time" +
        "    <div style='height: 10px; width: 100%; border-bottom: 1px solid #000;'></div>" +
      "      <div id='vte-loading-edits' class='vte-loading'>Loading user edits...</div>" +
        "  </div>" +
        "</div>"
      );
      $("#vte-members-contribution").css(s_vteMembersContribution);
      $("#vte-members-contribution-title").css({"float": "left"});
      $("#vte-members-contribution-close").css({"float": "right", "cursor": "pointer"});
      $(".vte-loading").css(s_vteLoadingText);
      $("#vte-members-contribution-edits").css(s_vteMembersContributionEdits);

      // Add action to close the window
      $("#vte-members-contribution-close").click(function(e) {
        $("#vte-members-contribution").remove();
      });

      // Add line graph of user edits, separated by namespace
      var url = "https://alahele.ischool.uw.edu:8997/api/getEdits?user=" + alt_name + "&namespace=0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15&group=page|user|date";
      $.ajax({
        url: url,
        dataType: "json",
        success: function(data, stat, xhr) {
          if (data.errostatus == "fail") {
            console.error("Error: " + data.message);
            return false;
          }
          console.log("Sucessfully fetched user edits.");
          $("#vte-loading-edits").remove();
          vte.drawUserEdits(data);
        },
        error: function(xhr, stat, err) {
          console.error("Failed to request user edits: " + JSON.stringify(xhr));
        },
        complete: function() {
          $("#vte-loading-edits").remove();
        },
      });
    }); // End view user contributions vte-action-user click

    // Add the set user inactive action
    $("#vte-action-remove").on("click", function(e) {
      e.preventDefault();
      // Update the Members page first, then the VTE if we're successful. 
      var name = $(e.currentTarget).attr("vte-name");
      var obj = $("#vte-window").data("Members");
      var members_str = obj.pre + "{{#invoke:" + obj.mod + "|" + obj.func + "|cols=" + obj.cols.join(",") + "\n";
      var name_i = 0;
      var stat = "";
      for (var i in obj.cols) { if (obj.cols[i].slice(0,4) == "name") name_i = i; break; }
      for (var i in obj.struc) {
        // If this is the user we're removing, update status and continue
        stat = obj.struc[i][ obj.cols.indexOf("member_status") ] == "Active" ? "Inactive" : "Active";
        if (obj.struc[i][name_i] == name)
          obj.struc[i][ obj.cols.indexOf("member_status") ] = stat;
        members_str += "|" + obj.struc[i].join("|");
        members_str += parseInt(i)+1 == obj.struc.length ? "}}" : "\n";
      }
      members_str += obj.post;
      // Make the request to update the wikitext for the Members page
      $.ajax({
        url: mw.util.wikiScript( 'api' ),
        type: 'POST',
        dataType: 'json',
        data: {
          format: 'json',
          action: 'edit',
          title: "Wikipedia:" + $("#vte-window").data("vte-project") + "/Members",
          text: members_str, // will replace entire page content
          summary: "[VTE] Removing user " + name +" from project members for  project: "+ $("#vte-window").data("vte-project"),
          token: mw.user.tokens.get( 'editToken' )
        }
      })
      .done( function(data) {
        // 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"),
          view: "Members",
          type: "status"
        });

        console.log("Successfully set project member as " + stat + ": " + name);
        // Then update the display of the user
        if (stat == "Inactive") {
          $("#vte-members-name-" + name).css(s_vteMembersInactive);
          $("#vte-members-date-" + name).css(s_vteMembersInactive);
          $("#vte-members-comment-" + name).css(s_vteMembersInactive);
        } else {
          $("#vte-members-name-" + name).css(s_vteMembersName);
          $("#vte-members-date-" + name).css(s_vteMembersDate);
          $("#vte-members-comment-" + name).css(s_vteMembersComment);
        }
        // And save the new version of the members struc
        $("#vte-window").data("Members", obj);
      })
      .fail( function(xhr) {
        console.error("Failed to remove project member - " + name + ": " + JSON.stringify(xhr));
      });
    }); // End vte-action-remove click

  },
  drawTasks: function(data) {
    console.log("in drawTasks");
    // Clear the current content window
    $("#vte-window-right-content").html("");

    // Grab current task list from the Tasks page text
    var obj = vte.parseTable(data, "TaskList", "printTasksTable");

    // 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
    // Possible column names given in [[Module:TaskList]] (although those aren't enforced).
    var c_map = {};
    for (var i in obj.cols) {
      if (obj.cols[i] == "title") { c_map["title"] = i; }
      else if (obj.cols[i] == "notes") { c_map["notes"] = i; }
      else if (obj.cols[i] == "page") { c_map["page"] = i; }
      else if (obj.cols[i] == "created") { c_map["created"] = i; }
      else if (obj.cols[i] == "due") { c_map["due"] = i; }
      else if (obj.cols[i] == "priority") { c_map["priority"] = i; }
      else if (obj.cols[i] == "owner") { c_map["owner"] = i; }
      else if (obj.cols[i] == "completed") { c_map["completed"] = i; }
      else if (obj.cols[i] == "remaining") { c_map["remaining"] = i; }
      else if (obj.cols[i] == "progress") { c_map["progress"] = i; }
      else if (obj.cols[i] == "subtask") { c_map[ obj.cols[i] ] = i; }
      else if (obj.cols[i] == "subcomplete") { c_map[ obj.cols[i] ] = i; }
      else { console.warn("Unknown column: " + obj.cols[i]); }
    }
    // Make sure we have the required columns defined (title, priority, owner, created, due, etc)
    // - These will be added to the Tasks page module invocation by the VTE if they don't exist.
    //   What gets displayed won't change since that depends on the project's invocation of
    //   the module with the displayed_cols argument.
    if (! ("title" in c_map)) { 
      obj.cols.push("title"); c_map["title"] = obj.cols.length-1; 
      // Also add empty values in the tasks struc
      for (var j in obj.struc) { obj.struc[j][obj.cols.length-1] = ""; }
    }
    if (! ("priority" in c_map)) { 
      obj.cols.push("priority"); c_map["priority"] = obj.cols.length-1; 
      for (var j in obj.struc) { obj.struc[j][obj.cols.length-1] = ""; }
    }
    if (! ("page" in c_map)) {
      obj.cols.push("page"); c_map["page"] = obj.cols.length-1;
      for (var j in obj.struc) { obj.struc[j][obj.cols.length-1] = ""; }
    }
    if (! ("owner" in c_map)) { 
      obj.cols.push("owner"); c_map["owner"] = obj.cols.length-1; 
      for (var j in obj.struc) { obj.struc[j][obj.cols.length-1] = ""; }
    }
    if (! ("created" in c_map)) { 
      obj.cols.push("created"); c_map["created"] = obj.cols.length-1; 
      for (var j in obj.struc) { obj.struc[j][obj.cols.length-1] = ""; }
    }
    if (! ("due" in c_map)) { 
      obj.cols.push("due"); c_map["due"] = obj.cols.length-1; 
      for (var j in obj.struc) { obj.struc[j][obj.cols.length-1] = ""; }
    }
    if (! ("remaining" in c_map)) {
      obj.cols.push("remaining"); c_map["remaining"] = obj.cols.length-1;
      for (var j in obj.struc) { obj.struc[j][obj.cols.length-1] = ""; }
    }
    if (! ("completed" in c_map)) {
      obj.cols.push("completed"); c_map["completed"] = obj.cols.length-1;
      for (var j in obj.struc) { obj.struc[j][obj.cols.length-1] = ""; }
    }
    if (! ("notes" in c_map)) {
      obj.cols.push("notes"); c_map["notes"] = obj.cols.length-1;
      for (var j in obj.struc) { obj.struc[j][obj.cols.length-1] = ""; }
    }
    if (! ("subtask" in c_map)) {
      obj.cols.push("subtask"); c_map["subtask"] = obj.cols.length-1;
      for (var j in obj.struc) { obj.struc[j][obj.cols.length-1] = ""; }
    }
    if (! ("subcomplete" in c_map)) {
      obj.cols.push("subcomplete"); c_map["subcomplete"] = obj.cols.length-1;
      for (var j in obj.struc) { obj.struc[j][obj.cols.length-1] = ""; }
    }
    // If we do have subtasks, make sure subcomplete matches the number of subtasks for each task and
    // ensure the format is either 0 for undefined or 0 and 1 otherwise (ie, non-zero or null values
    // indicate the task was completed)
    for (var j in obj.struc) {
      var n_sc = [];
      var sc = obj.struc[j][c_map["subcomplete"]].split(",");
      var st = obj.struc[j][c_map["subtask"]].split(",");
      for (var k in st) {
        if (typeof(sc[k]) === 'undefined' || sc[k] == 0 || sc[k] == "") {
          n_sc[k] = 0;
        } else {
          n_sc[k] = 1;
        }
      }
      obj.struc[j][c_map["subcomplete"]] = n_sc.join(",");
    }
    // And then update the tasks struc
    $("#vte-window").data("TaskList", obj);
    $("#vte-window").data("TaskList-c_map", c_map);

    // Once we've parsed all the tasks from the Tasks page, draw the VTE Tasks content
    // Add the create task button first
    $("#vte-window-right-content").append(
      "<input type='submit' class='vte-tasks-create' value='Create New Task' />"
    );

    // Display existing open tasks (header on top, only show columns that were saved on the project's Tasks page)
    $("#vte-window-right-content").append(
      "<table class='vte-open-tasks-table' cellpadding='0'>" +
      "  <colgroup>" +
      "    <col class='vte-open-tasks-table-title'>" +
      "    <col class='vte-open-tasks-table-priority'>" +
      "    <col class='vte-open-tasks-table-owners'>" +
      "    <col class='vte-open-tasks-table-created'>" +
      "    <col class='vte-open-tasks-table-due'>" +
      "  </colgroup>" +
      "  <thead>" +
      "    <tr class='vte-open-tasks-row-head'>" +
      "      <th class='oh vte-tasks-head-title'>Title</th>" +
      "      <th class='oh vte-tasks-head-priority'>Pri</th>" +
      "      <th class='oh vte-tasks-head-owner'>Owners</th>" + 
      "      <th class='oh vte-tasks-head-created'>Created</th>" +
      "      <th class='oh vte-tasks-head-due'>Due</th>" +
      "    </tr>" +
      "  </thead>" +
      "  <tbody class='vte-open-tasks-table-body'/>" +
      "</table>"
    );
    // Also display completed tasks (this table will be removed if no tasks have been completed)
    $("#vte-window-right-content").append(
      "<table class='vte-closed-tasks-table' cellpadding='0'>" +
      "  <colgroup>" +
      "    <col class='vte-closed-tasks-table-title'>" +
      "    <col class='vte-closed-tasks-table-priority'>" +
      "    <col class='vte-closed-tasks-table-owners'>" +
      "    <col class='vte-closed-tasks-table-created'>" +
      "    <col class='vte-closed-tasks-table-completed'>" +
      "  </colgroup>" +
      "  <thead>" +
      "    <tr class='vte-closed-tasks-row-head'>" +
      "      <th class='ch vte-tasks-head-title'>Title</th>" +
      "      <th class='ch vte-tasks-head-priority'>Pri</th>" +
      "      <th class='ch vte-tasks-head-owner'>Owners</th>" +
      "      <th class='ch vte-tasks-head-created'>Created</th>" +
      "      <th class='ch vte-tasks-head-due'>Due</th>" +
      "    </tr>" +
      "  </thead>" +
      "  <tbody class='vte-closed-tasks-table-body' />" +
      "</table>"
    );

    var closed = 0;
    //var link_reg = /\[\[([^\|\]]+).*?\]\]/g;
    var link_reg = /\[\[:?([^\|\]]+)\|?(.*?)\]\]/g;
    for (var i in obj.struc) {
      var t = obj.struc[i][c_map["title"]];
      var c = obj.struc[i][c_map["created"]];
      var d = obj.struc[i][c_map["due"]];
      var p = obj.struc[i][c_map["priority"]];
      var o = obj.struc[i][c_map["owner"]];
      // Attempt to parse the creation date
      if (c) {
        var m = c.match(/(\d+):(\d+), (\d+) (\S+) (\d+)/);
        var date = m === null ? "" : new Date(m[5] + "-" + vte.getMonth(m[4]) + "-" + m[3] + " " + m[1] + ":" + m[2] + ":00 UTC");
        if (date) c = vte.getDateStr(date);
      }
      // Attempt to parse the due date
      if (d) {
        var m = d.match(/^(\d{4})-(\d{2})-(\d{2})$/);
        var date = m === null ? "" : new Date(d);
        if (date) d = vte.getDateStr(date);
      }
      // Attempt to parse wiki links in the title
      t = t.replace(link_reg, function(i, m) {
        return "<a href='http://" + mw.config.get("wgContentLanguage") + ".wikipedia.org/wiki/" + m + "'>" + m + "</a>";
      });

      // Title is displayed first, then priority, owner, created, and due
      if (! obj.struc[i][c_map["completed"]]) {
        $(".vte-open-tasks-table-body").append(
          "<tr id='task-" + i + "' class='vte-open-tasks-row' vte-task-index='" + i + "'>" +
          "  <td id='vte-open-tasks-table-title-" + i + "' class='vte-open-tasks-table-title vte-t-td'>" + t + "</td>" +
          "  <td id='vte-open-tasks-table-priority-" + i + "' class='vte-open-tasks-table-priority vte-t-td'>" + p + "</td>" +
          "  <td id='vte-open-tasks-table-owner-" + i + "' class='vte-open-tasks-table-owner vte-t-td'>" + o + "</td>" +
          "  <td id='vte-open-tasks-table-created-" + i + "' class='vte-open-tasks-table-created vte-t-td'>" + c + "</td>" +
          "  <td id='vte-open-tasks-table-due-" + i + "' class='vte-open-tasks-table-due vte-t-td'>" + d + "</td>" +
          "</tr>"
        );
      } else {
        closed = 1;
        $(".vte-closed-tasks-table-body").append(
          "<tr id='task-" + i + "' class='vte-closed-tasks-row' vte-task-index='" + i + "'>" +
          "  <td id='vte-closed-tasks-table-title-" + i + "' class='vte-closed-tasks-table-title vte-t-td'>" + t + "</td>" +
          "  <td id='vte-closed-tasks-table-priority-" + i + "' class='vte-closed-tasks-table-priority vte-t-td'>" + p + "</td>" +
          "  <td id='vte-closed-tasks-table-owner-" + i + "' class='vte-closed-tasks-table-owner vte-t-td'>" + o + "</td>" +
          "  <td id='vte-closed-tasks-table-created-" + i + "' class='vte-closed-tasks-table-created vte-t-td'>" + c + "</td>" +
          "  <td id='vte-closed-tasks-table-due-" + i + "' class='vte-closed-tasks-table-due vte-t-td'>" + d + "</td>" +
          "</tr>"
        );
      }

    }
    // If not tasks have been closed, remove the closed tasks table
    if (closed == 0) $(".vte-closed-tasks-table").remove();
    
    // Style the tables
    $(".vte-open-tasks-table, .vte-closed-tasks-table").css(s_vteTasksTable);
    $(".vte-t-td").css(s_vteTasksRow);
    $(".vte-open-tasks-table-title, .vte-closed-tasks-table-title").css(s_vteTasksTableTitle);
    $(".vte-open-tasks-table-priority, .vte-closed-tasks-table-priority").css(s_vteTasksTablePriority);
    $(".vte-open-tasks-table-created, .vte-closed-tasks-table-created").css(s_vteTasksTableCreated);
    $(".vte-open-tasks-table-due, .vte-closed-tasks-table-due").css(s_vteTasksTableDue);
    $(".oh, .ch").css({ "cursor": "pointer", "padding": "4px 0px" });
    $(".vte-tasks-create").css(s_vteTasksCreate);

    // Action to highlight row on hover
    $(".vte-closed-tasks-row, .vte-open-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) {
      e.preventDefault();
      // Draw the lightbox
      vte.drawTaskEdit();
    }); // END submit new task

    // Action to edit an existing task
    $(".vte-open-tasks-row, .vte-closed-tasks-row").click(function(e) {
      var index = $(e.currentTarget).attr("vte-task-index");
      vte.drawTaskEdit(index);
    });
  }, 
  drawChat: function() {
    var project = $("#vte-window").data("vte-project");
    // Draw the chat form
    $("#vte-window-left-chat").append(
      "<div id='vte-communication-chat'>" +
      "  <ul id='vte-communication-chat-messages' />" +
      "  <div id='vte-communication-chat-form'>" +
      "    <form id='vte-communication-chat-form' action=''>" +
      "      <input id='vte-communication-chat-input' autocomplete='off'/>" +
      "      <input type='submit' class='vte-communication-chat-send' value='Send' />" +
      "    </form>" +
      "  </div>" +
      "</div>"
    );

    // 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: $("#vte-window").data("vte-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(
        "<li class='vte-communication-chat-line'>" +
        "  <div class='vte-communication-chat-user'>" + obj.name + ":</div>" +
        "  <div class='vte-communication-chat-message'>" + obj.message + "</div>" +
        "</li>"
      );
      // 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");

    // Clear the current content window and draw the chat form
    $("#vte-window-right-content").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("TaskList");
    var c_map = $("#vte-window").data("TaskList-c_map");
    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 = "<input type='submit' class='vte-task-mark-complete' value='Mark Complete' />";
    } else { for (var i = 0; i < Object.keys(c_map).length; i++) task[i] = ""; }
    // Draw the lightbox
    $("#vte-window").append(
      "<div id='vte-task-edit'>" +
      "  <div id='vte-task-close'>" +
      "    <img src='/media/wikipedia/commons/6/60/Close_icon.svg' width='25' height='25'>" +
      "  </div>" +
      "  <input type='submit' class='vte-task-save' value='Save' />" +
      complete_button +
      "  <div style='margin-top: 10px; clear: both;'>&nbsp;</div>" +
      "  <table style=''>" +
      "    <tr>" +
      "      <td> " +
      "        <div id='vte-task-title-label' class='vte-task-edit-label'>Task Title: </div>" +
      "      </td>" +
      "      <td>" +
      "        <div class='vte-task-edit-input'>" +
      "          <input type='text' id='vte-task-title' value='" + task[c_map["title"]] + "'/>" +
      "        </div>" +
      "      </td>" +
      "    </tr>" +
      "    <tr>" +
      "      <td>" +
      "        <div id='vte-task-page-label' class='vte-task-edit-label'>Related Page: </div>" +
      "      </td>" +
      "      <td>" +
      "        <div class='vte-task-edit-input'>" +
      "          <input type='text' id='vte-task-page' value='" + task[c_map["page"]] + "'/>" +
      "        </div>" +
      "      </td>" +
      "    </tr>" +
      "    <tr>" +
      "      <td colspan=2>" +
      "        <table><tr><td style='width:33%;'>" +
      "          <div id='vte-task-priority-label' class='vte-task-edit-label'>Priority: </div>" +
      "          <select id='vte-task-priority'>" +
      "            <option value='0' " + (task[c_map["priority"]] == 0 ? "SELECTED" : "") + ">0 (most urgent)</option>" +
      "            <option value='1' " + (task[c_map["priority"]] == 1 ? "SELECTED" : "") + ">1</option>" +
      "            <option value='2' " + (task[c_map["priority"]] == 2 ? "SELECTED" : "") + ">2</option>" +
      "            <option value='3' " + (task[c_map["priority"]] == 3 ? "SELECTED" : "") + ">3</option>" +
      "            <option value='4' " + (task[c_map["priority"]] == 4 ? "SELECTED" : "") + ">4 (least urgent)</option>" +
      "          </select>" +
      "        </td><td style='width:33%;'>" +
      "          <div id='vte-task-remaining-label' class='vte-task-edit-label'>Time Remaining: </div>" +
      "          <div class='vte-task-edit-input'>" +
      "            <input type='text' id='vte-task-remaining' value='" + task[c_map["remaining"]] + "'/>" +
      "          </div>" +
      "        </td><td style='width;33%;'>" +
      "          <div id='vte-task-due-label' class='vte-task-edit-label'>Due date (YYYY-mm-dd): </div>" +
      "          <div class='vte-task-edit-input'>" +
      "            <input type='text' id='vte-task-due' value='" + task[c_map["due"]] + "'/>" +
      "          </div>" +
      "        </td></tr></table>" +
      "      </td>" +
      "    </tr>" +
      "  </table>" +
      "  <div style='clear: both;'>&nbsp;</div>" +
      "  <div class='vte-task-edit-left'>" +
      "    <div class='vte-task-edit-label' style='display: block;'>Assigned To:</div>" +
      "    <div class='vte-task-edit-owners' />" +
      "    <div class='vte-task-edit-label' style='display: block; margin-top: 20px;'>Sub Tasks:</div>" +
      "    <div class='vte-task-edit-subtasks' />" +
      "  </div>" + 
      "  <div class='vte-task-edit-right'>" +
      "    <div class='vte-task-edit-graph' />" +
      "    <div class='vte-task-edit-label' style='display: block;'>Comments/Details</div>" +
      "    <div class='vte-task-edit-notes'>" +
      "      <textarea id='vte-task-notes' rows='5' cols='40'>" + task[c_map["notes"]] + "</textarea>" +
      "    </div>" +
      "  </div>" +
      "</div>"
    );
    // 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 = task[c_map["owner"]].split(",");
    for (var i in owners) {
      if (owners[i] == "") continue;
      $(".vte-task-edit-owners").append(
        "<div class='vte-owner-row'>" +
        "  <input type='submit' vte-owner-index='" + i + "' class='vte-task-edit-remove-owner' value='-' />" +
        "  <div class='vte-task-edit-owner' vte-owner-index='" + i + "'>" + owners[i] + "</div>" +
        "</div>"
      );
    }
    // And then add the owner's edit field
    $(".vte-task-edit-owners").append(
      "<div class='vte-task-edit-input' id='vte-task-edit-owner-input'>" +
      "  <input type='text' id='vte-task-owner' value='' />" +
      "</div>" +
      "<input type='submit' class='vte-task-edit-add-owner' value='Add' />"
    );

    // Subtasks -
    var subtasks = task[c_map["subtask"]].split(",");
    var subcompletes = task[c_map["subcomplete"]].split(",");
    for (var i in subtasks) {
      if (subtasks[i] == "") continue;
      $(".vte-task-edit-subtasks").append(
        "<div class='vte-subtask-row'>" +
        "  <input type='checkbox' vte-subtask-index='" + i + "' class='vte-task-edit-subcomplete' />" +
        "  <div class='vte-task-edit-subtask'>" + subtasks[i] + "</div>" +
        "</div>"
      );
      if (subcompletes[i] == 1) $("#vte-task-edit-subtask-" + i).prop("checked", true);
    }
    // And then add the subtasks edit field
    $(".vte-task-edit-subtasks").append(
      "<div class='vte-task-edit-input' id='vte-task-edit-subtask-input'>" +
      "  <input type='text' id='vte-task-subtask' value='' />" +
      "</div>" +
      "<input type='submit' class='vte-task-edit-add-subtask' value='+' />"
    );

    // 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 = task[c_map["owner"]].split(",").length;
      var $html = $(
        "<div class='vte-owner-row' vte-owner-index='" + index + "'>" +
        "  <input type='submit' vte-owner-index='" + index + "' class='vte-task-edit-remove-owner' value='-'/>" +
        "  <div class='vte-task-edit-owner' vte-owner-index='" + index + "'>" + $("#vte-task-owner").val() + "</div>"+
        "</div>"
      );
      $("#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[c_map["owner"]].split(",").splice(index, 1);
      $("[vte-owner-index='" + index + "']").remove();
    }
    function addSubtask() {
      var index = task[c_map["subtask"]].split(",").length;
      var $html = $(
        "<div class='vte-subtask-row' vte-subtask-index='" + index + "'>" +
        "  <input type='checkbox' vte-subtask-index='" + index + "' class='vte-task-edit-subcomplete' />" +
        "  <div class='vte-task-edit-subtask'>" + $("#vte-task-subtask").val() + "</div>" +
        "</div>"
      );
      $("#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 save action
    $(".vte-task-save").button().click(function(e) {

    });
    // Handle the mark complete action
    $(".vte-task-mark-complete, .vte-task-save").button().click(function(e) {
      e.preventDefault();
      var obj = $("#vte-window").data("TaskList");
      var c_map = $("#vte-window").data("TaskList-c_map");

      // Update the completed date if we've clicked Mark Complete
      if ($(e.currentTarget).attr("value") == "Mark Complete") {
        var d = new Date();
        obj.struc[index][c_map["completed"]] = String(d.getFullYear()) + "-" + String(vte.pad( parseInt(d.getMonth()) + 1, 2)) + "/" + String(vte.pad(d.getDate(), 2));
      }
      // Build the module invocation string
      var tasks_str = obj.pre + "{{#invoke:" + obj.mod + "|" + obj.func + "|cols=" + obj.cols.join(",") + "\n";
      if (obj.displayed_cols.length > 0) {
        tasks_str += "|displayed_cols=" + obj.displayed_cols.join(",") + "\n";
      }
      // If this is a new task, add it to the obj.struc
      if (typeof(index) === 'undefined') {
        task[c_map["created"]] = "~~~~~";
        obj.struc.push(task);
        index = obj.struc.length-1;
      }
      for (var i in obj.struc) {
        if (i == index) {
          obj.struc[i][c_map["title"]] = $("#vte-task-title").val();
          obj.struc[i][c_map["page"]] = $("#vte-task-page").val();
          obj.struc[i][c_map["priority"]] = $("#vte-task-priority").val();
          obj.struc[i][c_map["remaining"]] = $("#vte-task-remaining").val();
          obj.struc[i][c_map["due"]] = $("#vte-task-due").val();
          var owner = [];
          $(".vte-task-edit-owner").each(function() {
            owner.push($(this).html());
          });
          obj.struc[i][c_map["owner"]] = owner.join(",");
          var subtask = [];
          $(".vte-task-edit-subtask").each(function() {
            subtask.push($(this).html());
          });
          obj.struc[i][c_map["subtask"]] = subtask.join(",");
          var subcomplete = [];
          $(".vte-task-edit-subcomplete").each(function() {
            subcomplete.push( $(this).prop("checked") == true ? 1 : 0 );
          });
          obj.struc[i][c_map["subcomplete"]] = subcomplete.join(",");
          obj.struc[i][c_map["notes"]] = $("#vte-task-notes").val();
        }
        tasks_str += "|" + obj.struc[i].join("|");
        tasks_str += parseInt(i) + 1 == obj.struc.length ? "}}" : "\n";
      }
      tasks_str += obj.post;

      // Make the request to update the text for the Tasks page
      $.ajax({
        url: mw.util.wikiScript( 'api' ),
        type: 'POST',
        dataType: 'json',
        data: {
          format: 'json',
          action: 'edit',
          title: "Wikipedia:" + $("#vte-window").data("vte-project") + "/Tasks",
          text: tasks_str, // will replace entire page content
          summary: "[VTE] Updating details for task: " + $("#vte-task-title").val(),
          token: mw.user.tokens.get( 'editToken' )
        }
      })
      .done( function(data) {
        // 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"),
          view: "Tasks",
        });

        console.log("Successfully updated details for task: " + $("#vte-task-title").val());
        mw.notify( "Successfully updated task: " + $("#vte-task-title").val() + "." );
        $("#vte-tasks").click();
        $("#vte-task-edit").remove();
      })
      .fail( function(xhr) {
        console.error("Failed to update details for task: " + JSON.stringify(xhr));
        mw.notify( "Failed to update details for task: " + JSON.stringify(xhr));
      });
    });
    // 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"});
  },

  // populateContent - draws the main content when no projects are selected (project browse)
  populateContent: function() {
    // Loading text
    $("#vte-window-right-content").html(
      "<div class='vte-loading'>Loading project content...</div>"
    );
    $(".vte-loading").css(s_vteLoadingText);
    // Request the most active projects
    var url = "https://alahele.ischool.uw.edu:8997/api/getActiveProjects?group=project|namespace";
    $.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;
        }
        // Parse the data here, otherwise it will be re-parsed every time we sort
        var struc = {};
        for (var i in data.result) {
          if (! (data.result[i].p_id in struc)) {
            struc[data.result[i].p_id] = {
              p_title: data.result[i].p_title, p_id: data.result[i].p_id,
              total_edits: 0, total_pages: 0, p_created: data.result[i].p_created,
              total_project_pages: 0,
              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, 100: 0, 101: 0, 108: 0,
              109: 0, 118: 0, 119: 0, 446: 0, 447: 0, 710: 0,
              711: 0, 828: 0, 829: 0, 2600: 0,
            };
          }
          struc[data.result[i].p_id][data.result[i].pa_page_namespace] += data.result[i].edits;
          struc[data.result[i].p_id]["total_edits"] += data.result[i].edits;
          struc[data.result[i].p_id]["total_pages"] += data.result[i].pages;
          if (data.result[i].pa_page_namespace == 4 || data.result[i].pa_page_namespace == 5) 
            struc[data.result[i].p_id]["total_project_pages"] += data.result[i].pages;
        }
        var a_struc = [];
        for (var p in struc) {
          // Add the ratio and push to the array
          struc[p].ratio = struc[p].total_edits / struc[p].total_pages;
          a_struc.push(struc[p]);
        }
        data.result = a_struc;

        // If we successfully fetched active projects, draw them in vte-summary-projects
        vte.drawSummaryContent(data, "edits");
      },
      error: function(xhr, stat, err) {
        console.error("Failed to request active projects: " + JSON.stringify(xhr));
      }
    });
  },

  drawSummaryContent: function(data, sort_by) {
    // We can sort if we want to..
    if (typeof(sort_by) !== 'undefined') {
      if (sort_by == 'edits') {
        if (data.result[0].total_edits < data.result[ data.result.length-1 ].total_edits) {
          data.result.sort( function(a,b) { return a.total_edits - b.total_edits } );
        } else {
          data.result.sort( function(a,b) { return b.total_edits - a.total_edits } );
        }
      } else if (sort_by == 'pages') { 
        if (data.result[0].total_pages < data.result[ data.result.length-1 ].total_pages) {
          data.result.sort( function(a,b) { return a.total_pages - b.total_pages } );
        } else {
          data.result.sort( function(a,b) { return b.total_pages - a.total_pages } ); 
        }
      } else if (sort_by == 'ratio') { 
        if (data.result[0].ratio < data.result[ data.result.length-1 ].ratio) {
          data.result.sort( function(a,b) { return a.ratio - b.ratio } );
        } else {
          data.result.sort( function(a,b) { return b.ratio - a.ratio } ); 
        }
      } else if (sort_by == 'project_edits') {
        if (data.result[0]["4"] < data.result[data.result.length-1]["4"]) {
          data.result.sort( function(a,b) { return a["4"] - b["4"] } );
        } else {
          data.result.sort( function(a,b) { return b["4"] - a["4"] } );
        }
      } else if (sort_by == 'project_pages') {
        if (data.result[0].total_project_pages < data.result[data.result.length-1].total_project_pages) {
          data.result.sort( function(a,b) { return a.total_project_pages - b.total_project_pages } );
        } else {
          data.result.sort( function(a,b) { return b.total_project_pages - a.total_project_pages } );
        }
      }
    }

    // Clear the content div, print initial greeting
    $("#vte-window-right-content").html(
      "<div id='vte-summary-instructions'>" +
      "  Search for a WikiProject in the box above, or select from the list of most " +
      "  active WikiProjects below to continue.<br/>" +
      "  (Projects below represent the most active WikiProjects by edits to " +
      "  member pages within the last month, limited to those with at least 30 edits)" +
      "</div>" +
      "<div id='vte-summary-projects' />"
    );
    $("#vte-summary-instructions").css(s_vteSummaryInstructions);

    // Add in buttons to sort projects by edits, pages edited, or edits per page
    $("#vte-summary-projects").append(
      "<div class='vte-sort-summary-div'>" +
      "  <input type='submit' class='vte-sort-summary' vte-sort-summary-by='edits' value='Sort by edits' />" +
      "</div>" +
      "<div class='vte-sort-summary-div'>" +
      "  <input type='submit' class='vte-sort-summary' vte-sort-summary-by='pages' value='Sort by pages' />" +
      "</div>" +
      "<div class='vte-sort-summary-div'>" +
      "  <input type='submit' class='vte-sort-summary' vte-sort-summary-by='ratio' value='Sort by ratio' />" +
      "</div>" +
      "<div class='vte-sort-summary-div'>" +
      "  <input type='submit' class='vte-sort-summary' vte-sort-summary-by='project_edits' value='Sort by project edits' />" +
      "</div>"
    );

    // Add the project thumbnail for each of the most active projects
    $.each(data.result, 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(
        "<div id='vte-active-project-" + proj.p_id + "' class='vte-active-project' p_id='" + proj.p_id + "' " +
        "  p_title='" + proj.p_title + "' p_created='" + proj.p_created + "' " + style + ">" +
        "  <table style='width: 100%;'><tr>" +
        "    <td colspan='2' class='vte-active-project-title'>" + proj.p_title.replace(/_/g," ") + "</td></tr>" +
        "    <tr><td class='vte-active-project-label'>Project Edits</td>" +
        "    <td class='vte-active-project-value'>" + proj["4"] + "</td>" +
        "    </tr><tr><td class='vte-active-project-label'>Edits</td>" +
        "    <td class='vte-active-project-value'>" + proj.total_edits + "</td>" +
        "    </tr><tr><td class='vte-active-project-label'>Pages Edited</td>" +
        "    <td class='vte-active-project-value'>" + proj.total_pages + "</td>" +
        "    </tr><tr><td class='vte-active-project-label'>Edits per page</td>" +
        "    <td class='vte-active-project-value'>" + (Math.round(proj.ratio * 100) / 100) + "</td>" +
        "  </tr></table>" +
        "</div>"
      );

      // 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", proj.p_title);
        $("#vte-window").data("vte-project-attr", {
          id: $(e.currentTarget).attr("p_id"),
          title: $(e.currentTarget).attr("p_title"),
          created: $(e.currentTarget).attr("p_created"),
        });
        $("#vte-project-select-input").val( $(e.currentTarget).attr("p_title").replace(/_/g," ") );
        // Style the input
        $("#vte-project-select-input").prop("disabled", true);
        $("#vte-project-select-input").css("color", "#A4A4A4");
        // Draw the page
        vte.populateNav();
        vte.drawProjectSummary();
      });
    });
    // 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");
      vte.drawSummaryContent(data, sort_by);
    });
  },

  //
  // HELPER FUNCTIONS
  //

  // 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();
  },

  // 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.
  getDateStr: function(d) {
    if (typeof(d) === 'undefined') {
      d = new Date();
    }
    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) {
    var 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];
  },

};

/**** 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_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,.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)",
  "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 #EEE",
  "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",
};

// 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": s_wpFont,
  "font-size": "10px",
  "margin": "5px 0px 5px 10px",
  "color": "#424242",
};
var s_vteTasksTable = {
  "font-family": s_wpFont,
  "font-size": "12px",
  "color": "#424242",
  "border-collapse": "collapse",
};
var s_vteTasksRow = {
  "border-bottom": "1px solid #EEE",
  "cursor": "pointer",
  "padding": "3px 0px",
};
var s_vteTasksTableTitle = {
  "width": "40%",
};
var s_vteTasksTablePriority = s_vteTasksTableCreated = s_vteTasksTableDue = {
  "text-align": "center",
};
s_vteTasksTablePriority["width"] = "5%";
s_vteTasksTableCreated["width"] = "20%";
s_vteTasksTableDue["width"] = "15%";
// 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_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_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,
  "font-size": "10px",
  "font-style": "italic",
  "border-bottom": "solid 1px #EEE",
  "padding-top": "10px",
};
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)
//if (mw.config.get('wgCanonicalNamespace') === 'Wikipedia') {
  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("https://alahele.ischool.uw.edu:8997", {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);
  });
//}
//</nowiki>