Jump to content

User:Md gilbert/vte.js

From Wikipedia, the free encyclopedia
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 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>";
var data_api = "https://alahele.ischool.uw.edu:8997";

var vte = {
  // initialize - application constructor
  initialize: function() {
    // Load the external libraries
    var head = document.getElementsByTagName("head")[0];
    var script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = data_api + '/static/d3.min.js';
    head.appendChild(script);
    script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = data_api + '/static/socket.io-1.2.0.js';
    head.appendChild(script);
    script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = data_api + '/static/wiky.js';
    head.appendChild(script);
    //wiky.js didn't work for what we needed, trying Pilaf's InstaView
    //Grabbed from https://en.wikipedia.org/wiki/User:Pilaf/instaview.js
    script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = data_api + '/static/instaview.js';
    head.appendChild(script);

    // fdeb requires d3, make sure it's loaded first
    var t1 = setInterval(function() {
      if (typeof(d3) != 'undefined') {
        clearInterval(t1);
        script = document.createElement('script');
        script.type = 'text/javascript';
        script.src = data_api + '/static/fdeb.js';
        head.appendChild(script);
      }
    }, 100);

    // Create the VTE button
    var $btn = $(
      "<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.setCookie("vte-view", "Explorer");
      vte.setCookie("vte-status", "Open");
      $("#vte-window").show();
    });

    // Preload the vte once required variables are loaded (ie, socket.io)
    var t2 = setInterval(function() {
      if (typeof(vte_sock.emit) !== 'undefined') {
        clearInterval(t2);
        vte.renderOverlay();
      }
    }, 100);
  }, // end initialize

  // renderOverlay - draws the initial vte lightbox
  renderOverlay: function() {
    // Emit vte load
    vte_sock.emit("vte_load", {
      name: mw.config.get("wgUserName"),
      time: new Date(),
      page: mw.config.get("wgTitle"),
      namespace: mw.config.get("wgNamespaceNumber"),
    });

    // If the window already exists, just display it
    if ( $("#vte-window").length > 0 ) {
      $("#vte-window").show();
      return;
    }

    // Otherwise, create the vte window
    var $vteWindow = $(
      "<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-nav' />" +
      "    <div id='vte-window-right-content'>" +
      "      <div class='vte-page' id='vte-window-loading'/>" +
      "      <div class='vte-page' id='vte-window-explorer'/>" +
      "      <div class='vte-page' id='vte-window-summary'/>" +
      "      <div class='vte-page' id='vte-window-members'/>" +
      "      <div class='vte-page' id='vte-window-tasks'/>" +
      "      <div class='vte-page' id='vte-window-communication'/>" +
      "    </div>" +
      "  </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-nav").css(s_vteWindowRightNav);
    $("#vte-window-right-content").css(s_vteWindowRightContent);
    // Initially hide the window (we render on page load, only display if it was previously open)
    $("#vte-window").css("display", "none");
    // Initially hide each of the content pages (ie, loading, explorer, summary, etc)
    $(".vte-page").css("display", "none");
    // Populate the loading page
    $("#vte-window-loading").html(
      "<div>" +
      "Loading..." +
      "</div>"
    );
    $("#vte-window-loading").css(s_vteWindowLoading);

    // Fill in basic vte elements
    vte.populateTitle();
    vte.populateChat();
    // Get project and active project information
    vte.getProjectData();

    // If we render the overlay from a WikiProject page, preload the summary for that project
    // The general flow is:
    // 1) Draw the initial VTE window and request project data
    // 2) Once project data is received, we have a few options:
    //   2.0) If the VTE was previously open, draw it immediately with a loading window. The steps
    //        below will populate the proper content.
    //   2.1) Whether or not we're on a project page, if the VTE was not previously open, get the
    //        project data, draw the project explorer, but don't show the VTE.
    //   2.2) If we're on a project page and the VTE was not previously closed, draw the VTE at the 
    //        project summary view.
    //   2.3) If we're not on a project page but the VTE was previously open to a project, draw the 
    //        VTE with the previously opened project's summary.
    //   2.4) If we're not on a project page but the VTE was previously open to the project explorer,
    //        open the VTE to the project explorer.

    // 2.0) If the VTE was previously open, draw it immediately and show the loading page.
    var stat = vte.getCookie("vte-status");
    var view = vte.getCookie("vte-view");
    if (stat != null && stat != "Closed") {
      $("#vte-window").show();
      $("#vte-window-loading").show();
    }
    var t1 = setInterval(function() {
      if (typeof($("#vte-window").data("vte-projects")) !== "undefined") {
        // Projects loaded, clear the interval and compare with the current page
        clearInterval(t1);
        // 2.1) Populate the project explorer page, regardless of page or whether the VTE was open
        vte.populateProjectSelect();
        var t2 = setInterval(function() {
          if (typeof($("#vte-window").data("vte-active-projects")) !== "undefined") {
            clearInterval(t2);
            vte.populateProjectExplorer();
          }
        }, 100);

        // 2.2) Iterate over projects and see if we're on a project page
        var index = -1;
        for (var i = 0; i < $("#vte-window").data("vte-projects").result.length; i++) {
          if (mw.config.get('wgTitle').replace(/ /g, "_") == $("#vte-window").data("vte-projects").result[i].p_title) {
            index = i;
            break;
          }
        }
        if (mw.config.get('wgNamespaceNumber') == 4 && index != -1) {
          // We've got a match.  Set data and draw the summary
          console.log("vte - Loading project summary");
          $("#vte-window").data("vte-project", {
            title: $("#vte-window").data("vte-projects").result[index].p_title,
            id: $("#vte-window").data("vte-projects").result[index].p_id,
            created: $("#vte-window").data("vte-projects").result[index].p_created,
            members: {},
            tasks: {},
          });
          vte.pageTransition("vte-window-summary", function() {
            vte.populateNav();
            vte.populateProjectSummary();
          });
        } else {
          // 2.3) If we're not on a project page, still render that project page if the project cookie is set
          var project = vte.getCookie("vte-project");
          if (project) {
            console.log("vte - not on a project page but vte-project cookie set: " + project.title);
            $("#vte-window").data("vte-project", project);
            vte.pageTransition("vte-window-summary", function() {
              vte.populateNav();
              vte.populateProjectSummary();
            });
          } else {
            // 2.4) Otherwise, switch to the project explorer page
            vte.pageTransition("vte-window-explorer", function() {
              $(".vte-page").hide();
              $("#vte-window-explorer").show();
            });
          }
        }
      } else {
        // Projects aren't loaded yet, keep waiting...
      }      
    }, 100);
  },

  // getProjectData - Called when the vte is rendered on page load. Requests active and all projects.
  getProjectData: function() {
    // First try to load the project data from Storage variables
    // (see http://www.w3schools.com/html/html5_webstorage.asp)
    var vte_projects = vte.getStorage("vte-projects");
    var vte_active_projects = vte.getStorage("vte-active-projects");
    if (vte_projects !== null && vte_active_projects !== null) {
      console.log("vte - found project data in localStorage");
      $("#vte-window").data("vte-projects", vte_projects);
      $("#vte-window").data("vte-active-projects", vte_active_projects);
      return true;
    }

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

    // Request all projects
    var url = data_api + '/api/getProjects';
    $.ajax({
      url: url,
      dataType: "json",
      success: function(data, stat, xhr) {
        if (data.errorstatus != "success") {
          console.error("Failed to request projects: " + data.message);
          return false;
        }
        $("#vte-window").data("vte-projects", data);
        vte.setStorage("vte-projects", data, {expires: 7});
      },
      error: function(xhr, stat, err) {
        console.error("Failed to request project data from API: " + JSON.stringify(xhr));
      },
    });
    // Request active projects
    var url = data_api + "/api/getActiveProjects?group=project|namespace&compress=project";
    $.ajax({
      url: url,
      dataType: "json",
      success: function(data, stat, xhr) {
        if (data.errorstatus != "success") {
          console.error("Failed to request active projects: " + data.message);
          return false;
        }
        // Add in the ratio
        for (var i in data.result) {
          data.result[i].ratio = data.result[i].total_edits / data.result[i].total_pages;
        }
        $("#vte-window").data("vte-active-projects", data);
        vte.setStorage("vte-active-projects", data, {expires: 7});
      },
      error: function(xhr, stat, err) {
        console.error("Failed to request active projects: " + JSON.stringify(xhr));
      }
    });
  },

  // getWikiPage - requests page content for the last revision of a wiki page
  // obj can contain:
  //   title: The page title to request data for
  //   onCreate: Function called if the requested page doesn't exist
  //   onSuccess: Function called on successfully fetching the page
  //   onFailure: Function called on failing to fetch the page
  getWikiPage: function(obj) {
    if (typeof(obj) !== 'object') obj = {};
    var title = obj.title;
    if (! title) {
      console.error("getWikiPage: 'title' argument is required");
      return false;
    }
    if (! ("onSuccess" in obj)) obj.onSuccess = function() {};
    if (! ("onFailure" in obj)) obj.onFailure = function() {};
    if (! ("onCreate" in obj)) obj.onCreate = function() {};
    $.getJSON(
      mw.util.wikiScript('api'),
      {
        format: "json",
        action: "query",
        prop: "revisions",
        rvprop: "content",
        rvlimit: 1,
        titles: title,
      }
    )
    .done(function(data) {
      var page, text;
      //try {
        for (page in data.query.pages) {
          text = data.query.pages[page].revisions[0]["*"];
        }
        obj.onSuccess(text);
/*
      } catch(e) {
        // If the page is missing call obj.onCreate()
        if ("-1" in data.query.pages && data.query.pages["-1"].missing == "") {
          console.log("Requested page not found: " + obj.title);
          obj.onCreate();
        } else {
          obj.onFailure(e);
        }
      }
*/
    })
    .fail(function(e) {
      obj.onFailure(e);
    });
  },
  // updateWikiPage - updates a wiki page with a given string
  // obj can contain:
  //   title: The page title to update
  //   text: The full text of the updated page
  //   summary: The summary for the revision
  //   onSuccess: Function called on successful updates
  //   onFailure: Function called on failing to update
  updateWikiPage: function(obj) {
    if (typeof(obj) !== 'object') obj = {};
    var title = obj.title;
    if (! title) {
      console.error("getWikiPage: 'title' argument is required");
      return false;
    }
    if (! ("onSuccess" in obj)) obj.onSuccess = function() {};
    if (! ("onFailure" in obj)) obj.onFailure = function() {};
    if (! ("summary" in obj)) obj.summary = "[VTE] Updating page contents";
    // Make the request to update the page
    $.ajax({
      url: mw.util.wikiScript( 'api' ),
      type: 'POST',
      dataType: 'json',
      data: {
        format: 'json',
        action: 'edit',
        title: obj.title,
        text: obj.text, // will replace entire page content
        summary: obj.summary,
        token: mw.user.tokens.get( 'editToken' )
      }
    })
    .done( obj.onSuccess )
    .fail( obj.onFailure );
  },
  // getTaskData - requests data from the Tasks page for this project, creates the page if it doesn't exist
  getTaskData: function() {
    var vte_project = $("#vte-window").data("vte-project");
    var obj = {
      title: "User:Vtebot/" + vte_project.title + "/Tasks",
      onCreate: function() {
        vte.updateTaskData();
        vte.getTaskData();
      },
      onSuccess: function(text) {
        var res = vte.parseTable(text, "tasks");
        vte_project = $("#vte-window").data("vte-project");
        vte_project.tasks = res;
        $("#vte-window").data("vte-project", vte_project);
      },
      onFailure: function(e) {
        console.error("Failed to request wiki page: " + JSON.stringify(e));
      },
    };
    vte.getWikiPage(obj);
  },
  // getTaskTalkData - requests data from the Tasks Talk page for this project, create if it doesn't exist
  getTaskTalkData: function() {
    var vte_project = $("#vte-window").data("vte-project");
    var obj = {
      title: "User_talk:Vtebot/" + vte_project.title + "/Tasks",
      onCreate: function() {
        vte.updateTaskTalkData();
        vte.getTaskTalkData();
      },
      onSuccess: function(text) {
        var res = vte.parseTalk(text, "tasks_talk");
        vte_project = $("#vte-window").data("vte-project");
        vte_project.tasks_talk = res;
        $("#vte-window").data("vte-project", vte_project);
      },
      onFailure: function(e) {
        console.error("Failed to request wiki talk page: " + JSON.stringify(e));
      },
    };
    vte.getWikiPage(obj);
  },
  // getMemberData - requests data from the Members page for this project, creates the page if it doesn't exist.
  //   This function will /also/ grab procedural members, ie, those required to build out the social network
  //   for the project and inform the import function.
  getMemberData: function() {
    var vte_project = $("#vte-window").data("vte-project");
    var obj = {
      title: "User:Vtebot/" + vte_project.title + "/Members",
      onCreate: function() {
        vte.updateMemberData();
        vte.getMemberData();
      },
      onSuccess: function(text) {
        var res = vte.parseTable(text, "members");
        vte_project = $("#vte-window").data("vte-project");
        vte_project.members = res;
        $("#vte-window").data("vte-project", vte_project);
      },
      onFailure: function(e) {
        console.error("Failed to request Members page: " + JSON.stringify(e));
      },
    };
    vte.getWikiPage(obj);

    /****
     * Grab the procedural member data.  This will require 4 data requests:
     * 1) Get the top <topEditors> editors to the current project, subpages, and talk pages.
     * 2) Get all users who have links on the project page and all sub-pages (not talk pages).
     * Note - Users flagged 3 ways: user link on page, edit to page, edit to talk page
     * 3) Get all pages that are under the scope of this project.
     * 4) For each of those user, get the top <topPages> pages each of those 
     *    editors edited, 
     * Note - Pages flagged 2 ways: in-project or out-project
     *
     * And that's it.
     * Queries 1, 2, and 3 can be run concurrently. 4 depends on 1 and 2.
     ****/

    // object to hold the data
    var res = {
      editors: {},
      links: {},
      p_pages: {},
      pages: {},
    };
    // Boolean to track ongoing requests
    var complete = {
      editors: 0,
      links: 0,
      p_pages: 0,
      pages: 0,
    };

    // 1) Fetch top editors to the project page, sub-pages, and corresponding talk pages
    $.ajax({
      url: data_api + "/api/getEdits?",
      data: {
        page: vte_project.title,
        //sd: , // Default range is 1 year, ending now, which should be appropriate
        //ed: ,
        group: "user|page|date",
        subpages: 1,
        namespace: "4|5",
        //limit: topEditors,
        excludeBots: 1,
      },
      dataType: "json",
      error: function(xhr, stat, err) {
        console.error("Failed to request data, project editors. Response: " + JSON.stringify(xhr));
      },
      success: function(data, stat, xhr) {
        // Check for error
        if (data.errorstatus == 'fail') {
          console.error("Error: Failed to request data, project editors: " + data.message);
        }
        // Save the results
        complete.editors = 1;
        res.editors = data.result;
        if (res.editors.length == 0) {
          console.warn("No edits to the project page or subpages found for project: " + vte_project.title);
        }
      }
    });

    // 2) Get all users who have links on the project page and all sub-pages
    $.ajax({
      url: data_api + "/api/getProjectMembers?",
      data: {
        project: vte_project.title,
        //sd: , // Default time span is 1 year, which is appropriate for now.
        //ed: ,
      },
      dataType: "json",
      error: function(xhr, stat, err) {
        console.error("Failed to request data, member links: " + JSON.stringify(xhr));
      },
      success: function(data, stat, xhr) {
        // Check for error
        if (data.errorstatus == 'fail') {
          console.error("Error: Failed to request data, project members: " + data.message);
        }
        // Save the results
        complete.links = 1;
        res.links = data.result;
        if (Object.keys(res.links).length == 0) {
          console.warn("No project member user links found for project: " + vte_project.title);
        }
      }
    });

    // 3) Get all pages that are under the scope of this project
    $.ajax({
      url: data_api + "/api/getProjectPages?",
      data: {
        project: vte_project.title,
      },
      dataType: "json",
      error: function(xhr, stat, err) {
        console.error("Failed to request data, project pages: " + JSON.stringify(xhr));
      },
      success: function(data, stat, xhr) {
        // Check for error
        if (data.errorstatus == 'fail') {
          console.error("Error: Failed to request data, project pages: " + data.message);
        }
        // Save the results
        complete.p_pages = 1;
        res.p_pages = data.result;
        if (Object.keys(res.p_pages).length == 0) {
          $rlog("No pages found for project: " + vte_project.title);
        }
      }
    });

    // 4) For each of the users, get the top <topPages> pages each of those users edited.
    // This depends on 1 & 2 above, so wait until they're done to complete
    var start = new Date().getTime();
    var t = setInterval( function() {
      if (complete.editors == 1 && complete.links == 1) {
        clearInterval(t);
        // Grab the users from editors and links and look for all pages they edited
        var uids = [];
        for (var u in res.links) {
          for (var p in res.links[u]) {
            if (res.links[u][p].link_count > 0 && res.links[u][p].pm_user_id != 0) 
              uids.push(res.links[u][p].pm_user_id);
          }
        }
        for (var i in res.editors) {
          if (res.editors[i].tu_id != 0) 
            uids.push(res.editors[i].tu_id);
        }
        // And then, finally, we collect the edit histories of the users who worked on
        // the project page (editors) and those who placed their user links on the project
        // page (links).
        $.ajax({
          url: data_api + "/api/getEdits?",
          data: {
            userid: uids.join("|"),
            //sd: , // Default is to get edits for 1 year, ending now, which is fine.
            //ed: ,
            group: "user|page",
            namespace: "0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|100|101|108|109|118|119|446|447|710|711|828|829",
            limit: 2000, // Limiting mostly to reduce download size of result, usually about 3-4 Mb
            excludeBots: 1,
          },
          dataType: "json",
          error: function(xhr, stat, err) {
            console.error("Failed to request data, global edits: " + JSON.stringify(xhr));
          },
          success: function(data, stat, xhr) {
            // Check for an error
            if (data.errorstatus == 'fail') {
              console.error("Error, failed to request data, global edits: " + data.message);
            }
            // Save the results
            complete.pages = 1;
            res.pages = data.result;
            if (res.pages.length == 0) {
              console.warn("No edits found for editors and members of project: " + vte_project.title);
            }
          }
        });

      } else {
        // Timeout after 60 seconds
        if ( (new Date().getTime()) - start >= 60000) {
          clearInterval(t);
          console.error("Timed out requesting project network data: " + JSON.stringify(complete));
        }
      }
    }, 100);

    // Wait until all 4 data requests are complete
    var t1 = setInterval( function() {
      if (complete.editors == 1 && complete.links == 1 &&
          complete.p_pages == 1 && complete.pages == 1) {
          // We're all done, clear the interval and save the data
          clearInterval(t1);
          vte_project = $("#vte-window").data("vte-project");
          vte_project.members_network = res;
          $("#vte-window").data("vte-project", vte_project);
      } else {
        // Timeout after 60 seconds
        if ( (new Date().getTime()) - start >= 60000) {
          clearInterval(t1);
          console.error("Timed out requesting project network data: " + JSON.stringify(complete));
        }
      }
    }, 100);

  },
  // getMemberTalkData - requests data from the Talk page for this project's members, creates it if needed
  getMemberTalkData: function() {
    var vte_project = $("#vte-window").data("vte-project");
    var obj = {
      title: "User_talk:Vtebot/" + vte_project.title + "/Members",
      onCreate: function() {
        vte.updateMemberTalkData();
        vte.getMemberTalkData();
      },
      onSuccess: function(text) {
        var res = vte.parseTalk(text, "members_talk");
        vte_project = $("#vte-window").data("vte-project");
        vte_project.members_talk = res;
        $("#vte-window").data("vte-project", vte_project);
      },
      onFailure: function(e) {
        console.error("Failed to request wiki talk page: " + JSON.stringify(e));
      },
    };
  },

  // updateTaskData - will update the current task list with data from $("#vte-window").data("vte-project").tasks
  //   and create a corresponding talk page section, saved in $("#vte-window").data("vte-project").tasks_talk
  updateTaskData: function(onSuccess, onFailure) {
    if (typeof(onSuccess) === 'undefined') onSuccess = function(){};
    if (typeof(onFailure) === 'undefined') onFailure = function(){};
    var vte_project = $("#vte-window").data("vte-project");
    var title       = "User:Vtebot/" + vte_project.title + "/Tasks";

    // Build the page text, if we don't currently have any tasks we're probably creating the stub page
    var tasks_str = "";
    if ($.isEmptyObject(vte_project.tasks)) {
      // Create the page with the tasks stub, default display is task title, description, and priority
      tasks_str = 
        "<!-- To add a task, copy the following line and place inside the ListMaster invocation: \n" +
        "  {{#title=<task title>|description=<task description>|priority=<task priority>}}\n" +
        "-->\n\n" +
        "{{#invoke:ListMaster|printTable|style=table|display=title,description,priority|\n" +
        "}}";
    } else {
      // Otherwise, build the task string from the tasks object
      tasks_str = vte_project.tasks.pre +
        "{{#invoke:ListMaster|printTable|style=" + vte_project.tasks.style + 
        "|display=" + vte_project.tasks.display + "|\n";
      for (var i in vte_project.tasks.struc) {
        tasks_str += "  {{#";
        var attribs = [];
        for (var n in vte_project.tasks.struc[i]) {
          // Don't save empty values
          if (vte_project.tasks.struc[i][n]) attribs.push(n + "=" + vte_project.tasks.struc[i][n]);
        }
        tasks_str += attribs.join("|") + "}}\n";
      }
      tasks_str += "}}" + vte_project.tasks.post;
    }

    var obj = {
      title: title,
      text: tasks_str,
      summary: "[VTE] Updating details for task: " + $("#vte-task-title").val(),
      onSuccess: function() {
        // Emit vte update
        vte_sock.emit("update", {
          name: mw.config.get("wgUserName"),
          time: new Date(),
          page: mw.config.get("wgTitle"),
          namespace: mw.config.get("wgNamespaceNumber"),
          project: $("#vte-window").data("vte-project").title,
          view: "Tasks",
        });
        onSuccess();
      },
      onFailure: function(e) {
        console.error("Failed to update tasks page: " + JSON.stringify(e));
        onFailure();
      },
    };
    //vte.updateWikiPage(obj);
onSuccess();
  },
  // updateTaskTalkData - will update the current task talk data from $("#vte-window").data("vte-project").task_talk
  updateTaskTalkData: function(onSuccess, onFailure) {
    if (typeof(onSuccess) === 'undefined') onSuccess = function(){};
    if (typeof(onFailure) === 'undefined') onFailure = function(){};
    var vte_project = $("#vte-window").data("vte-project");
    var title = "User_talk:Vtebot/" + vte_project.title + "/Tasks";

    // Build the Tasks Talk page string, if we don't currently have any content we're probably creating the page
    var talk_str = "";
    if ($.isEmptyObject(vte_project.tasks_talk)) {
      // Create the page with the talk stub (empty string)
      talk_str = "";
    } else {
      // Otherwise, build the talk page string from the tasks_talk object
      for (var task in vte_project.tasks_talk) {
        talk_str += "== " + task + " ==\n";
        for (var i in vte_project.tasks_talk[task]) {
          var obj = vte_project.tasks_talk[task][i];
          talk_str += Array(obj.level + 1).join(":") + obj.msg + "\n";
        }
      }
    }

    // Finally, request the page update
    var obj = {
      title: title,
      text: talk_str,
      summary: "[VTE] Updating Tasks Talk page",
      onSuccess: function() {
        onSuccess();
      },
      onFailure: function(e) {
        console.error("Failed to update Tasks Talk page: " + JSON.stringify(e));
        onFailure(e);
      },
    };
    //vte.updateWikiPage(obj);
onSuccess();
  },
  // updateMemberData - will update the current member list with data from 
  // $("#vte-window").data("vte-project").members
  updateMemberData: function(onSuccess, onFailure) {
    if (typeof(onSuccess) === 'undefined') onSuccess = function(){};
    if (typeof(onFailure) === 'undefined') onFailure = function(){};
    var vte_project = $("#vte-window").data("vte-project");
    var title       = "User:Vtebot/" + vte_project.title + "/Members";

    // Build the page text, if we don't currently have any members we're probably creating the stub page
    var members_str = "";
    if ($.isEmptyObject(vte_project.members)) {
      // Create the page with the members stub, default display is member name and interests
      members_str =
        "<!-- To add a member, copy the following line and place inside the ListMaster invocation: \n" +
        "  {{#name=<user name>|interests=<what you're interested in>}}\n" +
        "-->\n\n" +
        "{{#invoke:ListMaster|printTable|style=table|display=name,interests|\n" +
        "}}";
    } else {
      // Otherwise, build the member string from the members object
      members_str = vte_project.members.pre +
        "{{#invoke:ListMaster|printTable|style=" + vte_project.members.style +
        "|display=" + vte_project.members.display + "|\n";
      for (var i in vte_project.members.struc) {
        members_str += "  {{#";
        var attribs = [];
        for (var n in vte_project.members.struc[i]) {
          attribs.push(n + "=" + vte_project.members.struc[i][n]);
        }
        members_str += attribs.join("|") + "}}\n";
      }
      members_str += vte_project.members.post;
    }

    var obj = {
      title: title,
      text: members_str,
      summary: "[VTE] Updating project members",
      onSuccess: function() {
        // Emit vte update
        vte_sock.emit("update", {
          name: mw.config.get("wgUserName"),
          time: new Date(),
          page: mw.config.get("wgTitle"),
          namespace: mw.config.get("wgNamespaceNumber"),
          project: $("#vte-window").data("vte-project").title,
          view: "Members",
        });
        onSuccess();
      },
      onFailure: function(e) {
        console.error("Failed to update project members: " + JSON.stringify(e));
        onFailure();
      },
    };
    vte.updateWikiPage(obj);
  },
  // updateMemberTalkData - will update the current member talk data from 
  //  $("#vte-window").data("vte-project").members_talk
  updateMemberTalkData: function(onSuccess, onFailure) {
    if (typeof(onSuccess) === 'undefined') onSuccess = function(){};
    if (typeof(onFailure) === 'undefined') onFailure = function(){};
    var vte_project = $("#vte-window").data("vte-project");
    var title = "User_talk:Vtebot/" + vte_project.title + "/Members";

    // Build the Memberss Talk page string, if we don't currently have any content we're probably creating the page
    var talk_str = "";
    if ($.isEmptyObject(vte_project.members_talk)) {
      // Create the page with the talk stub (empty string)
      talk_str = "";
    } else {
      // Otherwise, build the talk page string from the members_talk object
      for (var member in vte_project.members_talk) {
        talk_str += "== " + member + " ==\n";
        for (var i in vte_project.members_talk[member]) {
          var obj = vte_project.members_talk[member][i];
          talk_str += Array(obj.level + 1).join(":") + obj.msg + "\n";
        }
      }
    }

    // Finally, request the page update
    var obj = {
      title: title,
      text: talk_str,
      summary: "[VTE] Updating Members Talk page",
      onSuccess: function() {
        onSuccess();
      },
      onFailure: function(e) {
        console.error("Failed to update Members Talk page: " + JSON.stringify(e));
        onFailure(e);
      },
    };
    //vte.updateWikiPage(obj);
onSuccess();
  },


  // populateTitle - draws title bar content
  populateTitle: function() {
    var $vteTitle = $(
      "<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='https://upload.wikimedia.org/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='https://upload.wikimedia.org/wikipedia/commons/7/77/Gear_icon.svg' width='25' height='25'/>" +
      "  </div>" +
      "  <div id='vte-title-action-close' title='Close the VTE'>" +
      "    <img src='https://upload.wikimedia.org/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.setCookie("vte-status", "Closed");
      $("#vte-window").hide();
    });
  },

  // populateProjectSelect - draws the project browser content
  populateProjectSelect: function() {
    // Add search box and main nav
    var $projectSelect = $(
      "<input id='vte-project-select-input' type='text' placeholder='Enter a WikiProject to explore'/>" +
      "<div id='vte-project-select-multi' />"
    );
    // Add it
    $("#vte-window-right-tool").html($projectSelect);
    $("#vte-project-select-multi").hide();
    // Style it
    $("#vte-project-select-label").css(s_vteProjectSelectLabel);
    $("#vte-project-select-input").css(s_vteProjectSelectInput);

    // For each of the projects, add it to the dropdown
    var projects = $("#vte-window").data("vte-projects").result;
    $.each(projects, function(i,v) {
      var project = projects[i]['p_title'].replace(/_/g, " ").toLowerCase();
      var input = $("#vte-project-select-input").val().replace(/_/, " ").toLowerCase();
      $("#vte-project-select-multi").append(
        "<div style='display: block;' 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>"
      );
    });
    // Then style the things
    $("#vte-project-select-multi").css(s_vteProjectSelectMulti);
    $(".vte-project-select-multi-proj").css(s_vteProjectSelectMultiProj);
    // Add hover color for project
    $(".vte-project-select-multi-proj").hover(
      function() {
        $( this ).css("color", "#3B0B0B");
      }, function() {
        $( this ).css("color", "#000");
      }
    );
    // Add the action to watch for keyup in the project input
    vte.updateProjectSelect();
    // Add click action to hide the list
    $("body").on("click", function(evt) {
      $("#vte-project-select-multi").hide();
    });
    // Add click action to load project summary
    $(".vte-project-select-multi-proj").on("click", function(evt) {
      var id      = $(evt.currentTarget).attr("vte-p-id");
      var title   = $(evt.currentTarget).attr("vte-p-title");
      var seen    = $(evt.currentTarget).attr("vte-p-seen");
      var touched = $(evt.currentTarget).attr("vte-p-touched");
      var created = $(evt.currentTarget).attr("vte-p-created");
      // Clear the project selection div
      $("#vte-project-select-multi").remove();
      // Load the project summary
      console.log("loading summary for project " + title + ", id: " + id);
      $("#vte-window").data("vte-project", {
        title: title,
        id: id,
        created: created,
        members: {},
        tasks: {},
      });
      vte.pageTransition("vte-window-summary", function() {
        vte.populateNav();
        vte.populateProjectSummary();
      });
    });
  },

  updateProjectSelect: function() {
    // Add the actions (everytime there's a key-up, update list of visible projects)
    $("#vte-project-select-input").on("keyup", function() {
      var input = $("#vte-project-select-input").val().replace(/ /g,"_").toLowerCase();
      // FIRST, update the list of active projects
      $(".vte-active-project").each(function(i, v) {
        var project = $(v).attr("p_title").replace(/ /g, "_").toLowerCase();
        if (project.indexOf(input) != -1) {
          $(v).css("display", "block");
        } else {
          $(v).css("display", "none");
        }
      });
      // SECOND, update the list from the multi-select dropdown
      // Make sure we're showing the selection div
      $("#vte-project-select-multi").show();
      $(".vte-project-select-multi-proj").each(function(i,v) {
        var project = $(v).attr("vte-p-title").replace(/ /g, "_").toLowerCase();
        if (project.indexOf(input) != -1) {
          $(v).css("display", "block");
        } else {
          $(v).css("display", "none");
        }
      });
      // Print a message if no projects match the input
      if ($(".vte-project-select-multi-proj").not(":hidden").length == 0) {
        $("#vte-project-select-multi").append(
          "<div class='vte-project-select-multi-none' style='font-size: 10px; color: #848484;'>" +
          "  No matching projects found" + 
          "</div>"
        );
      } else {
        $(".vte-project-select-multi-none").remove();
      }
    });
  },

  populateProjectExplorer: function() {
    // Clear the content div, print initial greeting
    $("#vte-window-explorer").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' id='vte-sort-summary-by-edits' 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
    var active = $("#vte-window").data("vte-active-projects").result;
    $.each(active, function(i,v) {
      if (v.total_edits < 30) return true;
      var proj = v;
      // Mark the style as hidden if the project doesn't match the search input box
      var project = proj.p_title.replace(/ /g, "_").toLowerCase();
      var input = $("#vte-project-select-input").val().replace(/ /g,"_").toLowerCase();
      var style = project.indexOf(input) != -1 ? " style='display: block;' " : " style='display: none;' ";
      $("#vte-summary-projects").append(
        "<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>"
      );
      // Save data on the div for sorting
      $("#vte-active-project-" + proj.p_id).data("sort", {
        edits: proj.total_edits,
        pages: proj.total_pages,
        ratio: proj.ratio,
        project_edits: proj["4"],
      });

      // Add the hover action
      $("#vte-active-project-" + proj.p_id).hover(
        function() {
          $(this).css("border", "solid 1px #848484");
        }, function() {
          $(this).css("border", "solid 1px #000000");
        }
      );

      // Add the click action to the thumbnail
      $("#vte-active-project-" + proj.p_id).click(function(e) {
        // Set project attributes
        $("#vte-window").data("vte-project", {
          title: proj.p_title,
          id: $(e.currentTarget).attr("p_id"),
          title: $(e.currentTarget).attr("p_title"),
          created: $(e.currentTarget).attr("p_created"),
        });
        // Draw the page
        vte.pageTransition("vte-window-summary", function() {
          vte.populateNav();
          vte.populateProjectSummary();
        });
      });
    });
    // Style the thumbnails
    $(".vte-active-project").css(s_vteActiveProject);
    $(".vte-active-project-title").css(s_vteActiveProjectTitle);
    $(".vte-active-project-label").css(s_vteActiveProjectLabel);
    $(".vte-active-project-value").css(s_vteActiveProjectValue);
    $(".vte-sort-summary-div").css(s_vteSortSummaryDiv);

    // Add the action to sort
    $(".vte-sort-summary").button().click(function(e) {
      var sort_by = $(e.currentTarget).attr("vte-sort-summary-by");
      var s = $("#vte-window").data("vte-active-projects-sort");
    
      var items = $(".vte-active-project").sort(function(a,b) {
        var da = $(a).data("sort")[sort_by];
        var db = $(b).data("sort")[sort_by];
        if (s.by == sort_by && s.direction == "desc") {
          $("#vte-window").data("vte-active-projects-sort", {by: sort_by, direction: "asc"});
          return (da < db) ? -1 : (da > db) ? 1 : 0;
        } else {
          $("#vte-window").data("vte-active-projects-sort", {by: sort_by, direction: "desc"});
          return (db < da) ? -1 : (db > da) ? 1 : 0;
        }
      });
      $("#vte-summary-projects").append(items);
    });

    // Trigger the initial sort action
    $("#vte-window").data("vte-active-projects-sort", {by: "edits", direction: "asc"});
    $("#vte-sort-summary-by-edits").click();
  },

  // populateProjectSummary - draws summary information for the project once it is selected
  //   from the vte-project-select-multi dropdown (or clicked on)
  populateProjectSummary: function() {
    // Update the project search input
    $("#vte-project-select-input").val( $("#vte-window").data("vte-project").title.replace(/_/g," ") );
    // Style the input
    $("#vte-project-select-input").prop("disabled", true);
    $("#vte-project-select-input").css("color", "#A4A4A4");

    // Request/create the Tasks and Members pages under the vtebot user page.
    vte.getTaskData();
    vte.getTaskTalkData();
    //vte.getMemberData();

    // Set the project cookies
    vte.setCookie("vte-project", $("#vte-window").data("vte-project"));
    vte.setCookie("vte-view", "Summary");

    var title, id, created;
    title = $("#vte-window").data("vte-project").title;
    id = $("#vte-window").data("vte-project").id;
    created = $("#vte-window").data("vte-project").created;
    // Emit vte project select
    vte_sock.emit("project_load", {
      name: mw.config.get("wgUserName"),
      time: new Date(),
      page: mw.config.get("wgTitle"),
      namespace: mw.config.get("wgNamespaceNumber"),
      project: $("#vte-window").data("vte-project").title,
    });

    // Add the close icon
    $("#vte-project-select-input").after("<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-close-project").remove();
      $("#vte-project-select-input").val("");
      $("#vte-project-select-input").prop("disabled", false);
      $("#vte-project-select-input").css("color", "#000000");
      vte.removeCookie("vte-project");
      vte.setCookie("vte-view", "Explorer");
      vte.pageTransition("vte-window-explorer", function() {
        vte.populateProjectExplorer();
        vte.populateNav();
      });
    });

    // Clear any existing data in the content window and add summary divs
    $("#vte-window-summary").html(
      "<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);
    // Dynamically set the width so we don't get squished graphs if they're loaded too quickly.
    // The graphs should be in the right-content, which is 80% of vte-window, which is 80% of the
    // total width, minus padding (5px * 2 for vte-window right, 8px * 2 for the graph divs, 26px padding).
    var width = (window.innerWidth * .8 * .8) - 52;
    $("#vte-project-summary-graph").css("width", width + "px");

    // Request summary data from our backend
    var t = title.replace(/ /g, "_");
    var sd = created.substr(0, 8);
    var sw = vte.convertDateToWikiWeek(sd);
    var url = data_api + "/api/getEdits?page=" + t + "&namespace=4|5&group=page|user|date&sd=" + sd;
    $.ajax({
      url: url,
      dataType: "json",
      success: function(data, stat, xhr) {
        vte.drawProjectEdits(data, sw, "vte-project-summary-graph");
      },
      error: function(xhr, stat, err) {
        console.error("Failed to request project edits: " + JSON.stringify(xhr));
        $("#vte-window-right-content-summary").append("Failed to request project edits: " + JSON.stringify(xhr));
      },
      complete: function() {
        $("#vte-loading-edits").remove();
      },
    });

    // Request most active project pages
    url = data_api + "/api/getActiveProjectPages?project_id=" + id;
    $.ajax({
      url: url,
      dataType: "json",
      success: function(data, stat, xhr) {
        // Once we've got recent active project pages, grab edit histories for those pages
        var ids = [];
        for (var i in data.result) {
          if (data.result[i].tp_namespace == 0 || data.result[i].tp_namespace == 1) 
            ids.push(data.result[i].pa_page_id);
        }
        // We'll want to get edits for the last year
        var now = new Date();
        var sd = String(now.getFullYear()-1) + String(vte.pad(now.getMonth()+1,2)) +
          String(vte.pad(now.getDate(), 2));
        var sw = vte.convertDateToWikiWeek(sd);
        var ew = vte.convertDateToWikiWeek() - 1;
        url = data_api + "/api/getEdits?pageid=" + ids.join("|") + "&limit=0&namespace=0|1&group=page|user|date&sw=" + sw + "&ew=" + ew;
        $.ajax({
          url: url,
          dataType: "json",
          success: function(data,stat,xhr) {
            // Split the results by page
            var pages = {};
            for (var i in data.result) {
              if (! pages.hasOwnProperty( data.result[i].rc_page_id )) pages[ data.result[i].rc_page_id ] = [];
              pages[ data.result[i].rc_page_id ].push( data.result[i] );
            }
            for (var id in pages) {
            //$.each(pages, function(i,v) {
              // Create the graph div for each of the returned articles and draw the graph
              $("#vte-window-right-content-summary-pages").append(
                "<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-window-right-content-summary-page-" + id).css("width", width + "px");
              vte.drawProjectEdits({result: pages[id]}, sw, "vte-window-right-content-summary-page-" + id);
            }
            $(".vte-summary-page-title").css(s_vteSummaryPageTitle);
            // Add actions to article titles to set cookies and go to the page
            $(".vte-summary-page-title").click(function(e) {
              var title = $(e.currentTarget).html().replace(/ /g, "_");
              window.location.href = "/wiki/" + title;
            });
          },
          error: function(xhr, stat, err) {
            console.error("Failed to request edits to most active articles: " + JSON.stringify(xhr));
            $("#vte-window-right-content-summary").append("Failed to request active article edits: " + 
              JSON.stringify(xhr));
          },
          complete: function() {
            $("#vte-loading-pages").remove();
          },
        });
      },
      error: function(xhr, stat, err) {
        console.error("Failed to request active project pages: " + JSON.stringify(xhr));
        $("#vte-window-right-content-summary").append("Failed to request active project pages: " + 
          JSON.stringify(xhr));
      },
    });
  },

  // drawProjectEdits - draws summary edit information for a project and its corresponding Talk page
  drawProjectEdits: function(data, sw, div_id) {
    // Structure the edits
    var ew = vte.convertDateToWikiWeek();  // This should be 1 greater than what was requested.
    var talk_edits = Array(ew - sw);
    var page_edits = Array(ew - sw);
    for (var i = 0; i < talk_edits.length; i++) talk_edits[i] = 0;
    for (var i = 0; i < page_edits.length; i++) page_edits[i] = 0;
    for (var i in data["result"]) {
      if (data["result"][i].rc_page_namespace % 2 == 0) page_edits[ data["result"][i].rc_wikiweek - sw ] += data["result"][i].rc_edits;
      if (data["result"][i].rc_page_namespace % 2 == 1) talk_edits[ data["result"][i].rc_wikiweek - sw ] += data["result"][i].rc_edits;
    }
    // D3 sparkline graph
    // Get the width from the style of the parent element (the actual width from .width() may not be 
    // correct if the element is still being drawn)
    var w = parseInt($("#" + div_id).css("width").replace("px", "")) - 10;
    var h = $("#" + div_id).height();

    var t_max = d3.max(talk_edits);
    var p_max = d3.max(page_edits);
    var maxy = t_max > p_max ? t_max : p_max;
    var y = d3.scale.linear()
      .domain([0, maxy])
      .range([0, h]);
    var x = d3.scale.linear()
      .domain([0, page_edits.length])
      .range([0, w]);
    var vis = d3.select("#" + div_id)
      .append("svg:svg")
      .attr("width", w)
      .attr("height", h);
    var g1 = vis.append("svg:g").attr("transform", "translate(2, " + h + ")");
    var g2 = vis.append("svg:g").attr("transform", "translate(2, " + h + ")");
    var line = d3.svg.line()
      .x(function(d, i) {
        return x(i);
      })
      .y(function(d) {
        return -1 * y(d);
      });
    g1.append("svg:path").attr("d", line(page_edits)).style({"stroke": "#0000FF", "fill": "transparent"});
    g2.append("svg:path").attr("d", line(talk_edits)).style({"stroke": "#545454", "fill": "transparent"});

    // Add the legend text
    var count_text = [
      { "cx": 10, "cy": 12, "text": maxy + " edits" },
      { "cx": 10, "cy": h-5, "text": "0" }
    ];
    var date_text = [
      { "cx": w / 3, "text": vte.convertWikiWeekToDate( ((ew - sw) / 3) + sw ) },
      { "cx": w * 2 / 3, "text": vte.convertWikiWeekToDate( ((ew - sw) * 2 / 3) + sw) }
    ];
    var text_c = vis.selectAll("text.count")
      .data(count_text)
      .enter().append("text")
      .attr("x", function(d) { return d.cx; })
      .attr("y", function(d) { return d.cy; })
      .text( function(d) { return d.text; })
      .attr("font-family", s_wpFont)
      .attr("font-size", "10px")
      .attr("fill", "#000000");
    var text_d = vis.selectAll("text.date")
      .data(date_text)
      .enter().append("text")
      .attr("x", function(d) { return d.cx; })
      .attr("y", function(d) { return 12; })
      .text( function(d) { return d.text.substring(0,4) + "/"+d.text.substring(4,6) + "/"+d.text.substring(6,8); })
      .attr("font-family", s_wpFont)
      .attr("font-size", "10px")
      .attr("text-anchor", "middle")
      .attr("fill", "#848484");
  },

  // populateNav - draws the navigation content
  populateNav: function() {
    if ($("#vte-window").data("vte-project")) {
      // Make sure we're not duplicating the nav links (there was a bug where this happened that
      // I can't seem to repro)
      $("#vte-window-right-nav").empty();
      $("#vte-window-right-nav").append(
        "<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.pageTransition("vte-window-summary", function() {
            $(".vte-content-nav").css("color", "#0B0B61");
            $("#vte-summary").css("color", "#000");
            vte.populateProjectSummary();
          });
        } else if (id == "vte-members") {
          vte.pageTransition("vte-window-members", function() {
            $(".vte-content-nav").css("color", "#0B0B61");
            $("#vte-members").css("color", "#000");
            vte.clickMembers();
          });
        } else if (id == "vte-tasks") {
          vte.pageTransition("vte-window-tasks", function() {
            $(".vte-content-nav").css("color", "#0B0B61");
            $("#vte-tasks").css("color", "#000");
            vte.clickTasks();
          });
        } else if (id == "vte-communication") {
          vte.pageTransition("vte-window-communication", function() {
            $(".vte-content-nav").css("color", "#0B0B61");
            $("#vte-communication").css("color", "#000");
            vte.clickCommunication();
          });
        } else {
          console.error("Unknown vte action: " + id);
        }
      });
      // And make sure it's visible
      $("#vte-window-right-nav").show();
    } else {
      $(".vte-content-nav, .vte-content-nav-spacer").remove();
    }
  },

  // Function to handle page transitions
  pageTransition: function(page, load_function) {
    // Before the transition, hide all pages and show the loading window
    $(".vte-page").hide();
    $("#vte-window-right-nav").hide();
    $("#vte-window-loading").show();
    // Add pulse animation to loading text
    var i = 0;
    var t = setInterval(function() {
      if (i % 2 == 0) {
        $("#vte-window-loading").animate({opacity: 0.3}, 1000, "linear");
      } else {
        $("#vte-window-loading").animate({opacity: 1.0}, 1000, "linear");
      }
      i++;
    }, 1000);
    // Then load the page
    load_function();
    // Then switch to it
    $(".vte-page").hide();
    vte.populateNav();
    $("#" + page).show();
    // And stop the pulse animation
    clearInterval(t);
  },

  // Given a chunk of text, will return an object containing text before, after, and an array of 
  // top-level module invocations (won't parse modules in modules). Returns false if no modules found.
  parseInvocation: function(data) {
    //var obj = {pre: "", post: "", mods: []};
    var obj = [];
    var s_index = 0, e_index = 0, s_paren = 0, c_paren = 0;
    for (var i = 0; i < data.length; i++) {
      if ((data.slice(i, i+3) == "{{#") && (s_paren + c_paren == 0)) s_index = i;
      if (data[i] == "{") s_paren += 1;
      if (data[i] == "}") c_paren += 1;
      if ((s_paren == c_paren) && (s_paren + c_paren > 0)) {
        e_index = i + 1;
        s_paren = 0, c_paren = 0;
        obj.push({
          pre: data.slice(0, s_index),
          post: data.slice(e_index),
          mod: data.slice(s_index, e_index),
        });
      }
    }
    if (s_paren > 0 || c_paren > 0) console.error("Uneven brace count, possible incorrect module declaration.");
    return obj.length > 0 ? obj : false;
  },

  parseTable: function(data, table) {
    // Grab module invocation from the page text (at this level only accepting one table)
    var obj = vte.parseInvocation(data);
    //s_index = data.indexOf("{{#invoke:ListMaster");
    if (! obj) {
      console.error("Failed to find module invocation, page contains: " + data);
      return false;
    }
    // Then grab top-level submodule invocations from within this module
    var subs = [], mod = {};
    for (var i in obj) {
      if (obj[i].mod.slice(0, 20) == "{{#invoke:ListMaster") {
        mod = obj[i];
        subs = vte.parseInvocation(obj[i].mod.slice(3, -2));
      }
    }
    if (Object.keys(mod).length == 0) {
      console.error("Module invocation on page, but not {{#invoke:ListMaster...");
      return false;
    }

    // Break apart sub-module invocations, grabbing columns for each row
    var struc = [];
    for (var i in subs) {
      // Strip the braces
      subs[i].mod = subs[i].mod.slice(3, -2);
      var attribs = subs[i].mod.split("|");
      var row = {};
      for (var j in attribs) {
        // Split pair at first equals sign, so "=" can be used in values
        var pair = attribs[j].split(/=([\s\S]+)?/);
        // Don't add keys without values
        if (typeof(pair[1]) !== 'undefined') row[pair[0].trim()] = pair[1].trim();
      }
      struc.push(row);
    }

    // Then, pull out the style and display values from the parent module
    var re1 = new RegExp("\\|[^\\|]*style=([^\\|]+)");
    var style = mod.mod.match(re1)[1].trim();
    var re2 = new RegExp("\\|[^\\|]*display=([^\\|]+)");
    var display = mod.mod.match(re2)[1].split(",").map(function(str) { return str.trim(); });

    // And save everything
    var vte_project = $("#vte-window").data("vte-project");
    obj = {
      pre: mod.pre,
      post: mod.post,
      struc: struc,
      style: style,
      display: display,
    };
    vte_project[table] = obj;
    $("#vte-window").data("vte-project", vte_project);
    return obj;
  },

  parseUser: function(text) {
    var m2, m3, user, date;
    // Try to grab the user from the prior post
    m2 = text.match(/\[\[User:([^\|\]]+).+(\d{2}:\d{2}, \d+ \S+ \d{4} \(UTC\))/);
    m3 = text.match(/\[\[User:([^\|\]]+)/);
    if (m2 !== null) {
      user = m2[1]; date = m2[2];
    } else if (m3 !== null) {
      user = m3[1]; date = "Unknown";
    } else {
      user = "Unknown"; date = "Unknown";
    }
    return {user: user, date: date};
  },
  parseTalkSection: function(section) {
    var m1, m2, m3, o, text, posts = [], level = 0, index_to = 0;
    for (var i in section) {
      // We have a complete post if we're starting a new indent (":"), if we found a user
      // signature, or if we're the last element of the array
      m1 = section[i].match(/^(:+)(.*)/);
      o = vte.parseUser(section[i]);
      if (m1 !== null) {
        // Strip the colon from the beginning of the string
        section[i] = section[i].replace(/^(:+)/, "");
        text = section.slice(index_to, (parseInt(i)+1)).join("\n");
        index_to = (parseInt(i)+1);
        level = m1[1].length;
        o = vte.parseUser(text);
        posts.push({
          msg: text.trim(),
          user: o.user,
          date: o.date,
          level: level,
        });
      } else if (o.user != "Unknown") {
        text = section.slice(index_to, (parseInt(i)+1)).join("\n");
        index_to = (parseInt(i) + 1);
        posts.push({
          msg: text.trim(),
          user: o.user,
          date: o.date,
          level: 0,
        });
      } else if (i == section.length-1) {
        text = section.slice(index_to, i+1).join("\n");
        if (text == "") continue;
        index_to = (parseInt(i) + 1);
        o = vte.parseUser(text);
        posts.push({
          msg: text.trim(),
          user: o.user,
          date: o.date,
          level: 0,
        });
      }
    }
    return posts;
  },
  // parseTalk - Parses a talk page, returns object where key is section heading and value
  //   is an array of objects. Supports nested conversations.
  parseTalk: function(data, table) {
    // Go through the talk page text, build each talk object by section header
    var lines = data.split("\n");
    var obj = {}; var section = []; var title = ""; var p_title = ""; var post = "";
    for (var i in lines) {
      if (! lines[i]) continue;
      // If this is a new section, add the prior one to the return obj (if it exists)
      var m;
      m = lines[i].match(/^== ?(.+) ?== *$/);
      if (m !== null && section.length == 0) {
        title = m[1].trim();
      } else if (m !== null && section.length > 0) {
        obj[title] = vte.parseTalkSection(section);
        title = m[1].trim();
        section = [];
      } else {
        // Otherwise save the section text
        section.push(lines[i]);
      }
    }
    // And add the final section
    obj[title] = vte.parseTalkSection(section);
    return obj;
  },

  // Functions to populate the primary vte systems (ie, members, tasks, etc)
  clickMembers: function() {
    // Update the view cookie
    vte.setCookie("vte-view", "Members");
    // Clear the current content window
    $("#vte-window-members").html("");

    // Emit vte view
    vte_sock.emit("view", {
      name: mw.config.get("wgUserName"),
      time: new Date(),
      page: mw.config.get("wgTitle"),
      namespace: mw.config.get("wgNamespaceNumber"),
      project: $("#vte-window").data("vte-project").title,
      view: "Members"
    });

    console.log("vte - drawing member content");
    // We've already requested the Member content, draw page or wait for content to load
    var t = setTimeout(function() {
      var members = $("#vte-window").data("vte-project").members;
      if (typeof(members) !== 'undefined' && ! $.isEmptyObject(members)) {
        clearInterval(t);
        vte.drawMembers();
      }
    }, 100);
  },
  clickTasks: function() {
    // Update the view cookie
    vte.setCookie("vte-view", "Tasks");
    // Clear the current content window
    $("#vte-window-tasks").html("");

    // Emit vte view
    vte_sock.emit("view", {
      name: mw.config.get("wgUserName"),
      time: new Date(),
      page: mw.config.get("wgTitle"),
      namespace: mw.config.get("wgNamespaceNumber"),
      project: $("#vte-window").data("vte-project").title,
      view: "Tasks"
    });

    console.log("vte - drawing task content");
    // We've already requested the Task content, draw page or wait for content to load
    var t = setInterval(function() {
      var tasks = $("#vte-window").data("vte-project").tasks;
      if (typeof(tasks) !== 'undefined' && ! $.isEmptyObject(tasks)) {
        clearInterval(t);
        vte.drawTasks();
      }
    }, 100);
  },
  clickCommunication: function() {
    // Update the view cookie
    vte.setCookie("vte-view", "Communication");
    // Clear the current content window
    $("#vte-window-communication").html("");

    // Emit vte view
    vte_sock.emit("view", {
      name: mw.config.get("wgUserName"),
      time: new Date(),
      page: mw.config.get("wgTitle"),
      namespace: mw.config.get("wgNamespaceNumber"),
      project: $("#vte-window").data("vte-project").title,
      view: "Communication"
    });

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

    // TODO: Load wiki communication
    

    vte.drawCommunication();
  },

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

    // Grab the member list data
    var obj = $("#vte-window").data("vte-project").members;
    var talk = $("#vte-window").data("vte-project").members_talk;

    // Add the add member and import members buttons first
    $("#vte-window-members").append(
      "<div class='vte-members-create'>+ Add member</div>" +
      "<div class='vte-members-import'>+ Import members from project network</div>" +
      "<div id='vte-members-view'>View:All" + action + "</div>" +
      "<div id='vte-members-sort'>Sort:Created" + action + "</div>" +
      "<div style='clear: both;'></div>"
    );
    // Style the buttons
    $(".vte-members-create, .vte-members-import").css(s_vteMembersCreate);

    // If we don't have any members, prompt to import from project pages
    if (obj.struc.length == 0) {
      $("#vte-window-members").append(
        "<div class='vte-members-empty'>" +
        "  This project currently does not have any members listed.  " +
        "  Add members by clicking the '+ Add member' link, or import from project-related activity " +
        "  by clicking the '+ Import members from project network' link." +
        "</div>"
      );
      // Remove the view and sort dropdowns
      $("#vte-members-view, #vte-members-sort").remove();
      $(".vte-members-empty").css(s_vteMembersEmpty);
    }

    // Draw the members table
    $("#vte-window-members").append(
      "<table class='vte-members-table' cellpadding='0'>" +
      "  <colgroup>" +
      "    <col class='vte-members-table-name' />" +
      "    <col class='vte-members-table-since' />" +
      "    <col class='vte-members-table-proj' />" +
      "    <col class='vte-members-table-page' />" +
      "  </colgroup>" +
      "  <tbody class='vte-members-table-body'/>" +
      "</table>"
    );

    // And display all the current members
    for (var i in obj.struc) {
      var n = "name" in obj.struc[i] ? obj.struc[i].name : "";
      var proj = "project_edits" in obj.struc[i] ? obj.struc[i].project_edits : "";
      var page = "page_edits" in obj.struc[i] ? obj.struc[i].page_edits : "";
      var since = "member_since" in obj.struc[i] ? obj.struc[i].member_since : "";

      // Attempt to parse date
      var s_date = vte.parseDateStr(since);
      var s_str = vte.getMonthText(s_date.getMonth() + 1, {abbrev: 1}) + " " + s_date.getDate() + ", " + 
        s_date.getFullYear();

      // Distinguish between explicit members and activity-based members
      var explicit = ("activity" in obj.struc[i] && obj.struc[i].activity) ? "vte-member-activity" : "vte-member-explicit";

      // Display the row
      $(".vte-members-table-body").append(
        "<tr id=member-" + i + "' class='vte-members-row' vte-member-index='" + i + " " + explicit + "'>" +
        "  <td id='vte-members-table-name-" + i + "' class='vte-members-table-name vte-m-td'>" + n + "</div>" +
        "  <td id='vte-members-table-since-" + i + "' class='vte-members-table-since vte-m-td'>" + since + "</div>" +
        "  <td id='vte-members-table-proj-" + i + "' class='vte-members-table-proj vte-m-td'>" + proj + "</div>" +
        "  <td id='vte-members-table-page-" + i + "' class='vte-members-table-page vte-m-td'>" + page + "</div>" +
        "</tr>"
      );
    }
    // Style the table
    $(".vte-members-table").css(s_vteMembersTable);
    $(".vte-m-td").css(s_vteMembersRow);
    $("#vte-members-view, #vte-members-sort").css(s_vteMembersView);
    $("#vte-members-view").css(s_vteMembersView);

    // Action when clickinig the View or Sort links
    $("#vte-members-view").click(function(e) {
      vte.drawMembersView(e);
    });
    $("#vte-members-sort").click(function(e) {
      vte.drawMembersSort(e);
    });

    // Highlight row on hover
    $(".vte-members-row").hover(
      function() {
        $(this).css("background-color", "#EFF5FB");
      }, function() {
        $(this).css("background-color", "#FFFFFF");
      }
    );

    // Action to add a new member
    $(".vte-members-create").click(function(e) {
      // Draw the lightbox
      vte.drawMemberEdit();
    });
    // Action to edit details for an existing user
    $(".vte-members-row").click(function(e) {
      var index = $(e.currentTarget).attr("vte-member-index");
      vte.drawMemberEdit(index);
    });
    // Action to import member from project/page edits
    $(".vte-members-import").click(function(e) {
      vte.getMemberImportData();
    });
  },

  drawMembersView: function(e) {
    e.stopPropagation();
    // Draw the View window, supports choosing from All, Activity, or Explicit
    $("#vte-members-sort-actions").hide();
    if ($("#vte-members-view-actions").length == 0) {
      $("#vte-members-view").append(
        "<div id='vte-members-view-actions'>" +
        "  <div id='vte-members-view-all' class='vte-dropdown-item'>All</div>" +
        "  <div id='vte-members-view-activity' class='vte-dropdown-item'>Activity</div>" +
        "  <div id='vte-members-view-explicit' class='vte-dropdown-item'>Explicit</div>" +
        "</div>"
      );
      $("#vte-members-view-actions").css(s_vteDropdownList);
      $(".vte-dropdown-item").css(s_vteDropdownItem);
    } else {
      $("#vte-members-view-actions").show();
    }

    // Close the menu if clicking outside of it or hitting escape
    $("body, .vte-dropdown-item").one("click", function(e) {
      e.stopPropogation();
      var table = $(".vte-members-table");
      if (e.target.id == "vte-members-view-all") {
        $(".vte-member-activity").show();
        $(".vte-member-explicit").show();
      } else if (e.target.id == "vte-members-view-activity") {
        $(".vte-member-activity").show();
        $(".vte-member-explicit").hide();
      } else if (e.target.id == "vte-members-view-explicit") {
        $(".vte-member-activity").hide();
        $(".vte-member-explicit").show();
      }
      $("#vte-members-view-actions").hide();
    });
    $(document).on("keyup.hide_actions", function(e) {
      if (e.keyCode == 27) {
        $("#vte-members-view-actions").hide();
        $(document).unbind("keyup.hide_actions");
      }
    });
  },

  drawMembersSort: function(e) {
    e.stopPropagation();
    // Draw the sort window, supports sorting by name, since, project edits, page edits, etc
    $("#vte-members-view-actions").hide();
    if ($("#vte-members-sort-actions").length == 0) {
      $("#vte-members-sort").append(
        "<div id='vte-members-sort-actions'>" +
        "  <div id='vte-members-sort-name' class='vte-dropdown-item'>Name</div>" +
        "  <div id='vte-members-sort-since' class='vte-dropdown-item'>Member Since</div>" +
        "  <div id='vte-members-sort-proj' class='vte-dropdown-item'>Project Edits</div>" +
        "  <div id='vte-members-sort-page' class='vte-dropdown-item'>Page Edits</div>" +
        "</div>"
      );
      $("#vte-members-sort-actions").css(s_vteDropdownList);
      $(".vte-dropdown-item").css(s_vteDropdownItem);
    } else {
      $("#vte-members-sort-actions").show();
    }

    // Close the menu if clicking outside of it or hitting escape
    $("body, .vte-dropdown-item").one("click", function(e) {
      e.stopPropagation();
      var table = $(".vte-members-table");
      if (e.target.id == "vte-members-sort-name") {
        var rows = table.find('tr').toArray().sort(vte.comparer(0));
        // Determine if we're ascending or descending
        $(".vte-members-table").data("name", !$(".vte-members-table").data("name"));
        if (!$(".vte-members-table").data("name")) rows = rows.reverse();
        for (var i = 0; i < rows.length; i++) { table.append(rows[i]); }
        $("#vte-members-sort").html("Sort:Name" + action);
      } else if (e.target.id == "vte-members-sort-since") {
        var rows = table.find('tr').toArray().sort(vte.comparer(1));
        $(".vte-members-table").data("since", !$(".vte-members-table").data("since"));
        if (!$(".vte-members-table").data("since")) rows = rows.reverse();
        for (var i = 0; i < rows.length; i++) { table.append(rows[i]); }
        $("#vte-members-sort").html("Sort:Member Since" + action);
      } else if (e.target.id == "vte-members-sort-proj") {
        var rows = table.find('tr').toArray().sort(vte.comparer(2));
        $(".vte-members-table").data("proj", !$(".vte-members-table").data("proj"));
        if (!$(".vte-members-table").data("since")) rows = rows.reverse();
        for (var i = 0; i < rows.length; i++) { table.append(rows[i]); }
        $("#vte-members-sort").html("Sort:Project Edits" + action);
      } else if (e.target.id == "vte-members-sort-page") {
        var rows = table.find('tr').toArray().sort(vte.comparer(3));
        $(".vte-members-table").data("page", !$(".vte-members-table").data("page"));
        if (!$(".vte-members-table").data("page")) rows = rows.reverse();
        for (var i = 0; i < rows.length; i++) { table.append(rows[i]); }
        $("#vte-members-sort").html("Sort:Page Edits" + action);
      }
      $("#vte-members-sort-actions").hide();
    });
    $(document).on('keyup.hide_actions', function(e) {
      if (e.keyCode == 27) {
        $("#vte-members-sort-actions").hide();
        $(document).unbind("keyup.hide_actions");
      }
    });
  },

  drawMemberEdit: function(index) {
    console.log("in drawMemberEdit");
  },

  getMemberImportData: function() {
    console.log("in getMemberImportData");
    // Grab any potential current member data
    var vte_project = $("#vte-window").data("vte-project");

    // Draw the import lightbox
    $("#vte-window").append(
      "<div id='vte-member-import'>" +
      "  <div id='vte-import-close'>" +
      "    <img src='https://upload.wikimedia.org/wikipedia/commons/6/60/Close_icon.svg' width='25' height='25'>" +
      "  </div>" +
      "  <input type='submit class='vte-import-save' value='Import' />" +
      "  <div style='margin-top: 10px; clear: both;'>&nbsp;</div>" +
      "  <div id='vte-import-loading-links' class='vte-loading'>Loading user links on project pages...</div>" +
      "  <div id='vte-import-loading-proj' class='vte-loading'>Loading edits to project pages...</div>" +
      "  <div id='vte-import-loading-page' class='vte-loading'>Loading project pages...</div>" +
      "  <div id='vte-import-loading-edits' class='vte-loading'>Loading edits by top project editors...</div>" +
      "  <table style=''>" +
      "    <colgroup>" +
      "      <col class='vte-import-check'/>" +
      "      <col class='vte-import-name'/>" +
      "      <col class='vte-import-proj'/>" +
      "      <col class='vte-import-page'/>" +
      "      <col class='vte-import-user-link'/>" +
      "    </colgroup>" +
      "    <tbody class='vte-import-table-body'/>" +
      "  </table>" +
      "</div>"
    );
    // Style the lightbox
    $("#vte-member-import").css(s_vteMemberImport)
    $("#vte-import-loading-links, #vte-import-loading-proj, #vte-import-loading-page, #vte-import-loading-edits").css(s_vteMemberImportLoading);

    // Handle the loading text if we're still requesting the data, located in vte_project.members_network
    var elapsed = 0;
    var t = setInterval(function() {
      if ("members_network" in vte_project) {
        clearInterval(t);
        vte.drawMemberImport();
      } else {
        // Check for timeout
        if (elapsed >= 60000) {
          clearInterval(t);
          console.error("Timed out requesting project member data");
        }
      }
      elapsed += 100;
    }, 100);
    
  },

  drawMemberImport: function(data) {
    console.log("In drawMemberImport");
    // Data will contain {editors: [], links: {}, p_pages: {}, pages: []} in vte_project.members_network
    var vte_project = $("#vte-window").data("vte-project");
    var network = vte_project.members_network;
    var members = vte_project.members.struc;
    $(".vte-loading").remove();

    // Structure the network data - all project edits will be included, and all users with links
    // on project pages.  We can ignore network.[p_pages|pages] at this point.
    

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

  },

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

    // Grab the task list data
    var obj = $("#vte-window").data("vte-project").tasks;
    var talk = $("#vte-window").data("vte-project").tasks_talk;

    // Projects have the option to include anything in the tasks table, but for the
    // VTE we'll want to display the title, created, due, priority, and owner. In
    // the task details we'll additionally display subtasks, burndown, etc.

    // Add the create task button first
    $("#vte-window-tasks").append(
      "<div class='vte-tasks-create'>+ Add task</div>" +
      "<div id='vte-tasks-view'>View:All" + action + "</div>" +
      "<div id='vte-tasks-sort'>Sort:Created" + action + "</div>" +
      "<div style='clear: both;'></div>"
    );
    $("#vte-tasks-view, #vte-tasks-sort").css(s_vteTasksView);

    // If we don't have any tasks, prompt to create a new one
    if (obj.struc.length == 0) {
      $("#vte-window-tasks").append(
        "<div class='vte-tasks-empty'>" +
        "  This project currently does not have any tasks listed.  " +
        "  Add tasks by clicking the '+ Add task' link." +
        "</div>"
      );
      // Remove the view and sort dropdowns
      $("#vte-tasks-view, #vte-members-sort").remove();
      $(".vte-tasks-empty").css(s_vteTasksEmpty);
    }


    // Will display created date, priority, title, number of comments, and owner
    // Created color will be based on date since creation
    // Priority color will be based on priority (either high/medium/low or 1/2/3)
    // Font color will be based on whether the task is completed
    $("#vte-window-tasks").append(
      "<table class='vte-tasks-table' cellpadding='0'>" +
      "  <colgroup>" +
      "    <col class='vte-tasks-table-created'>" +
      "    <col class='vte-tasks-table-priority'>" +
      "    <col class='vte-tasks-table-title'>" +
      "    <col class='vte-tasks-table-comments'>" +
      "    <col class='vte-tasks-table-owner'>" +
      "  </colgroup>" +
      "  <tbody class='vte-tasks-table-body'/>" +
      "</table>"
    );

    var closed = 0;
    var open = 0;
    for (var i in obj.struc) {
      var t = "title" in obj.struc[i] ? obj.struc[i].title : "";
      var c = "created" in obj.struc[i] ? obj.struc[i].created : "";
      var d = "due" in obj.struc[i] ? obj.struc[i].due : "";
      var p = "priority" in obj.struc[i] ? obj.struc[i].priority : "";
      var o = "owner" in obj.struc[i] ? obj.struc[i].owner : "";

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

      // Attempt to parse dates
      var c_date = vte.parseDateStr(c);
      var c_str = vte.getMonthText(c_date.getMonth() + 1, {abbrev: 1}) + " " + c_date.getDate() + ", " + 
        c_date.getFullYear();
      var n_date = new Date();
      var n_str = vte.getMonthText(n_date.getMonth() + 1, {abbrev: 1}) + " " + n_date.getDate() + ", " +
        n_date.getFullYear();

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

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

      // Color of the creation date will be red for older open tasks, going towards black for
      // newer tasks.  Color progression will be for each week going back one month (ie, tasks
      // created in the last week will be black, two weeks ago will be slightly red, etc).
      // If we have a due date for this task, color will still go from black to red, but color
      // steps will be between the current date and creation date and due date (ie, background
      // color will get more red the closer we are to the due date, split into four equal time increments).
      // If the due date passed, the color will be red.
      var c_color;
      if (d_date) {
        var inc = (d_date.getTime() - c_date.getTime()) / 4;
        var spent = n_date.getTime() - c_date.getTime();
        if (d_date.getTime() < n_date.getTime()) {
          c_color = "#FF0400";
        } else if (Math.ceil(spent / inc) == 4) {
          c_color = "#FF0400";
        } else if (Math.ceil(spent / inc) == 3) {
          c_color = "#BA0300";
        } else if (Math.ceil(spent / inc) == 2) {
          c_color = "#590200";
        } else {
          c_color = "#000000";
        }
      } else {
        var w = 1000 * 60 * 60 * 24 * 7;
        if (n_date.getTime() - c_date.getTime() > w * 3) {
          c_color = "#FF0400";
        } else if (n_date.getTime() - c_date.getTime() > w * 2) {
          c_color = "#BA0300";
        } else if (n_date.getTime() - c_date.getTime() > w) {
          c_color = "#590200";
        } else {
          c_color = "#000000";
        }
      }
      // Or, if we've already completed the task created background should just be black
      if (comp == "vte-task-completed") c_color = "#000000";

      //c = vte.getDateStr( vte.parseDateStr(c) );
      //d = vte.getDateStr( vte.parseDateStr(d) );
      // Parse any wikitext in the title
      //t = wiky.process( t ); // Didn't work
      t = InstaView.convert( t ).slice(3); // Removing first 4 characters, InstaView adds <p> to everything.
      
      // Display the row
      $(".vte-tasks-table-body").append(
        "<tr id='task-" + i + "' class='vte-tasks-row " + comp + "' vte-task-index='" + i + "'>" +
        "  <td id='vte-tasks-table-created-" + i + "' class='vte-tasks-table-created vte-t-td' style='background-color: " + c_color + "; color: #FFF'>" + c_str + "</td>" +
        "  <td id='vte-tasks-table-priority-" + i + "' class='vte-tasks-table-priority vte-t-td'>" + p + "</td>" +
        "  <td id='vte-tasks-table-title-" + i + "' class='vte-tasks-table-title vte-t-td'>" + t + "</td>" +
        "  <td id='vte-tasks-table-comments-" + i + "' class='vte-tasks-table-comments vte-t-td'>" + com + " comments</td>" +
        "  <td id='vte-tasks-table-owner-" + i + "' class='vte-tasks-table-owner vte-t-td'>" + o + "</td>" +
        "</tr>"
      );
    }
    
    // Style the tables
    $(".vte-tasks-table").css(s_vteTasksTable);
    $(".vte-t-td").css(s_vteTasksRow);
    $(".vte-task-completed").css(s_vteTaskCompleted);
    $(".vte-tasks-table-title").css(s_vteTasksTableTitle);
    $(".vte-tasks-table-priority").css(s_vteTasksTablePriority);
    $(".vte-tasks-table-created").css(s_vteTasksTableCreated);
    $(".vte-tasks-table-comments").css(s_vteTasksTableComments);
    $(".vte-tasks-table-owner").css(s_vteTasksTableOwner);
    $(".vte-tasks-table-due").css(s_vteTasksTableDue);
    $(".oh, .ch").css({ "cursor": "pointer", "padding": "4px 0px" });
    $(".vte-tasks-create").css(s_vteTasksCreate);

    // Action when clicking the View or Sort links
    $("#vte-tasks-view").click(function(e) {
      vte.drawTasksView(e);
    });
    $("#vte-tasks-sort").click(function(e) {
      vte.drawTasksSort(e);
    });

    // Action to highlight row on hover
    $(".vte-tasks-row").hover(
      function() {
        $(this).css("background-color", "#EFF5FB");
      }, function() {
        $(this).css("background-color", "#FFFFFF");
      }
    );

    // Make the table sortable by clicking the headers
    $('.oh, .ch').click(function() {
      $(".oh, .ch").css("background-color", "#FFFFFF");
      $( this ).css("background-color", "#F2F2F2");
      var table = $(this).parents('table').eq(0);
      var rows = table.find('tr:gt(0)').toArray().sort(vte.comparer($(this).index()));
      this.asc = !this.asc;
      if (!this.asc) rows = rows.reverse();
      for (var i = 0; i < rows.length; i++) { table.append(rows[i]); }
    });

    // Action to add new task
    //$(".vte-tasks-create").button().click(function(e) {
    $(".vte-tasks-create").click(function(e) {
      e.preventDefault();
      // Draw the lightbox
      vte.drawTaskEdit();
    }); // END submit new task

    // Action to edit an existing task
    $(".vte-tasks-row").click(function(e) {
      var index = $(e.currentTarget).attr("vte-task-index");
      vte.drawTaskEdit(index);
    });
  }, 
  drawTasksView: function(e) {
    e.stopPropagation();
    // Draw the View window, supports choosing from All, Open, or Closed
    $("#vte-tasks-sort-actions").hide();
    if ($("#vte-tasks-view-actions").length == 0) {
      $("#vte-tasks-view").append(
        "<div id='vte-tasks-view-actions'>" +
        "  <div id='vte-tasks-view-all' class='vte-dropdown-item'>All</div>" +
        "  <div id='vte-tasks-view-open' class='vte-dropdown-item'>Open</div>" +
        "  <div id='vte-tasks-view-closed' class='vte-dropdown-item'>Closed</div>" +
        "</div>"
      );
      $("#vte-tasks-view-actions").css(s_vteDropdownList);
      $(".vte-dropdown-item").css(s_vteDropdownItem);
    } else {
      $("#vte-tasks-view-actions").show();
    }
    
    // Close the menu if clicking outside of it or hitting escape
    $("body, #vte-tasks-view-actions").one("click", function(e) {
      e.stopPropagation();
      if (e.target.id == "vte-tasks-view-all") {
        $(".vte-task-open").show();
        $(".vte-task-completed").show();
      } else if (e.target.id == "vte-tasks-view-open") {
        $(".vte-task-open").show();
        $(".vte-task-completed").hide();
      } else if (e.target.id == "vte-tasks-view-closed") {
        $(".vte-task-open").hide();
        $(".vte-task-completed").show();
      }
      $("#vte-tasks-view-actions").hide();
    });
    $(document).on('keyup.hide_actions', function(e) {
      if (e.keyCode == 27) {
        $("#vte-tasks-view-actions").hide();
        $(document).unbind('keyup.hide_actions');
      }
    });
  },
  drawTasksSort: function(e) {
    e.stopPropagation();
    // Draw the Sort window, supports sorting by Created date, priority, title, comments, owner, etc
    $("#vte-tasks-view-actions").hide();
    if ($("#vte-tasks-sort-actions").length == 0) {
      $("#vte-tasks-sort").append(
        "<div id='vte-tasks-sort-actions'>" +
        "  <div id='vte-tasks-sort-created' class='vte-dropdown-item'>Created</div>" +
        "  <div id='vte-tasks-sort-priority' class='vte-dropdown-item'>Priority</div>" +
        "  <div id='vte-tasks-sort-title' class='vte-dropdown-item'>Title</div>" +
        "  <div id='vte-tasks-sort-comments' class='vte-dropdown-item'>Comments</div>" +
        "  <div id='vte-tasks-sort-owner' class='vte-dropdown-item'>Owner</div>" +
        "</div>"
      );
      $("#vte-tasks-sort-actions").css(s_vteDropdownList);
      $(".vte-dropdown-item").css(s_vteDropdownItem);
    } else {
      $("#vte-tasks-sort-actions").show();
    }

    // Close the menu if clicking outside of it or hitting escape
    $("body, .vte-dropdown-item").one("click", function(e) {
      e.stopPropagation();
      var table = $(".vte-tasks-table");
      if (e.target.id == "vte-tasks-sort-created") {
        console.log("Sorting created");
        var rows = table.find('tr').toArray().sort(vte.comparer(0));
        // Determine if we're ascending/descending
        $(".vte-tasks-table").data("created", !$(".vte-tasks-table").data("created"));
        if (!$(".vte-tasks-table").data("created")) rows = rows.reverse();
        for (var i = 0; i < rows.length; i++) { table.append(rows[i]); }
        $("#vte-tasks-sort").html("Sort:Created" + action);
      } else if (e.target.id == "vte-tasks-sort-priority") {
        console.log("Sorting priority");
        var rows = table.find('tr').toArray().sort(vte.comparer(1));
        $(".vte-tasks-table").data("priority", !$(".vte-tasks-table").data("priority"));
        if (!$(".vte-tasks-table").data("priority")) rows = rows.reverse();
        for (var i = 0; i < rows.length; i++) { table.append(rows[i]); }
        $("#vte-tasks-sort").html("Sort:Priority" + action);
      } else if (e.target.id == "vte-tasks-sort-title") {
        console.log("Sorting title");
        var rows = table.find('tr').toArray().sort(vte.comparer(2));
        $(".vte-tasks-table").data("title", !$(".vte-tasks-table").data("title"));
        if (!$(".vte-tasks-table").data("title")) rows = rows.reverse();
        for (var i = 0; i < rows.length; i++) { table.append(rows[i]); }
        $("#vte-tasks-sort").html("Sort:Title" + action);
      } else if (e.target.id == "vte-tasks-sort-comments") {
        console.log("Sorting comments");
        var rows = table.find('tr').toArray().sort(vte.comparer(3));
        $(".vte-tasks-table").data("comments", !$(".vte-tasks-table").data("comments"));
        if (!$(".vte-tasks-table").data("comments")) rows = rows.reverse();
        for (var i = 0; i < rows.length; i++) { table.append(rows[i]); }
        $("#vte-tasks-sort").html("Sort:Comments" + action);
      } else if (e.target.id == "vte-tasks-sort-owner") {
        console.log("Sorting owner");
        var rows = table.find('tr').toArray().sort(vte.comparer(4));
        $(".vte-tasks-table").data("owner", !$(".vte-tasks-table").data("owner"));
        if (!$(".vte-tasks-table").data("owner")) rows = rows.reverse();
        for (var i = 0; i < rows.length; i++) { table.append(rows[i]); }
        $("#vte-tasks-sort").html("Sort:Owner" + action);
      }
      $("#vte-tasks-sort-actions").hide();
    });
    $(document).on('keyup.hide_actions', function(e) {
      if (e.keyCode == 27) {
        $("#vte-tasks-sort-actions").hide();
        $(document).unbind('keyup.hide_actions');
      }
    });
  },
  populateChat: function() {
    var project = typeof($("#vte-window").data("vte-project")) !== 'undefined' ? 
      $("#vte-window").data("vte-project").title : "";
    // Draw the chat form
    $("#vte-window-left-chat").append(
      "<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: 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").title;

    // Clear the current content window and draw the chat form
    $("#vte-window-communication").html("WIP - Communication system");
  },

  // drawTaskEdit: Draws the task edit lightbox.  Will prepopulate with task info if an existing
  //   task was clicked, otherwise will draw the empty box to create a new task.
  drawTaskEdit: function(index) {
    var obj = $("#vte-window").data("vte-project").tasks;
    var task = {};
    var complete_button = "";
    // If we're given an index, pull out the data for that task
    if (typeof(index) !== 'undefined') { 
      task = obj.struc[index];
      complete_button = "<input type='submit' class='vte-task-mark-complete' value='Mark Complete' index='" + index + "'/>";
    }
    // Make sure task has required fields
    if (!("title" in task)) task.title = "";
    if (!("page" in task)) task.page = "";
    if (!("priority" in task)) task.priority = "";
    if (!("remaining" in task)) task.remaining = "";
    if (!("due" in task)) task.due = "";
    if (!("notes" in task)) task.notes = "";
    if (!("owner" in task)) task.owner = "";

    // Draw the lightbox
    $("#vte-window").append(
      "<div id='vte-task-edit'>" +
      "  <div id='vte-task-close'>" +
      "    <img src='https://upload.wikimedia.org/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.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.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.priority == 0 ? "SELECTED" : "") + ">0 (most urgent)</option>" +
      "            <option value='1' " + (task.priority == 1 ? "SELECTED" : "") + ">1</option>" +
      "            <option value='2' " + (task.priority == 2 ? "SELECTED" : "") + ">2</option>" +
      "            <option value='3' " + (task.priority == 3 ? "SELECTED" : "") + ">3</option>" +
      "            <option value='4' " + (task.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.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.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.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 = "owner" in task ? task.owner.split(",").map( function(str) { return str.trim(); } ) : [];
    for (var i in owners) {
      if (owners[i] == "") continue;
      $(".vte-task-edit-owners").append(
        "<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' />"
    );

    // And check for any/all subtasks
    var i = 0;
    while ("subtask" + i in task) {
      $(".vte-task-edit-subtasks").append(
        "<div class='vte-subtask-row'>" +
        "  <input type='checkbox' index='" + i + "' class='vte-task-edit-subcomplete' />" +
        "  <div class='vte-task-edit-subtask' index='" + i + "'>" + task["subtask" + i] + "</div>" +
        "</div>"
      );
      if ("subcomplete" + i in task && task["subcomplete" + i])
        $("#vte-task-edit-subtask-" + i).prop("checked", true);
      i += 1;
    }
    // And then add the subtasks edit field
    $(".vte-task-edit-subtasks").append(
      "<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 = "owner" in task ? task.owner.split(",").length : 0;
      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.owner.split(",").splice(index, 1);
      $("[vte-owner-index='" + index + "']").remove();
    }
    function addSubtask() {
      // find the next subtask index
      var index = 0;
      while ("subtask" + index in task) index += 1;
      var $html = $(
        "<div class='vte-subtask-row' vte-subtask-index='" + index + "'>" +
        "  <input type='checkbox' index='" + index + "' class='vte-task-edit-subcomplete' />" +
        "  <div class='vte-task-edit-subtask' index='" + index + "'>" + $("#vte-task-subtask").val() + "</div>" +
        "</div>"
      );
      task["subtask" + index] = $("#vte-task-subtask").val();
      $("#vte-task-subtask").val("");
      $("#vte-task-edit-subtask-input").before($html);
      $(".vte-task-edit-subtask").css(s_vteTaskEditSubtask);
      $(".vte-task-edit-subcomplete").css(s_vteTaskEditSubcomplete);
      $(".vte-subtask-row").css(s_vteSubtaskRow);
      $("#vte-task-edit input[type='submit']").css("font-size", "10px");
    }

    // Handle the add owner action
    $(".vte-task-edit-add-owner").button().click(addOwner);
    // Handle the remove owner action
    $(".vte-task-edit-remove-owner").button().click(removeOwner);

    // Handle the add subtask action
    $(".vte-task-edit-add-subtask").button().click(addSubtask);
    // Nothing needed to complete the subtask - we'll check the checkbox for each task on save

    // Handle the mark complete and save actions
    $(".vte-task-mark-complete, .vte-task-save").button().click(function(e) {
      e.preventDefault();
      // Task title is required
      if (! $("#vte-task-title").val()) {
        mw.notify("You must enter a Task Title before saving the task.");
        console.warn("vte: You must enter a Task Title before saving the task.");
        return false;
      }
      var obj = $("#vte-window").data("vte-project").tasks;

      // Update the completed date if we've clicked Mark Complete
      var index = $(e.currentTarget).attr("index");
      if ($(e.currentTarget).attr("value") == "Mark Complete") {
        var d = new Date();
        obj.struc[index].completed = vte.getDateStr();
      }
      // Add the created time if this is a new task
      if (typeof(index) === 'undefined') {
        task.created = vte.getWikiDateStr();
        obj.struc.push(task);
        index = obj.struc.length-1;
      }
      // Update the task object with the other values
      obj.struc[index].title = $("#vte-task-title").val();
      obj.struc[index].page = $("#vte-task-page").val();
      obj.struc[index].priority = $("#vte-task-priority").val();
      obj.struc[index].remaining = $("#vte-task-remaining").val();
      obj.struc[index].due = $("#vte-task-due").val();
      obj.struc[index].notes = $("#vte-task-notes").val();
      var owner = [];
      $(".vte-task-edit-owner").each(function() {
        owner.push($(this).html().trim());
      });
      obj.struc[index].owner = owner.join(",");
      $(".vte-task-edit-subtask").each(function() {
        obj.struc[index]["subtask" + $(this).attr("index")] = $(this).html();
      });
      $(".vte-task-edit-subcomplete").each(function() {
        obj.struc[index]["subcomplete" + $(this).attr("index")] = $(this).prop("checked") == true ? 1 : 0;
      });

      // Save the struc and call the update function for both the tasks page and the tasks talk page
      var vte_project = $("#vte-window").data("vte-project");
      vte_project.tasks = obj;
      vte_project.tasks_talk[ $("#vte-task-title").val() ] = [];
      var complete = {task: 0, talk: 0};
      vte.updateTaskData(function() {
        complete.task = 1;
        console.log("Successfully updated details for task: " + $("#vte-task-title").val());
        mw.notify( "Successfully updated task: " + $("#vte-task-title").val() + "." );
      }, function(xhr) {
        complete.task = 1;
        console.error("Failed to update details for task: " + JSON.stringify(xhr));
        mw.notify( "Failed to update details for task: " + JSON.stringify(xhr));
      });
      vte.updateTaskTalkData(function() {
        complete.talk = 1;
        console.log("Successfully updated talk page for task: " + $("#vte-task-title").val());
      }, function(xhr) {
        complete.talk = 1;
        console.error("Failed to update talk page for task: " + JSON.stringify(xhr));
      });
      var timeout = 0;
      var t1 = setInterval(function() {
        timeout += 100;
        if (complete.task == 1 && complete.talk == 1) {
          clearInterval(t1);
          $("#vte-tasks").click();
          $("#vte-task-edit").remove();
        }
        if (timeout >= 10000) {
          clearInterval(t1);
          console.error("Timed out attempting to save Tasks and Tasks Talk pages: " + JSON.stringify(complete));
        }
      }, 100);

    });
    // Handle the close action
    $("#vte-task-close").click(function() {
      $("#vte-task-edit").remove();
    });

  },

  // drawUserEdits: Will structure and graph user edits over time, separated by namespace
  drawUserEdits: function(data) {
    // Structure the data for the graph
    var sw = ew = vte.convertDateToWikiWeek();
    for (var i in data["result"]) if (data["result"][i].rc_wikiweek < sw) sw = data["result"][i].rc_wikiweek;
    var edits = Array(ew - sw);
    for (var i = 0; i < edits.length; i++) edits[i] = {
      date: vte.convertWikiWeekToDate(i), 0:0, 1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 
      8:0, 9:0, 10:0, 11:0, 12:0, 13:0, 14:0, 15:0
    };
    var y_max = 0;
    for (var i in data["result"]) {
      edits[ data["result"][i].rc_wikiweek - sw ][ data["result"][i].rc_page_namespace ] += 
        data["result"][i]["rc_edits"];
      if (data["result"][i].rc_edits > y_max) y_max = data["result"][i].rc_edits;
    }

    // Draw with d3
    var margin = {top: 20, right: 80, bottom: 50, left: 50};
    var w = $("#vte-members-contribution").width() - margin.left - margin.right - 20,
        h = 230 - margin.top - margin.bottom;
    var parseDate = d3.time.format("%Y%m%d").parse;
    var x = d3.time.scale()
        .range([0, w]);
    var y = d3.scale.linear()
        .range([h, 0]);
    var color = d3.scale.category10();
    var xAxis = d3.svg.axis()
        .scale(x)
        .orient("bottom");
    var yAxis = d3.svg.axis()
        .scale(y)
        .orient("left");
    var line = d3.svg.line()
        .interpolate("basis")
        .x(function(d) { return x(d.date); })
        .y(function(d) { return y(d.count); });
        //.attr("shape-rendering", "crispEdges");

    var svg = d3.select("#vte-members-contribution-edits").append("svg")
        .attr("width", w + margin.left + margin.right)
        .attr("height", h + margin.top + margin.bottom)
      .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    color.domain(d3.keys(edits[0]).filter( function(key) { return key !== "date"; }));
    edits.forEach(function(d) {
      d.date = parseDate(d.date);
    });
    var namespaces = color.domain().map(function(ns) {
      return {
        namespace: vte.convertIdToNamespace(ns),
        values: edits.map(function(d) {
          return {date: d.date, count: +d[ns]};
        })
      };
    });
    x.domain(d3.extent(edits, function(d) { return d.date; }));
    y.domain([
      d3.min(namespaces, function(c) { return d3.min(c.values, function(v) { return v.count; }); }),
      d3.max(namespaces, function(c) { return d3.max(c.values, function(v) { return v.count; }); })
    ]);

    svg.append("g")
        .style("fill", "none")
        .style("stroke", "#000")
        .style("shape-rendering", "crispEdges")
        .attr("transform", "translate(0," + h + ")")
        .call(xAxis);
    svg.append("g")
        .style("fill", "none")
        .style("stroke", "#000")
        .style("shape-rendering", "crispEdges")
        .call(yAxis)
      .append("text")
        .attr("transform", "rotate(-90)")
        .attr("y", 6)
        .attr("dy", ".70em")
        .style("text-anchor", "end")
        .text("Edits");

    var ns = svg.selectAll(".ns")
        .data(namespaces)
      .enter().append("g")
        .attr("class", "ns");
    ns.append("path")
        .style("fill", "none")
        .style("stroke", "steelblue")
        .style("stroke-width", "1.5px")
        .attr("d", function(d) { return line(d.values); })
        .style("stroke", function(d) { return color(d.namespace); });
    ns.append("text")
        .datum(function(d) { 
          // Return null string if the last value was 0 (to avoid overlap)
          if (d.values[d.values.length -1].count == 0) {
            return { namespace: "", value: d.values[d.values.length - 1] };
          } else {
            return { namespace: d.namespace, value: d.values[d.values.length - 1]}; 
          }
        })
        .attr("transform", function(d) { 
          return "translate(" + x(d.value.date) + "," + y(d.value.count) + ")"; 
        })
        .attr("x", 3)
        .attr("dy", ".35em")
        .text(function(d) { return d.namespace; });
    // And then fix the labels on the axes (needed since the ticks and text are defined at the same time above)
    $("#vte-members-contribution-edits > svg text").css({"stroke": "none", "fill": "#000"});
  },

  //
  // HELPER FUNCTIONS
  //

  // processWikiText - Converts WikiText to valid HTML - This could be done in a call to 
  //   the MediaWiki API, but that seems like it would be less efficient for the many
  //   small cases we would require it for (i.e., making an API request for every Title
  //   and Description field for each Task for a given project).
  //   Ie, Mediawiki API - https://www.mediawiki.org/wiki/API:Parsing_wikitext
  // Currently using InstaView as Wiky didn't suit what we needed, so this may be unnecessary.
  processWikiText: function(str) {
    
  },

  // Helper functions to sort table by clicking on the header 
  // (see http://stackoverflow.com/questions/3160277/jquery-table-sort)
  comparer: function(index) {
    return function(a, b) {
        var valA = vte.getCellValue(a, index), valB = vte.getCellValue(b, index)
        return $.isNumeric(valA) && $.isNumeric(valB) ? valA - valB : valA.localeCompare(valB)
    }
  }, 
  getCellValue: function(row, index) {
    return $(row).children('td').eq(index).html();
  },

  // parseDateStr - Given a string, will attempt to parse and create a date object
  parseDateStr: function(str) {
    if (typeof(str) === 'undefined') return new Date();
    var m=null;
    m = str.match(/(\d+):(\d+), (\d+) (\S+) (\d+)/);
    if (m !== null) return new Date(m[5] + "-" + vte.getMonth(m[4]) + "-" + m[3] + " " + m[1] + ":" + m[2] + ":00");
    // Any additional string formats we want to check?

    // Try and parse the string
    var d = new Date(str);
    if (isNaN(d.getTime())) {
      return str;
    } else {
      return d;
    }
  },
  // Checks to see if the supplied argument is a valid date string
  isValidDate: function(d) {
    if ( Object.prototype.toString.call(d) !== "[object Date]" )
      return false;
    return !isNaN(d.getTime());
  },
  // getDateStr - Given a date object, returns a string like YYYY/mm/dd hh:mm:ss. If no date
  // is given will return the string for the current time.  If the date object isn't valid,
  // just returns the supplied argument.
  getDateStr: function(d) {
    if (typeof(d) === 'undefined') {
      d = new Date();
    }
    if (! vte.isValidDate(d)) return d;
    return String(d.getFullYear()) + "/" + String(vte.pad( parseInt(d.getMonth()) + 1, 2)) + "/" + String(vte.pad(d.getDate(), 2)) + " " + String(vte.pad(d.getHours(), 2)) + ":" + String(vte.pad(d.getMinutes(), 2)) + ":" + String(vte.pad(d.getSeconds(), 2));
  },
  // getWikiDateStr - Given a date object, returns a wiki-fied date string (the same
  // format that is saved if users enter ~~~~~, ie, "13:15, 14 October 2014 (UTC)")
  getWikiDateStr: function(d) {
    if (typeof(d) === 'undefined') {
      d = new Date();
    }
    return String(vte.pad(d.getUTCHours(), 2)) + ":" + String(vte.pad(d.getUTCMinutes(), 2)) + ", " + String(d.getUTCDate()) + " " + vte.getMonthText(d.getUTCMonth() + 1) + " " + String(d.getUTCFullYear()) + " (UTC)";
  },

  getMonth: function(m) {
    var months = { "January": 1, "February": 2, "March": 3, "April": 4,
      "May": 5, "June": 6, "July": 7, "August": 8, "September": 9,
      "October": 10, "November": 11, "December": 12
    };
    if (! (m in months)) {
      console.error("Invalid month: " + m);
    }
    return months[m];
  },
  getMonthText: function(m, opt) {
    if (typeof opt === 'undefined') opt = {};
    var months = {};
    if ("abbrev" in opt && opt.abbrev) {
      months = {1: "Jan", 2: "Feb", 3: "Mar", 4: "Apr",
        5: "May", 6: "Jun", 7: "Jul", 8: "Aug", 9: "Sept",
        10: "Oct", 11: "Nov", 12: "Dec"
      };
    } else {
      months = {1: "January", 2: "February", 3: "March", 4: "April",
        5: "May", 6: "June", 7: "July", 8: "August", 9: "September",
        10: "October", 11: "November", 12: "December"
      };
    }
    if (! (m in months)) {
      console.error("Invalid month number: " + m);
    }
    return months[m];
  },

  // convertDateToWikiWeek - helper function to convert a date of the form YYYYmmdd to wikiweek
  convertDateToWikiWeek: function(d) {
    if (typeof(d) === 'undefined') {
      var date = new Date();
      d = String(date.getFullYear()) + String(vte.pad( parseInt(date.getMonth()) + 1, 2)) + String(vte.pad(date.getDate(), 2));
    }
    var ms = new Date(d.substring(0,4) + '/' + d.substring(4,6) + '/' + d.substring(6,8) + ' 00:00:00').getTime();
    var originMs = new Date('2001/01/01 00:00:00').getTime();
    var msDiff = ms - originMs;
    // milliseconds in a week
    var week = 7 * 24 * 60 * 60 * 1000;
    // weeks in the millisecond range
    return Math.floor(msDiff / week);
  },
  pad: function(n, width, z) {
    z = z || '0';
    n = n + '';
    return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n;
  },
  convertWikiWeekToDate: function(ww) {
    // milliseconds in wiki weeks
    var ms = ww * 7 * 24 * 60 * 60 * 1000;
    // Add milliseconds since the epoch to week ms value
    var mil = new Date('2001/01/01 00:00:00').getTime() + ms;
    var date = new Date(mil);

    // Date will be of form YYYYmmdd
    return String(date.getFullYear()) + String(vte.pad( parseInt(date.getMonth()) + 1, 2)) + String(vte.pad(date.getDate(), 2));
  },
  convertIdToNamespace: function(id) {
    var ns = {
      0: "Article", 1: "Article talk", 2: "User", 3: "User talk",
      4: "Wikipedia", 5: "Wikipedia talk", 6: "File", 7: "File talk",
      8: "MediaWiki", 9: "MediaWiki talk", 10: "Template", 11: "Template talk",
      12: "Help", 13: "Help talk", 14: "Category", 15: "Category talk",
      100: "Portal", 101: "Portal talk", 108: "Book", 109: "Book talk",
      118: "Draft", 119: "Draft talk"
    };
    return ns[id];
  },
  getNamespaceColor: function(ns) {
    // If the namespace is an int convert it to text
    if (! isNaN(ns)) {
      ns = vte.convertIdToNamespace(ns);
    }

    var un = "#424242";
    var ns_color = {
      "Article": "#CC0000", "Article talk": "#F7B7B7", "User": "#5C8D20", "User talk": "#85ED82",
      "Wikipedia": "#2E97E0", "Wikipedia talk": "#B9E3F9", "File": "#E1711D", "File talk": "#FFC04C",
      "MediaWiki": un, "MediaWiki talk": "#5555FF", "Template": "#55FFFF", "Template talk": "#0000C0",
      "Help": "#008800", "Help talk": "#00C0C0", "Category": "#FFAFAF", "Category talk": "#808080",
      "Portal": "#75A3D1", "Portal talk": "#A679D2", "Book": "#94EF2B", "Book talk": un,
      "Draft": "#99FFFF", "Draft talk": "#99BBFF"
    };
    return ns_color[ns];
  },
  isJson: function(str) {
    try {
      JSON.parse(str);
    } catch (e) {
      return false;
    }
    return true;
  },
  setCookie: function(key, value, options) {
    if (typeof(options) === 'undefined') options = {};
    // Set defaults
    if (! ("expires" in options)) options.expires = 7;
    if (! ("path" in options)) options.path = "/";
    // Then set the cookie
    value = typeof(value) === 'object' ? JSON.stringify(value) : value;
    $.cookie(key, value, options);
  },
  getCookie: function(key) {
    var value = $.cookie(key);
    return vte.isJson(value) ? JSON.parse(value) : value;
  },
  removeCookie: function(key) {
    $.cookie(key, null, { path: '/'});
  },

  setStorage: function(key, value, options) {
    // If the browser doesn't support storage, return null
    if (typeof(Storage) === 'undefined') return null;

    if (typeof(options) === 'undefined') options = {};
    // Set defaults
    if (! ("expires" in options)) options.expires = 7;
    // Convert expires option to seconds from the current time
    options.expires = (new Date().getTime() / 1000) + (options.expires * 60 * 60 * 24);
    // Then set the localStorage, add the options
    var obj = {data: value, options: options};
    localStorage.setItem(key, JSON.stringify(obj));
  },
  getStorage: function(key) {
    // If the browser doesn't support storage, return null
    if (typeof(Storage) === 'undefined') return null;

    var value = localStorage.getItem(key);
    // If the key doesn't exist, return null
    if (value === null) return null;
    value = JSON.parse(value);
    // If the value is expired, return null
    if (value.options.expires < new Date().getTime() / 1000) {
      return null;
    } else {
      return value.data;
    }
  },
  removeStorage: function(key) {
    // If the browser doesn't support storage, return null
    if (typeof(Storage) === 'undefined') return null;
    localStorage.removeItem(key);
  },

};


/**** Styles ****/
var s_wpFont = 'Verdana, "Verdana Ref", Corbel, "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", "DejaVu Sans", "Bitstream Vera Sans", "Liberation Sans", sans-serif';
var s_vteNavLink = {
  "margin": "5px 0px 0px 10px",
  "cursor": "pointer",
  "color": "#0B0B61"
};
var s_vteWindow = {
  "position": "fixed",
  "width": "80%",
  "height": "80%",
  "background-color": "#FFFFFF",
  "top": "50px",
  "left": "10%",
  "box-shadow": "0 1px 6px rgba(0, 0, 0, 0.2)",
  "-moz-box-shadow": "0 1px 6px rgba(0, 0, 0, 0.2)",
  "-webkit-box-shadow": "0 1px 6px rgba(0, 0, 0, 0.2)",
  "z-index": "5"
};
var s_vteSummaryInstructions = {
  "font-family": s_wpFont,
  "font-size": "11px",
  "font-style": "italic",
  "padding": "10px",
  "text-align": "center",
};
var s_vteActiveProject = {
  "font-family": s_wpFont,
  "font-size": "10px",
  "border": "1px solid #000",
  "border-radius": "10px",
  "-moz-border-radius": "10px",
  "width": "30%",
  "float": "left",
  "background-color": "#EEE",
  "margin": "4px 5px",
  "padding": "3px 5px",
  "cursor": "pointer",
};
var s_vteActiveProjectTitle = {
  "font-size": "11px",
  "font-weight": "bold",
  "height": "28px",
};
var s_vteActiveProjectLabel = {
  "padding-left": "20px"
};
var s_vteActiveProjectValue = {
  "font-style": "italic",
  "padding-left": "10px",
  "color": "#424242",
};
var s_vteCloseProject = {
  "font-family": s_wpFont,
  "font-size": "10px",
  "color": "#424242",
  "float": "right",
  "padding": "1px 5px",
  "margin": "-26px 2px 0px 0px",
};
var s_vteSortSummaryDiv = {
  "float": "left",
  "font-family": s_wpFont,
  "font-size": "10px",
  "margin": "4px 5px",
  "padding": "3px 5px",
  "width": "20%",
  "text-align": "center",
  "font-color": "#424242",
};
var s_vteMemberImport = s_vteTaskEdit = s_vteMembersContribution = {
  "font-family": s_wpFont,
  "font-size": "12px",
  "position": "absolute",
  "top": "5%",
  "left": "5%",
  "width": "80%",
  "height": "80%",
  "background-color": "rgba(255,255,255,.98)",
  "padding": "20px",
  "box-shadow": "0 1px 6px rgba(0, 0, 0, 0.2)",
  "-moz-box-shadow": "0 1px 6px rgba(0, 0, 0, 0.2)",
  "-webkit-box-shadow": "0 1px 6px rgba(0, 0, 0, 0.2)",
  "border-radius": "10px",
  "-moz-border-radius": "10px",
  "overflow-y": "auto",
};
var s_vteWindowRightContentTitle = {
  "font-family": s_wpFont,
  "font-size": "13px",
  "padding": "10px 10px 3px 10px",
  "margin": "0px 5px",
  "color": "#0B0B61",
  "float": "right",
  "font-weight": "bold",
  "font-style": "italic",
  "border": "1px solid #5882FA", // was #eee (light gray), now blue
  "border-top-left-radius": "10px",
  "border-top-right-radius": "10px",
  "-moz-border-top-left-radius": "10px",
  "-moz-border-top-right-radius": "10px",
  "cursor": "pointer",
};
var s_vteMembersActionsMessage = {
  "font-family": s_wpFont,
  "position": "absolute",
  "top": "100px",
  "left": "25%",
  "background-color": "rgba(255,255,255,.95)",
  "padding": "20px",
  "box-shadow": "0 1px 6px rgba(0, 0, 0, 0.2)",
  "-moz-box-shadow": "0 1px 6px rgba(0, 0, 0, 0.2)",
  "-webkit-box-shadow": "0 1px 6px rgba(0, 0, 0, 0.2)",
};
var s_vteMembersActionsDiv = {
  "position": "absolute",
  "background-color": "#F5DA81", // yellow-orange
  "padding": "10px",
  "border-radius": "10px",
  "-moz-border-radius": "10px",
};
var s_vteMembersActionsAction = {
  "font-family": s_wpFont,
  "font-size": "10px",
  "color": "#424242",
  "text-align": "left",
  "cursor": "pointer",
  "margin": "3px 0px 3px 5px",
};
var s_vteWindowRightContentTasksAdd = s_vteWindowRightContentMembersAdd = {
  "font-family": s_wpFont,
  "font-size": "12px",
  "padding": "10px 0px 20px 0px",
  "color": "#424242",
};
var s_vteMembersCreate = {
  "font-family": "'Trebuchet MS', Helvetica, sans-serif",
  "font-size": "16px",
  "margin": "15px 0px 15px 15px",
  "color": "#424242",
  "cursor": "pointer",
  "float": "left",
};
var s_vteMembersEmpty = s_vteTasksEmpty = {
  "font-family": s_wpFont,
  "font-size": "12px",
  "padding": "0px 50px",
  "color": "#424242",
};


// Communication view styles
var s_vteCommunicationChatSend = {
  "font-family": s_wpFont,
  "font-size": "10px",
  "padding": "3px",
  "float": "right",
};
var s_vteCommunicationChat = {
  "bottom": "5px",
  "position": "absolute",
};
var s_vteCommunicationChatInput = {
  "width": "-moz-calc(100% - 4px)",    // Firefox
  "width": "-webkit-calc(100% - 4px)", // Webkit
  "width": "-o-calc(100% - 4px)",      // Opera
  "width": "calc(100% - 4px)",         // Standard
  "margin": "0px 0px 2px 0px",
  "font-family": s_wpFont,
  "font-size": "10px",
};
var s_vteCommunicationChatMessages = {
  "list-style-type": "none",
  "margin": "0",
  "padding": "0",
  "overflow-y": "auto",
};
var s_vteCommunicationChatLine = {
  "padding": "0px 1px",
  "list-style": "none",
  "font-family": s_wpFont,
  "font-size": "10px",
};
var s_vteCommunicationChatUser = {
  "color": "#424242",
  "display": "inline",
};
var s_vteCommunicationChatMessage = {
  "display": "inline",
};

// Task view styles
var s_vteTasksCreate = {
  "font-family": "'Trebuchet MS', Helvetica, sans-serif",
  "font-size": "16px",
  "margin": "15px 0px 15px 15px",
  "color": "#424242",
  "cursor": "pointer",
  "float": "left",
};
var s_vteMembersView = s_vteTasksView = {
  "font-family": "'Trebuchet MS', Helvetica, sans-serif",
  "font-size": "13px",
  "color": "#424242",
  "cursor": "pointer",
  "display": "inline",
  "margin": "18px 0px 5px 30px",
  "float": "left",
};
var s_vteDropdownList = {
  "position": "absolute",
  "background-color": "#EEE",
  "font-family": "'Trebuchet MS', Helvetica, sans-serif",
  "min-width": "85px",
  "border-bottom-left-radius": "5px",
  "border-bottom-right-radius": "5px",
  "-moz-border-bottom-left-radius": "5px",
  "-moz-border-bottom-right-radius": "5px",

};
var s_vteDropdownItem = {
  "padding": "2px 4px",
};
var s_vteMembersTable = s_vteTasksTable = {
  "font-family": s_wpFont,
  "font-size": "12px",
  "color": "#424242",
  "border-collapse": "collapse",
  "width": "100%",
};
var s_vteMembersRow = s_vteTasksRow = {
  "border-bottom": "1px solid #EEE",
  "cursor": "pointer",
  "padding": "3px 0px",
};
var s_vteTasksTableTitle = {
  "width": "40%",
};
var s_vteTasksTablePriority = s_vteTasksTableCreated = s_vteTasksTableDue = s_vteTasksTableOwner = {
  "text-align": "center",
  "min-width": "50px",
};
var s_vteTasksTableComments = {
  "text-align": "center",
  "background-color": "#C9C9C9",
};
var s_vteTaskCompleted = {
  "text-decoration": "line-through",
};
// End Task view styles

// Task edit styles
var s_vteTaskMarkComplete = {
  "float": "right",
  "margin": "0px"
};
var s_vteTaskSave = {
  "float": "right",
  "margin": "0px 10px",
};
var s_vteTaskClose = {
  "float": "right",
  "padding": "1px",
  "cursor": "pointer",
};

var s_vteTaskEditLabel = {
  "display": "inline",
  "font-weight": "bold",
};
var s_vteTaskEditInput = {
  "display": "inline",
};
var s_vteTaskEditLeft = {
  "width": "45%",
  "float": "left",
  "margin": "10px 0px 10px 20px",
};
var s_vteTaskEditRight = {
  "width": "45%",
  "float": "right",
  "margin": "10px 0px 10px 0px",
};
var s_vteTaskEditOwners = {
  "color": "#424242",
  "margin-bottom": "20px",
};
var s_vteOwnerRow = s_vteSubtaskRow = {
  "padding": "5px 0px",
};
var s_vteTaskEditOwner = {
  "display": "inline",
};
var s_vteTaskEditRemoveOwner = {
  "display": "inline",
  "padding": "0px 5px",
  "margin": "2px 5px 0px 5px",
};
var s_vteTaskEditAddOwner = {
  "display": "inline",
};
var s_vteTaskEditSubtasks = {
  "color": "#424242",
};
var s_vteTaskEditSubtask = {
  "display": "inline",
};
var s_vteTaskEditAddSubtask = {
  "display": "inline",
};
var s_vteTaskEditSubcomplete = {
  "display": "inline",
};
var s_vteTaskEditGraph = {
  "float": "right",
  "height": "100px",
  "width": "100%",
  "border": "1px solid #000",
  "margin-bottom": "20px",
};
var s_vteTaskEditNotes = {
};
// End Task edit styles

var s_vteMembersName = {
  "display": "inline",
  "font-family": s_wpFont,
  "font-size": "12px",
  "color": "#424242",
  "float": "left",
  "width": "20%",
  "padding-top": "5px",
  "border-bottom": "1px solid #EEE",
};
var s_vteMembersDate = {
  "display": "inline",
  "font-family": s_wpFont,
  "font-size": "12px",
  "color": "#424242",
  "float": "left",
  "width": "20%",
  "padding-top": "5px",
  "border-bottom": "1px solid #EEE",
};
var s_vteMembersComment = {
  "display": "inline",
  "font-family": s_wpFont,
  "font-size": "12px",
  "color": "#424242",
  "float": "left",
  "width": "-moz-calc(60% - 90px)",    // Firefox
  "width": "-webkit-calc(60% - 90px)", // Webkit
  "width": "-o-calc(60% - 90px)",      // Opera
  "width": "calc(60% - 90px)",         // Standard
  "text-align": "center",
  "padding-top": "5px",
  "border-bottom": "1px solid #EEE",
};
var s_vteMemberImportLoading = {
  "font-family": s_wpFont,
  "font-size": "11px",
  "padding": "5px 0px 0px 10px",
  "color": "#424242",
};
var s_vteTasksAction = s_vteMembersAction = {
  "display": "inline",
  "font-family": s_wpFont,
  "font-size": "12px",
  "color": "#424242",
  "float": "left",
  "width": "80px",
  "text-align": "center",
  "cursor": "pointer",
  "padding-top": "5px",
  "border-bottom": "1px solid #EEE",
};
var s_vteTasksComplete = s_vteMembersInactive = {
  "color": "#A4A4A4",
};
var s_vteWindowLeft = {
  "font-family": s_wpFont,
  "font-size": "10px",
  "float": "left",
  "padding": "5px 5px 5px 5px",
  "border-right": "1px solid #EEE",
  // Set width as 20% minus padding, borders, etc
  "width": "-moz-calc(20% - 11px)",    // Firefox
  "width": "-webkit-calc(20% - 11px)", // Webkit
  "width": "-o-calc(20% - 11px)",      // Opera
  "width": "calc(20% - 11px)",         // Standard
  // Same as width, height is 100% minus border, padding, etc
  "height": "-moz-calc(100% - 11px)",   
  "height": "-webkit-calc(100% - 11px)",
  "height": "-o-calc(100% - 11px)",
  "height": "calc(100% - 11px)",
};
var s_vteWindowRight = {
  "float": "right",
  "padding": "5px 5px 5px 5px",
  // Set width as 80% minus padding, borders, etc
  "width": "79%",
  "width": "-moz-calc(80% - 10px)",    // Firefox
  "width": "-webkit-calc(80% - 10px)", // Webkit
  "width": "-o-calc(80% - 10px)",      // Opera
  "width": "calc(80% - 10px)",         // Standard
  // Same as width, height is 100% minus border, padding, etc
  "height": "-moz-calc(100% - 21px)",   
  "height": "-webkit-calc(100% - 21px)",
  "height": "-o-calc(100% - 21px)",
  "height": "calc(100% - 21px)",
};
var s_vteWindowLeftOnline = {
  "float": "left",
  "width": "100%",
  "height": "15%",
  "background-color": "#F9F9F9"
};
var s_vteWindowLeftChat = {
  "float": "left",
  "width": "100%",
  "height": "80%",
  "background-color": "#FFFFFF",
  "font-family": s_wpFont,
  "font-size": "12px",
  "padding": "10px 0px",
};
var s_vteWindowRightTitle = {
  "float": "left",
  "width": "100%",
  "min-height": "10px",
  "background-color": "#F9F9F9",
  "border-bottom": "1px solid #EEE"
};
var s_vteWindowRightTool = {
  "width": "100%",
/*
  "background-color": "#F9F9F9",
  "border-bottom": "1px solid #000"
*/
};
var s_vteWindowLoading = {
  "width": "100%",
  "float": "left",
  "text-align": "center",
  "font-family": s_wpFont,
  "font-size": "10px",
  "color": "#6E6E6E",
  "margin-top": "10%",
};
var s_vteWindowRightNav = {
  "width": "-moz-calc(100% - 20px)",    // Firefox
  "width": "-webkit-calc(100% - 20px)", // Webkit
  "width": "-o-calc(100% - 20px)",      // Opera
  "width": "calc(100% - 20px)",         // Standard
  "border-bottom": "1px solid #EEE",
  "margin": "0px 10px",
};
var s_vteWindowRightContent = {
  "float": "left",
  "width": "100%",
  "height": "88%",
  "background-color": "#FFFFFF",
  "overflow-y": "auto",
};
var s_vteWindowRightContentSummary = {
  "font-family": s_wpFont,
  "font-size": "13px",
};
var s_vteWindowRightContentSummaryGraph = {
  "font-family": s_wpFont,
  "margin": "8px",
  "border": "1px solid #EEEEEE",
  "height": "112px",
};
var s_vteWindowRightContentSummaryPages = {
  "font-family": s_wpFont,
  "margin": "8px",
  "border": "1px solid #EEEEEE",
};
var s_vteWindowRightContentSummaryPage = {
  "height": "50px",
};
var s_vteWindowRightContentSummaryNew = {
  "font-family": s_wpFont,
  "margin": "8px",
  "border": "1px solid #EEEEEE",
};
var s_vteSummaryPageTitle = {
  "font-family": s_wpFont,
  "color": "#0B0080",
  "font-size": "10px",
  "font-style": "italic",
  "border-bottom": "solid 1px #EEE",
  "padding-top": "10px",
  "cursor": "pointer",
};
var s_vteMembersContributionEdits = {
  "font-family": s_wpFont,
  "font-size": "10px",
  "margin": "8px",
  "border": "1px solid #EEEEEE",
  "height": "250px",
};
var s_vteLoadingText = {
  "font-family": s_wpFont,
  "font-size": "10px",
  "margin-top": "20px",
  "color": "#848484",
  "-webkit-animation-name": "glow", 
  "-webkit-animation-duration": "1s", 
  "-webkit-animation-iteration-count": "infinite", 
  "-webkit-animation-direction": "alternate", 
  "-webkit-animation-timing-function": "ease-in-out", 
  "-moz-animation-name": "glow", 
  "-moz-animation-duration": "1s", 
  "-moz-animation-iteration-count": "infinite", 
  "-moz-animation-direction": "alternate", 
  "-moz-animation-timing-function": "ease-in-out", 
  "-o-animation-name": "glow", 
  "-o-animation-duration": "1s", 
  "-o-animation-iteration-count": "infinite", 
  "-o-animation-direction": "alternate", 
  "-o-animation-timing-function": "ease-in-out", 
  "animation-name": "glow", 
  "animation-duration": "1s", 
  "animation-iteration-count": "infinite", 
  "animation-direction": "alternate", 
  "animation-timing-function": "ease-in-out"
};
var s_vteTitle = {
  "font-family": s_wpFont,
  "font-size": "20px",
  "font-weight": "normal",
  "float": "left",
  "padding": "5px 0px 5px 10px"
};
var s_vteTitleActions = {
  "float": "right",
  "padding": "0px 10px 0px 0px",
};
var s_vteTitleAction = {
  "float": "left",
  "font-family": s_wpFont,
  "font-size": "12px",
  "cursor": "pointer",
  "padding": "5px 0px 0px 5px",
};
var s_vteProjectSelectLabel = {
  "font-family": s_wpFont,
  "font-size": "12px",
  "padding": "5px 0px 0px 7px",
};
var s_vteProjectSelectInput = {
  "margin": "5px 0px",
  "height": "1.4em",
  "background-color": "transparent",
  "color": "#000",
  "outline": "none",
  "font-family": s_wpFont,
  "font-size": "12px",
  "padding": "2px 5px",
  "width": "-moz-calc(100% - 12px)",    // Firefox
  "width": "-webkit-calc(100% - 12px)", // Webkit
  "width": "-o-calc(100% - 12px)",      // Opera
  "width": "calc(100% - 12px)",         // Standard
};
var s_vteProjectSelectMulti = {
  "position": "absolute",
  "width": "50%",
  "max-height": "20%",
  "overflow-y": "auto",
  "margin-top": "-4px",
  "padding": "7px 10px",
  "background-color": "#EEE",
  "border-bottom-left-radius": "10px",
  "border-bottom-right-radius": "10px",
  "-moz-border-bottom-left-radius": "10px",
  "-moz-border-bottom-right-radius": "10px",
  "border": "1px solid #000",
  "z-index": "2",
};
var s_vteProjectSelectMultiProj = {
  "font-family": s_wpFont,
  "font-size": "12px",
  "cursor": "pointer",
  "padding-left": "1em",
  "text-indent": "-1em",
  "padding-bottom": "4px",
};



// Only load vte on the Wikipedia namespace (may limit to project pages later)
window.onload = function() {
  //if (mw.config.get('wgNamespaceNumber') === '4') {
    console.log("Loading VTE");
    mw.loader.using( ["mediawiki.api"], function() {
      $(vte.initialize);
      // And create the websocket
      var t = setInterval(function() {
        if (typeof(io) !== 'undefined') {
          clearInterval(t);
          vte_sock = io(data_api, {secure: true});
          // Emit vte initialize
          vte_sock.emit("vte_init", {
            name: mw.config.get("wgUserName"),
            time: new Date(),
            page: mw.config.get("wgTitle"),
            namespace: mw.config.get("wgNamespaceNumber"),
          });
          // And handle updates to who's online
          vte_sock.on("online", function(obj) {
            if ($("#vte-window-left-online-num").length > 0) {
              $("#vte-window-left-online-num").html(Object.keys(obj).length);  
            }
          });
        } 
      }, 100);
    });
  //}
}
//</nowiki>