Jump to content

User:Md gilbert/vte.js

From Wikipedia, the free encyclopedia
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.
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='/media/wikipedia/commons/0/0a/Gnome-stock_person.svg' width='15' height='15' style='padding: 5px;'/>" +
      "  </div>" +
      "  <div id='vte-title-action-settings' title='View VTE settings'>" +
      "    <img src='/media/wikipedia/commons/7/77/Gear_icon.svg' width='25' height='25'/>" +
      "  </div>" +
      "  <div id='vte-title-action-close' title='Close the VTE'>" +
      "    <img src='/media/wikipedia/commons/6/60/Close_icon.svg' width='25' height='25'/>" +
      "  </div>" +
      "</div>"
    );
    // Attributions, via Wikimedia Commons:
    // User: By GNOME icon artists (GNOME download / GNOME FTP) [GPL (http://www.gnu.org/licenses/gpl.html)]
    // Gear: By MGalloway (WMF) (Own work) [CC-BY-SA-3.0 (http://creativecommons.org/licenses/by-sa/3.0)]
    // Close: By MGalloway (WMF) (Own work) [CC-BY-SA-3.0 (http://creativecommons.org/licenses/by-sa/3.0)]

    // Add the title and style the elements
    $("#vte-window-right-title").html($vteTitle);
    $("#vte-title").css(s_vteTitle);
    $("#vte-title-actions").css(s_vteTitleActions);
    $("#vte-title-action-user").css(s_vteTitleAction);
    $("#vte-title-action-settings").css(s_vteTitleAction);
    $("#vte-title-action-close").css(s_vteTitleAction);
    // Add the actions
    $("#vte-title-action-user").on("click", function() {

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

    });
    $("#vte-title-action-close").on("click", function() {
      console.log("closing the vte (will maintain current view).");
      vte.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='/media/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='/media/wikipedia/commons/6/60/Close_icon.svg' width='25' height='25'>" +
      "  </div>" +
      "  <input type='submit' class='vte-task-save' value='Save' />" +
      complete_button +
      "  <div style='margin-top: 10px; clear: both;'>&nbsp;</div>" +
      "  <table style=''>" +
      "    <tr>" +
      "      <td> " +
      "        <div id='vte-task-title-label' class='vte-task-edit-label'>Task Title: </div>" +
      "      </td>" +
      "      <td>" +
      "        <div class='vte-task-edit-input'>" +
      "          <input type='text' id='vte-task-title' value='" + task.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>