<commit_before>// ==UserScript==
// @name        Stack Exchange Pronoun Assistant
// @namespace   https://github.com/Glorfindel83/
// @description Displays users' pronouns (mentioned in their profiles)
// @author      Glorfindel
// @author      ArtOfCode
// @contributor wizzwizz4
// @updateURL   https://raw.githubusercontent.com/Glorfindel83/SE-Userscripts/master/pronoun-assistant/pronoun-assistant.user.js
// @downloadURL https://raw.githubusercontent.com/Glorfindel83/SE-Userscripts/master/pronoun-assistant/pronoun-assistant.user.js
// @supportURL  https://stackapps.com/questions/8440/pronoun-assistant
// @version     2.4
// @match       *://chat.stackexchange.com/rooms/*
// @match       *://chat.stackoverflow.com/rooms/*
// @match       *://chat.meta.stackexchange.com/rooms/*
// @match       *://*.stackexchange.com/questions/*
// @match       *://*.stackoverflow.com/questions/*
// @match       *://*.superuser.com/questions/*
// @match       *://*.serverfault.com/questions/*
// @match       *://*.askubuntu.com/questions/*
// @match       *://*.stackapps.com/questions/*
// @match       *://*.mathoverflow.net/questions/*
// @exclude     *://*.stackexchange.com/questions/ask
// @exclude     *://*.stackoverflow.com/questions/ask
// @exclude     *://*.superuser.com/questions/ask
// @exclude     *://*.serverfault.com/questions/ask
// @exclude     *://*.askubuntu.com/questions/ask
// @exclude     *://*.stackapps.com/questions/ask
// @exclude     *://*.mathoverflow.net/questions/ask
// @grant       GM_addStyle
// @require     https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
// @require     https://gist.github.com/raw/2625891/waitForKeyElements.js
// @require     https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js
// ==/UserScript==

/* global $, waitForKeyElements */

GM_addStyle(`
.tiny-signature {
  display: inline-flex;
  flex-direction: row-reverse;
  align-items: center;
  width: 100%;
}
.username {
  height: unset !important;
}
.pronouns, .pronouns a {
  color: #777;
}
.pronouns {
  padding-left: 5px;
}
.pronouns a:hover {
  text-decoration: underline;
}
`)

// List of pronouns to look out for
let allPronouns = [
  "him", "his",
  "she", "her?", // that covers 'he' as well
  "they", "them", "their",
  "ze", "hir", "zir",
  "xey", "xem", "xyr"
].join("|");
let pronounListRegex = new RegExp('\\W*((' + allPronouns + ')(\\s*/\\s*(' + allPronouns + '))+)\\W*', 'i');
let myPronounIsRegex = /(https?:\/\/)?(my\.)?pronoun\.is\/([\w/]+)/i;
let explicitPronounsRegex = /pronouns:\s*([^.\n)<]*)(\.|\n|\)|<|$)/im;

// Keys:   user IDs
// Values: either a list of DOM elements (specifically, the anchors to chat profiles)
//         or a string with pronouns.
var users = {};
// Keys:   user IDs (Q&A only)
// Values: the users' 'about me' values.
var profiles = {};

// If we're on a Q&A site, also cache all changes to the `users` object to save on API calls
if (location.hostname.indexOf("chat") === -1) {
  const localStorageData = localStorage.stackPronounAssistant_users;
  const cached = localStorageData ? JSON.parse(localStorage.stackPronounAssistant_users) : {};
  const userData = {};
  Object.keys(cached).forEach(k => {userData[parseInt(k, 10)] = cached[k]});
  users = new Proxy(userData, {
    get: (obj, prop) => obj[prop],
    set: (obj, prop, value) => {
      obj[prop] = value;
      localStorage.stackPronounAssistant_users = JSON.stringify(userData);
    }
  });
}

// Adds pronoun information to a user's 'signature' in chat.
function showPronounsForChat($element, pronouns) {
  if (pronouns == "") {
    return;
  }

  addPronounsToChatSignatures($element, pronouns);

  // After clicking the signature (to show the chat profile popup), *sometimes*
  // (the exact conditions are unclear - it happens in The Bridge but not in the Teachers' Lounge)
  // the signature gets rerendered. In that case, we need to readd the pronouns.
  $element.on("DOMSubtreeModified", function() {
    if ($(this).find(".pronouns").length == 0) {
      $(this).off("DOMSubtreeModified");
      addPronounsToChatSignatures($(this), pronouns);
    }
  });
}

function addPronounsToChatSignatures($element, pronouns) {
  // The element might contain both a tiny and a full signature
  $element.find("div.username").each(function (index, usernameElement) {
    usernameElement.innerHTML = '' + usernameElement.innerHTML + '
'
      + '' + pronouns + '';
  });
}

// Determines pronoun information and adds it to a user card (under a post) or author information (after a comment)
function decorate($element) {
  const link = $element.attr("href");
  const userId = parseInt(link.split("/users/")[1]);
  if (!users[userId]) {
    // No pronouns calculated yet, we need to calculate and store them.
    users[userId] = getPronouns(profiles[userId], true);
    showPronouns($element, users[userId]);
  } else {
    // We already have the pronouns, we can just use them.
    showPronouns($element, users[userId]);
  }
};

// Adds pronoun information to a user card or author information
function showPronouns($element, pronouns) {
  if (pronouns == "" || $element.siblings(".pronouns").length != 0) {
    return;
  }

  // Make sure the pronouns don't end up between the username and the diamond
  if ($element.next("span.mod-flair").length != 0) {
    $element = $element.next("span.mod-flair");
  }
  $element.after($('' + pronouns + ''));
}

// Check text (obtained from the user's 'about me' in their chat profile or Q&A profile) for pronoun indicators
function getPronouns(aboutMe, allowPronounIslandLinks) {
  // Link to Pronoun Island, e.g.
  // http://my.pronoun.is/she
  var match = myPronounIsRegex.exec(aboutMe);
  if (match != null) {
    return allowPronounIslandLinks ? '' + match[0] + ''
      : match[3];
  }

  // Explicit pronouns specification, e.g.
  // Pronouns: he/him.
  // The end is indicated by a dot, a newline, or simply the end of the text.
  match = explicitPronounsRegex.exec(aboutMe);
  if (match != null) {
    return match[1];
  }

  // Check for pronouns (see above for the list) joined by a forward slash, e.g.
  // she/her
  // (he/him/his)
  // they / them
  match = pronounListRegex.exec(aboutMe);
  if (match != null) {
    return match[1];
  }

  // No pronouns found
  return "";
}

// Chat signatures
waitForKeyElements("a.signature", function(jNode) {
  let userID = jNode.attr("href").split("/users/")[1];
  if (!users[userID]) {
    users[userID] = [];
    users[userID].push(jNode);
    // Read chat profile
    $.get("https://" + location.host + "/users/thumbs/" + userID + "?showUsage=true", function(data) {
      let pronouns = data.user_message == null ? "" : getPronouns(data.user_message, false);
      users[userID].forEach(function (element) {
        showPronounsForChat(element, pronouns);
      });
      users[userID] = pronouns;
    });
  } else if (typeof users[userID] == 'string') {
    // We already have the pronouns, we can just use them.
    showPronounsForChat(jNode, users[userID]);
  } else {
    // User appearing multiple times, their profile is already being fetched.
    users[userID].push(jNode);
  }
});

// Selector for Q&A sites
const selector = "div.user-details > a, a.comment-user";

// Q&A site user cards & comment usernames
(async () => {
  const sleep = async (ms) => {
    return new Promise(resolve => {
      setTimeout(resolve, ms);
    });
  };

  const userIds = [];
  const $userElements = $(selector);

  // Grab all the user IDs out of a page first. We'll go back over them later to add pronouns in.
  $userElements.each(function() {
    const link = $(this).attr("href");
    if (!link.startsWith("/users/")) {
      // not a user card, but a community wiki post
      return;
    }
    const userId = parseInt(link.split("/users/")[1]);
    if (userIds.indexOf(userId) === -1) {
      userIds.push(userId);
    }
  });

  // Split the list into 100-item pages and grab profiles for each page.
  // This works because splice modifies the userIds array in-place, removing elements from the front and
  // returning them into the `page` variable. When we've used them all, the array will be empty.
  while (userIds.length > 0) {
    const page = userIds.splice(0, 100);

    const resp = await fetch("https://api.stackexchange.com/2.2/users/" + page.join(';') + "?site=" + location.hostname
                             + "&key=L8n*CvRX3ZsedRQicxnjIA((&filter=!23IboywNfWUzv_nydJbn*&pagesize=100");
    const data = await resp.json();
    if (typeof data.items !== 'undefined') {
      data.items.forEach(i => {
        profiles[i.user_id] = i.about_me;
      });
    }

    if (data.backoff) {
      // Respect backoffs, not just pronouns.
      await sleep(data.backoff * 1000);
    }
  }

  $userElements.each(function() { decorate($(this)); });

  // Make sure new answers / comments receive the same treatment
  waitForKeyElements(selector, function(jNode) {
    decorate($(jNode));
  });
})();
<commit_msg>Add cache expiry<commit_after>// ==UserScript==
// @name        Stack Exchange Pronoun Assistant
// @namespace   https://github.com/Glorfindel83/
// @description Displays users' pronouns (mentioned in their profiles)
// @author      Glorfindel
// @author      ArtOfCode
// @contributor wizzwizz4
// @updateURL   https://raw.githubusercontent.com/Glorfindel83/SE-Userscripts/master/pronoun-assistant/pronoun-assistant.user.js
// @downloadURL https://raw.githubusercontent.com/Glorfindel83/SE-Userscripts/master/pronoun-assistant/pronoun-assistant.user.js
// @supportURL  https://stackapps.com/questions/8440/pronoun-assistant
// @version     2.4
// @match       *://chat.stackexchange.com/rooms/*
// @match       *://chat.stackoverflow.com/rooms/*
// @match       *://chat.meta.stackexchange.com/rooms/*
// @match       *://*.stackexchange.com/questions/*
// @match       *://*.stackoverflow.com/questions/*
// @match       *://*.superuser.com/questions/*
// @match       *://*.serverfault.com/questions/*
// @match       *://*.askubuntu.com/questions/*
// @match       *://*.stackapps.com/questions/*
// @match       *://*.mathoverflow.net/questions/*
// @exclude     *://*.stackexchange.com/questions/ask
// @exclude     *://*.stackoverflow.com/questions/ask
// @exclude     *://*.superuser.com/questions/ask
// @exclude     *://*.serverfault.com/questions/ask
// @exclude     *://*.askubuntu.com/questions/ask
// @exclude     *://*.stackapps.com/questions/ask
// @exclude     *://*.mathoverflow.net/questions/ask
// @grant       GM_addStyle
// @require     https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
// @require     https://gist.github.com/raw/2625891/waitForKeyElements.js
// @require     https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js
// ==/UserScript==

/* global $, waitForKeyElements */

GM_addStyle(`
.tiny-signature {
  display: inline-flex;
  flex-direction: row-reverse;
  align-items: center;
  width: 100%;
}
.username {
  height: unset !important;
}
.pronouns, .pronouns a {
  color: #777;
}
.pronouns {
  padding-left: 5px;
}
.pronouns a:hover {
  text-decoration: underline;
}
`)

// List of pronouns to look out for
let allPronouns = [
  "him", "his",
  "she", "her?", // that covers 'he' as well
  "they", "them", "their",
  "ze", "hir", "zir",
  "xey", "xem", "xyr"
].join("|");
let pronounListRegex = new RegExp('\\W*((' + allPronouns + ')(\\s*/\\s*(' + allPronouns + '))+)\\W*', 'i');
let myPronounIsRegex = /(https?:\/\/)?(my\.)?pronoun\.is\/([\w/]+)/i;
let explicitPronounsRegex = /pronouns:\s*([^.\n)<]*)(\.|\n|\)|<|$)/im;

// Keys:   user IDs
// Values: either a list of DOM elements (specifically, the anchors to chat profiles)
//         or a string with pronouns.
var users = {};
// Keys:   user IDs (Q&A only)
// Values: the users' 'about me' values.
var profiles = {};

// If we're on a Q&A site, also cache all changes to the `users` object to save on API calls
if (location.hostname.indexOf("chat") === -1) {
  const localStorageData = localStorage.stackPronounAssistant_users;
  let cached = localStorageData ? JSON.parse(localStorage.stackPronounAssistant_users) : {};

  if (Object.keys(cached).length > 0 && typeof cached[Object.keys(cached)[0]] === "string") {
    // v2.4 and before had no cache expiry, users[userId] was a string.
    // Currently, users[userId] is an array containing a string and an expiry.
    // If we have cached data but it's in <= 2.4 format, delete it - we'll regenerate it instead.
    delete localStorage.stackPronounAssistant_users;
    cached = {};
  }

  const userData = {};
  Object.keys(cached).forEach(k => {userData[parseInt(k, 10)] = cached[k]});
  users = new Proxy(userData, {
    get: (obj, prop) => {
      const data = obj[prop];
      if (!data) {
        return null;
      }
      else {
        const [pronouns, expiry] = data;
        if (expiry < Date.now()) {
          return null;
        }
        else {
          return pronouns;
        }
      }
    },
    set: (obj, prop, value) => {
      obj[prop] = [value, Date.now() + 86400 * 1000]; // 24h expiry
      localStorage.stackPronounAssistant_users = JSON.stringify(userData);
    }
  });
}

// Adds pronoun information to a user's 'signature' in chat.
function showPronounsForChat($element, pronouns) {
  if (pronouns == "") {
    return;
  }

  addPronounsToChatSignatures($element, pronouns);

  // After clicking the signature (to show the chat profile popup), *sometimes*
  // (the exact conditions are unclear - it happens in The Bridge but not in the Teachers' Lounge)
  // the signature gets rerendered. In that case, we need to readd the pronouns.
  $element.on("DOMSubtreeModified", function() {
    if ($(this).find(".pronouns").length == 0) {
      $(this).off("DOMSubtreeModified");
      addPronounsToChatSignatures($(this), pronouns);
    }
  });
}

function addPronounsToChatSignatures($element, pronouns) {
  // The element might contain both a tiny and a full signature
  $element.find("div.username").each(function (index, usernameElement) {
    usernameElement.innerHTML = '' + usernameElement.innerHTML + '
'
      + '' + pronouns + '';
  });
}

// Determines pronoun information and adds it to a user card (under a post) or author information (after a comment)
function decorate($element) {
  const link = $element.attr("href");
  const userId = parseInt(link.split("/users/")[1]);
  if (!users[userId]) {
    // No pronouns calculated yet, we need to calculate and store them.
    users[userId] = getPronouns(profiles[userId], true);
    showPronouns($element, users[userId]);
  } else {
    // We already have the pronouns, we can just use them.
    showPronouns($element, users[userId]);
  }
};

// Adds pronoun information to a user card or author information
function showPronouns($element, pronouns) {
  if (pronouns == "" || $element.siblings(".pronouns").length != 0) {
    return;
  }

  // Make sure the pronouns don't end up between the username and the diamond
  if ($element.next("span.mod-flair").length != 0) {
    $element = $element.next("span.mod-flair");
  }
  $element.after($('' + pronouns + ''));
}

// Check text (obtained from the user's 'about me' in their chat profile or Q&A profile) for pronoun indicators
function getPronouns(aboutMe, allowPronounIslandLinks) {
  // Link to Pronoun Island, e.g.
  // http://my.pronoun.is/she
  var match = myPronounIsRegex.exec(aboutMe);
  if (match != null) {
    return allowPronounIslandLinks ? '' + match[0] + ''
      : match[3];
  }

  // Explicit pronouns specification, e.g.
  // Pronouns: he/him.
  // The end is indicated by a dot, a newline, or simply the end of the text.
  match = explicitPronounsRegex.exec(aboutMe);
  if (match != null) {
    return match[1];
  }

  // Check for pronouns (see above for the list) joined by a forward slash, e.g.
  // she/her
  // (he/him/his)
  // they / them
  match = pronounListRegex.exec(aboutMe);
  if (match != null) {
    return match[1];
  }

  // No pronouns found
  return "";
}

// Chat signatures
waitForKeyElements("a.signature", function(jNode) {
  let userID = jNode.attr("href").split("/users/")[1];
  if (!users[userID]) {
    users[userID] = [];
    users[userID].push(jNode);
    // Read chat profile
    $.get("https://" + location.host + "/users/thumbs/" + userID + "?showUsage=true", function(data) {
      let pronouns = data.user_message == null ? "" : getPronouns(data.user_message, false);
      users[userID].forEach(function (element) {
        showPronounsForChat(element, pronouns);
      });
      users[userID] = pronouns;
    });
  } else if (typeof users[userID] == 'string') {
    // We already have the pronouns, we can just use them.
    showPronounsForChat(jNode, users[userID]);
  } else {
    // User appearing multiple times, their profile is already being fetched.
    users[userID].push(jNode);
  }
});

// Selector for Q&A sites
const selector = "div.user-details > a, a.comment-user";

// Q&A site user cards & comment usernames
(async () => {
  const sleep = async (ms) => {
    return new Promise(resolve => {
      setTimeout(resolve, ms);
    });
  };

  const userIds = [];
  const $userElements = $(selector);

  // Grab all the user IDs out of a page first. We'll go back over them later to add pronouns in.
  $userElements.each(function() {
    const link = $(this).attr("href");
    if (!link.startsWith("/users/")) {
      // not a user card, but a community wiki post
      return;
    }
    const userId = parseInt(link.split("/users/")[1]);
    if (userIds.indexOf(userId) === -1) {
      userIds.push(userId);
    }
  });

  // Split the list into 100-item pages and grab profiles for each page.
  // This works because splice modifies the userIds array in-place, removing elements from the front and
  // returning them into the `page` variable. When we've used them all, the array will be empty.
  while (userIds.length > 0) {
    const page = userIds.splice(0, 100);

    const resp = await fetch("https://api.stackexchange.com/2.2/users/" + page.join(';') + "?site=" + location.hostname
                             + "&key=L8n*CvRX3ZsedRQicxnjIA((&filter=!23IboywNfWUzv_nydJbn*&pagesize=100");
    const data = await resp.json();
    if (typeof data.items !== 'undefined') {
      data.items.forEach(i => {
        profiles[i.user_id] = i.about_me;
      });
    }

    if (data.backoff) {
      // Respect backoffs, not just pronouns.
      await sleep(data.backoff * 1000);
    }
  }

  $userElements.each(function() { decorate($(this)); });

  // Make sure new answers / comments receive the same treatment
  waitForKeyElements(selector, function(jNode) {
    decorate($(jNode));
  });
})();
<|endoftext|>


<commit_before>// ==UserScript==
// @name        Stack Exchange Pronoun Assistant
// @namespace   https://github.com/Glorfindel83/
// @description Displays users' pronouns (mentioned in their profiles) in chat
// @author      Glorfindel
// @contributor ArtOfCode
// @contributor wizzwizz4
// @updateURL   https://raw.githubusercontent.com/Glorfindel83/SE-Userscripts/master/pronoun-assistant/pronoun-assistant.user.js
// @downloadURL https://raw.githubusercontent.com/Glorfindel83/SE-Userscripts/master/pronoun-assistant/pronoun-assistant.user.js
// @supportURL  https://stackapps.com/questions/8440/pronoun-assistant
// @version     2.1
// @match       *://chat.stackexchange.com/rooms/*
// @match       *://chat.stackoverflow.com/rooms/*
// @match       *://chat.meta.stackexchange.com/rooms/*
// @match       *://*.stackexchange.com/questions/*
// @match       *://*.stackoverflow.com/questions/*
// @match       *://*.superuser.com/questions/*
// @match       *://*.serverfault.com/questions/*
// @match       *://*.askubuntu.com/questions/*
// @match       *://*.stackapps.com/questions/*
// @match       *://*.mathoverflow.net/questions/*
// @exclude     *://*.stackexchange.com/questions/ask
// @exclude     *://*.stackoverflow.com/questions/ask
// @exclude     *://*.superuser.com/questions/ask
// @exclude     *://*.serverfault.com/questions/ask
// @exclude     *://*.askubuntu.com/questions/ask
// @exclude     *://*.stackapps.com/questions/ask
// @exclude     *://*.mathoverflow.net/questions/ask
// @grant       GM_addStyle
// @require     https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
// @require     https://gist.github.com/raw/2625891/waitForKeyElements.js
// @require     https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js
// ==/UserScript==

/* global $, waitForKeyElements */

GM_addStyle(`
.tiny-signature {
  display: inline-flex;
  flex-direction: row-reverse;
  align-items: center;
  width: 100%;
}

.username {
  height: unset !important;
}

.pronouns {
  color: #777;
  padding-left: 5px;
}
`)

// List of pronouns to look out for
let allPronouns = [
  "him", "his",
  "she", "her?", // that covers 'he' as well
  "they", "them", "their",
  "ze", "hir", "zir",
  "xey", "xem", "xyr"
].join("|");
let pronounListRegex = new RegExp('\\W*((' + allPronouns + ')(\\s*/\\s*(' + allPronouns + '))+)\\W*', 'i');
let myPronounIsRegex = /(https?:\/\/)?(my\.)?pronoun\.is\/[\w/]+/i;
let explicitPronounsRegex = /pronouns:\s*([^.\n)]*)(\.|\n|\)|$)/im;

// Keys:   user IDs
// Values: either a list of DOM elements (specifically, the anchors to chat profiles)
//         or a string with pronouns.
var users = {};

// If we're on a Q&A site, also cache all changes to the `users` object to save on API calls
if (location.hostname.indexOf("chat") === -1) {
  const localStorageData = localStorage.stackPronounAssistant_users;
  const cached = localStorageData ? JSON.parse(localStorage.stackPronounAssistant_users) : {};
  const userData = Object.keys(cached).forEach(k => userData[parseInt(k, 10)] = cached[k]);
  users = new Proxy(userData, {
    get: (obj, prop) => userData,
    set: (obj, prop, value) => {
      obj[prop] = value;
      localStorage.stackPronounAssistant_users = JSON.stringify(userData);
  });
}

// Adds pronoun information to a user's 'signature' in chat.
function showPronounsForChat(element, pronouns) {
  if (pronouns == "") {
    return;
  }
  // the element might contain both a tiny and a full signature
  element.find("div.username").each(function (index, usernameElement) {
    usernameElement.innerHTML = '' + usernameElement.innerHTML + '
'
      + '' + pronouns.replace(/(<([^>]+)>)/ig, "") + '';
  });
}

function showPronouns(element, pronouns) {
  if (pronouns == "") {
    return;
  }
  element.after($('' + pronouns.replace(/(<([^>]+)>)/ig, "") + ''));
}

// Check the user's 'about' in their chat profile for pronoun indicators
function getPronouns(aboutMe) {
  // Link to Pronoun Island, e.g.
  // http://my.pronoun.is/she
  var match = myPronounIsRegex.exec(aboutMe);
  if (match != null) {
    return match[0];
  }

  // Explicit pronouns specification, e.g.
  // Pronouns: he/him.
  // The end is indicated by a dot, a newline, or simply the end of the text.
  match = explicitPronounsRegex.exec(aboutMe);
  if (match != null) {
    return match[1];
  }

  // Check for pronouns (see above for the list) joined by a forward slash, e.g.
  // she/her
  // (he/him/his)
  // they / them
  match = pronounListRegex.exec(aboutMe);
  if (match != null) {
    return match[1];
  }

  // No pronouns found
  return "";
}

// Chat signatures
waitForKeyElements("a.signature", function(jNode) {
  let userID = jNode.attr("href").split("/users/")[1];
  if (!users.hasOwnProperty(userID)) {
    users[userID] = [];
    users[userID].push(jNode);
    // Read chat profile
    $.get("https://" + location.host + "/users/thumbs/" + userID + "?showUsage=true", function(data) {
      let pronouns = data.user_message == null ? "" : getPronouns(data.user_message);
      users[userID].forEach(function (element) {
        showPronounsForChat(element, pronouns);
      });
      users[userID] = pronouns;
    });
  } else if (typeof users[userID] == 'string') {
    // We already have the pronouns, we can just use them.
    showPronounsForChat(jNode, users[userID]);
  } else {
    // User appearing multiple times, their profile is already being fetched.
    users[userID].push(jNode);
  }
});

// Q&A site user cards & comment usernames
(async () => {
  const sleep = async (ms) => {
    return new Promise(resolve => {
      setTimeout(resolve, ms);
    });
  };

  const userIds = [];
  const profiles = {};

  const $userElements = $("div.user-details > a, a.comment-user");

  // Grab all the user IDs out of a page first. We'll go back over them later to add pronouns in.
  $userElements.each(function() {
    const link = $(this).attr("href");
    if (!link.startsWith("/users/")) {
      // not a user card, but a community wiki post
      return;
    }
    const userId = parseInt(link.split("/users/")[1]);
    if (userIds.indexOf(userId) === -1) {
      userIds.push(userId);
    }
  });

  // Split the list into 100-item pages and grab profiles for each page.
  // This works because splice modifies the userIds array in-place, removing elements from the front and
  // returning them into the `page` variable. When we've used them all, the array will be empty.
  while (userIds.length > 0) {
    const page = userIds.splice(0, 100);

    const resp = await fetch("https://api.stackexchange.com/2.2/users/" + page.join(';') + "?site=" + location.hostname
                             + "&key=L8n*CvRX3ZsedRQicxnjIA((&filter=!23IboywNfWUzv_nydJbn*&pagesize=100");
    const data = await resp.json();
    if (typeof data.items !== 'undefined') {
      data.items.forEach(i => {
        profiles[i.user_id] = i.about_me;
      });
    }

    if (data.backoff) {
      // Respect backoffs, not just pronouns.
      await sleep(data.backoff * 1000);
    }
  }

  $userElements.each(function() {
    const link = $(this).attr("href");
    const userId = parseInt(link.split("/users/")[1]);
    if (!users.hasOwnProperty(userId)) {
      // No pronouns calculated yet, we need to calculate and store them.
      users[userId] = getPronouns(profiles[userId]);
      showPronouns($(this), users[userId]);
    }
    else {
      // We already have the pronouns, we can just use them.
      showPronouns($(this), users[userId]);
    }
  });
})();
<commit_msg>So many tiny bugs<commit_after>// ==UserScript==
// @name        Stack Exchange Pronoun Assistant
// @namespace   https://github.com/Glorfindel83/
// @description Displays users' pronouns (mentioned in their profiles) in chat
// @author      Glorfindel
// @contributor ArtOfCode
// @contributor wizzwizz4
// @updateURL   https://raw.githubusercontent.com/Glorfindel83/SE-Userscripts/master/pronoun-assistant/pronoun-assistant.user.js
// @downloadURL https://raw.githubusercontent.com/Glorfindel83/SE-Userscripts/master/pronoun-assistant/pronoun-assistant.user.js
// @supportURL  https://stackapps.com/questions/8440/pronoun-assistant
// @version     2.1
// @match       *://chat.stackexchange.com/rooms/*
// @match       *://chat.stackoverflow.com/rooms/*
// @match       *://chat.meta.stackexchange.com/rooms/*
// @match       *://*.stackexchange.com/questions/*
// @match       *://*.stackoverflow.com/questions/*
// @match       *://*.superuser.com/questions/*
// @match       *://*.serverfault.com/questions/*
// @match       *://*.askubuntu.com/questions/*
// @match       *://*.stackapps.com/questions/*
// @match       *://*.mathoverflow.net/questions/*
// @exclude     *://*.stackexchange.com/questions/ask
// @exclude     *://*.stackoverflow.com/questions/ask
// @exclude     *://*.superuser.com/questions/ask
// @exclude     *://*.serverfault.com/questions/ask
// @exclude     *://*.askubuntu.com/questions/ask
// @exclude     *://*.stackapps.com/questions/ask
// @exclude     *://*.mathoverflow.net/questions/ask
// @grant       GM_addStyle
// @require     https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
// @require     https://gist.github.com/raw/2625891/waitForKeyElements.js
// @require     https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js
// ==/UserScript==

/* global $, waitForKeyElements */

GM_addStyle(`
.tiny-signature {
  display: inline-flex;
  flex-direction: row-reverse;
  align-items: center;
  width: 100%;
}

.username {
  height: unset !important;
}

.pronouns {
  color: #777;
  padding-left: 5px;
}
`)

// List of pronouns to look out for
let allPronouns = [
  "him", "his",
  "she", "her?", // that covers 'he' as well
  "they", "them", "their",
  "ze", "hir", "zir",
  "xey", "xem", "xyr"
].join("|");
let pronounListRegex = new RegExp('\\W*((' + allPronouns + ')(\\s*/\\s*(' + allPronouns + '))+)\\W*', 'i');
let myPronounIsRegex = /(https?:\/\/)?(my\.)?pronoun\.is\/[\w/]+/i;
let explicitPronounsRegex = /pronouns:\s*([^.\n)]*)(\.|\n|\)|$)/im;

// Keys:   user IDs
// Values: either a list of DOM elements (specifically, the anchors to chat profiles)
//         or a string with pronouns.
var users = {};

// If we're on a Q&A site, also cache all changes to the `users` object to save on API calls
if (location.hostname.indexOf("chat") === -1) {
  const localStorageData = localStorage.stackPronounAssistant_users;
  const cached = localStorageData ? JSON.parse(localStorage.stackPronounAssistant_users) : {};
  const userData = {};
  Object.keys(cached).forEach(k => {userData[parseInt(k, 10)] = cached[k]});
  users = new Proxy(userData, {
    get: (obj, prop) => obj[prop],
    set: (obj, prop, value) => {
      obj[prop] = value;
      localStorage.stackPronounAssistant_users = JSON.stringify(userData);
    }
  });
}

// Adds pronoun information to a user's 'signature' in chat.
function showPronounsForChat(element, pronouns) {
  if (pronouns == "") {
    return;
  }
  // the element might contain both a tiny and a full signature
  element.find("div.username").each(function (index, usernameElement) {
    usernameElement.innerHTML = '' + usernameElement.innerHTML + '
'
      + '' + pronouns.replace(/(<([^>]+)>)/ig, "") + '';
  });
}

function showPronouns(element, pronouns) {
  if (pronouns == "") {
    return;
  }
  element.after($('' + pronouns.replace(/(<([^>]+)>)/ig, "") + ''));
}

// Check the user's 'about' in their chat profile for pronoun indicators
function getPronouns(aboutMe) {
  // Link to Pronoun Island, e.g.
  // http://my.pronoun.is/she
  var match = myPronounIsRegex.exec(aboutMe);
  if (match != null) {
    return match[0];
  }

  // Explicit pronouns specification, e.g.
  // Pronouns: he/him.
  // The end is indicated by a dot, a newline, or simply the end of the text.
  match = explicitPronounsRegex.exec(aboutMe);
  if (match != null) {
    return match[1];
  }

  // Check for pronouns (see above for the list) joined by a forward slash, e.g.
  // she/her
  // (he/him/his)
  // they / them
  match = pronounListRegex.exec(aboutMe);
  if (match != null) {
    return match[1];
  }

  // No pronouns found
  return "";
}

// Chat signatures
waitForKeyElements("a.signature", function(jNode) {
  let userID = jNode.attr("href").split("/users/")[1];
  if (!users[userID]) {
    users[userID] = [];
    users[userID].push(jNode);
    // Read chat profile
    $.get("https://" + location.host + "/users/thumbs/" + userID + "?showUsage=true", function(data) {
      let pronouns = data.user_message == null ? "" : getPronouns(data.user_message);
      users[userID].forEach(function (element) {
        showPronounsForChat(element, pronouns);
      });
      users[userID] = pronouns;
    });
  } else if (typeof users[userID] == 'string') {
    // We already have the pronouns, we can just use them.
    showPronounsForChat(jNode, users[userID]);
  } else {
    // User appearing multiple times, their profile is already being fetched.
    users[userID].push(jNode);
  }
});

// Q&A site user cards & comment usernames
(async () => {
  const sleep = async (ms) => {
    return new Promise(resolve => {
      setTimeout(resolve, ms);
    });
  };

  const userIds = [];
  const profiles = {};

  const $userElements = $("div.user-details > a, a.comment-user");

  // Grab all the user IDs out of a page first. We'll go back over them later to add pronouns in.
  $userElements.each(function() {
    const link = $(this).attr("href");
    if (!link.startsWith("/users/")) {
      // not a user card, but a community wiki post
      return;
    }
    const userId = parseInt(link.split("/users/")[1]);
    if (userIds.indexOf(userId) === -1) {
      userIds.push(userId);
    }
  });

  // Split the list into 100-item pages and grab profiles for each page.
  // This works because splice modifies the userIds array in-place, removing elements from the front and
  // returning them into the `page` variable. When we've used them all, the array will be empty.
  while (userIds.length > 0) {
    const page = userIds.splice(0, 100);

    const resp = await fetch("https://api.stackexchange.com/2.2/users/" + page.join(';') + "?site=" + location.hostname
                             + "&key=L8n*CvRX3ZsedRQicxnjIA((&filter=!23IboywNfWUzv_nydJbn*&pagesize=100");
    const data = await resp.json();
    if (typeof data.items !== 'undefined') {
      data.items.forEach(i => {
        profiles[i.user_id] = i.about_me;
      });
    }

    if (data.backoff) {
      // Respect backoffs, not just pronouns.
      await sleep(data.backoff * 1000);
    }
  }

  $userElements.each(function() {
    const link = $(this).attr("href");
    const userId = parseInt(link.split("/users/")[1]);
    if (!users[userId]) {
      // No pronouns calculated yet, we need to calculate and store them.
      users[userId] = getPronouns(profiles[userId]);
      showPronouns($(this), users[userId]);
    }
    else {
      // We already have the pronouns, we can just use them.
      showPronouns($(this), users[userId]);
    }
  });
})();
<|endoftext|>


<commit_before>// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0

// default imports
const AWSXRay = require('aws-xray-sdk-core')
const AWS = AWSXRay.captureAWS(require('aws-sdk'))
const { metricScope, Unit } = require("aws-embedded-metrics")
const DDB = new AWS.DynamoDB({ apiVersion: "2012-10-08" })

// environment variables
const { TABLE_NAME, ENDPOINT_OVERRIDE, REGION } = process.env
const options = { region: REGION }
AWS.config.update({ region: REGION })

if (ENDPOINT_OVERRIDE !== "") {
    options.endpoint = ENDPOINT_OVERRIDE
}

const docClient = new AWS.DynamoDB.DocumentClient(options)
// response helper
const response = (statusCode, body, additionalHeaders) => ({
    statusCode,
    body: JSON.stringify(body),
    headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', ...additionalHeaders },
})

function isValidRequest(context, event) {
    return (event !== null) &&
        (event.pathParameters !== null) &&
        (event.pathParameters.id !== null) &&
        (/^[\w-]+$/.test(event.pathParameters.id))
}

function getCognitoUsername(event){
    let authHeader = event.requestContext.authorizer;
    if (authHeader !== null)
    {
        return authHeader.claims["cognito:username"];
    }
    return null;

}

function deleteRecordById(username, recordId) {
    let params = {
        TableName: TABLE_NAME,
        KeyConditionExpression: "#username = :username",
        ExpressionAttributeNames:{
            "#username": "cognito-username"
        },
        ExpressionAttributeValues: {
            ":username": username
        }
    }

    return docClient.delete(params)
}

// Lambda Handler
exports.deleteToDoItem =
    metricScope(metrics =>
        async (event, context, callback) => {
            metrics.setNamespace('TodoApp')
            metrics.putDimensions({ Service: "deleteTodo" })
            metrics.setProperty("RequestId", context.requestId)

            if (!isValidRequest(context, event)) {
                metrics.putMetric("Error", 1, Unit.Count)
                return response(400, { message: "Error: Invalid request" })
            }

            try {
                let username = getCognitoUsername(event);
                let data = await deleteRecordById(username, event.pathParameters.id).promise()
                metrics.putMetric("Success", 1, Unit.Count)
                return response(200, data)
            } catch (err) {
                metrics.putMetric("Error", 1, Unit.Count)
                return response(400, { message: err.message })
            }
        })
<commit_msg>Fix a bug when deleting a TODO item.<commit_after>// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0

// default imports
const AWSXRay = require('aws-xray-sdk-core')
const AWS = AWSXRay.captureAWS(require('aws-sdk'))
const { metricScope, Unit } = require("aws-embedded-metrics")
const DDB = new AWS.DynamoDB({ apiVersion: "2012-10-08" })

// environment variables
const { TABLE_NAME, ENDPOINT_OVERRIDE, REGION } = process.env
const options = { region: REGION }
AWS.config.update({ region: REGION })

if (ENDPOINT_OVERRIDE !== "") {
    options.endpoint = ENDPOINT_OVERRIDE
}

const docClient = new AWS.DynamoDB.DocumentClient(options)
// response helper
const response = (statusCode, body, additionalHeaders) => ({
    statusCode,
    body: JSON.stringify(body),
    headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', ...additionalHeaders },
})

function isValidRequest(context, event) {
    return (event !== null) &&
        (event.pathParameters !== null) &&
        (event.pathParameters.id !== null) &&
        (/^[\w-]+$/.test(event.pathParameters.id))
}

function getCognitoUsername(event){
    let authHeader = event.requestContext.authorizer;
    if (authHeader !== null)
    {
        return authHeader.claims["cognito:username"];
    }
    return null;

}

function deleteRecordById(username, recordId) {
    let params = {
        TableName: TABLE_NAME,
        Key: {
            "cognito-username": username,
            "id": recordId
        }
    }

    return docClient.delete(params)
}

// Lambda Handler
exports.deleteToDoItem =
    metricScope(metrics =>
        async (event, context, callback) => {
            metrics.setNamespace('TodoApp')
            metrics.putDimensions({ Service: "deleteTodo" })
            metrics.setProperty("RequestId", context.requestId)

            if (!isValidRequest(context, event)) {
                metrics.putMetric("Error", 1, Unit.Count)
                return response(400, { message: "Error: Invalid request" })
            }

            try {
                let username = getCognitoUsername(event);
                let data = await deleteRecordById(username, event.pathParameters.id).promise()
                metrics.putMetric("Success", 1, Unit.Count)
                return response(200, data)
            } catch (err) {
                metrics.putMetric("Error", 1, Unit.Count)
                return response(400, { message: err.message })
            }
        })
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2019 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.u2',
  name: 'ExportModal',
  extends: 'foam.u2.View',

  documentation: 'Export Modal',

  imports: [
    'exportDriverRegistryDAO'
  ],

  requires: [
    'foam.u2.ModalHeader',
    'foam.u2.layout.Cols',
  ],

  properties: [
    {
      name: 'dataType',
      view: function(_, X) {
        return foam.u2.view.ChoiceView.create({
          dao: X.exportDriverRegistryDAO.where(X.data.predicate),
          objToChoice: function(a) {
            return [a.id, a.id];
          }
        });
      }
    },
    {
      name: 'note',
      view: 'foam.u2.tag.TextArea',
      value: ''
    },
    {
      class: 'FObjectProperty',
      of: 'foam.mlang.predicate.Predicate',
      name: 'predicate'
    },
    'exportData',
    'exportObj'
  ],

  css: `
    ^{
      width: 448px;
      margin: auto;
    }
    ^ .foam-u2-tag-Select {
      width: 125px;
      height: 40px;
      border-radius: 0;
      margin-left: 25px;
      padding: 12px 20px;
      border: solid 1px rgba(164, 179, 184, 0.5);
      background-color: white;
      outline: none;
    }
    ^ .foam-u2-tag-Select:hover {
      cursor: pointer;
    }
    ^ .foam-u2-tag-Select:focus {
      border: solid 1px #59A5D5;
    }
    ^ .label{
      margin: 10px 0px 0px 25px;
    }
    ^ .note {
      height: 150px;
      width: 398px;
      margin-left: 25px;
    }
    ^buttons {
      padding: 12px;
    }

    ^ .foam-u2-ActionView-primary {
      margin: 12px;
    }
  `,

  methods: [
    function initE() {
      this.SUPER();

      this
      .tag(this.ModalHeader.create({
        title: 'Export'
      }))
      .addClass(this.myClass())
      .startContext({ data: this })
        .start()
          .start().addClass('label').add('Data Type').end()
          .start(this.DATA_TYPE).end()
          .start().addClass('label').add('Response').end()
          .start(this.NOTE).addClass('input-box').addClass('note').end()
          .start(this.Cols).style({ 'justify-content': 'flex-start' }).addClass(this.myClass('buttons'))
            .start(this.DOWNLOAD).end()
            .start(this.CONVERT).end()
          .end()
        .end()
      .endContext();
    }
  ],

  actions: [
    async function convert() {
      if ( ! this.exportData && ! this.exportObj ) {
        console.log('Neither exportData nor exportObj exist');
        return;
      }

      var exportDriver = await this.exportDriverRegistryDAO.find(this.dataType);
      exportDriver = foam.lookup(exportDriver.driverName).create();

      this.note = this.exportData ?
        await exportDriver.exportDAO(this.__context__, this.exportData) :
        await exportDriver.exportFObject(this.__context__, this.exportObj);
    },

    async function download() {
      if ( ! this.exportData && ! this.exportObj ) {
        console.log('Neither exportData nor exportObj exist');
        return;
      }

      var exportDriverReg = await this.exportDriverRegistryDAO.find(this.dataType);
      var exportDriver    = foam.lookup(exportDriverReg.driverName).create();

      var p = this.exportData ?
        exportDriver.exportDAO(this.__context__, this.exportData) :
        Promise.resolve(exportDriver.exportFObject(this.__context__, this.exportObj));

      p.then(result => {
        result = 'data:' + exportDriverReg.mimeType + ',' + result;
        var link = document.createElement('a');
        link.setAttribute('href', encodeURI(result));
        link.setAttribute('download', 'data.' + exportDriverReg.extension);
        document.body.appendChild(link);
        link.click();
      })
    }
  ]

});
<commit_msg>Remove trailing comma.<commit_after>/**
 * @license
 * Copyright 2019 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.u2',
  name: 'ExportModal',
  extends: 'foam.u2.View',

  documentation: 'Export Modal',

  imports: [
    'exportDriverRegistryDAO'
  ],

  requires: [
    'foam.u2.ModalHeader',
    'foam.u2.layout.Cols'
  ],

  properties: [
    {
      name: 'dataType',
      view: function(_, X) {
        return foam.u2.view.ChoiceView.create({
          dao: X.exportDriverRegistryDAO.where(X.data.predicate),
          objToChoice: function(a) {
            return [a.id, a.id];
          }
        });
      }
    },
    {
      name: 'note',
      view: 'foam.u2.tag.TextArea',
      value: ''
    },
    {
      class: 'FObjectProperty',
      of: 'foam.mlang.predicate.Predicate',
      name: 'predicate'
    },
    'exportData',
    'exportObj'
  ],

  css: `
    ^{
      width: 448px;
      margin: auto;
    }
    ^ .foam-u2-tag-Select {
      width: 125px;
      height: 40px;
      border-radius: 0;
      margin-left: 25px;
      padding: 12px 20px;
      border: solid 1px rgba(164, 179, 184, 0.5);
      background-color: white;
      outline: none;
    }
    ^ .foam-u2-tag-Select:hover {
      cursor: pointer;
    }
    ^ .foam-u2-tag-Select:focus {
      border: solid 1px #59A5D5;
    }
    ^ .label{
      margin: 10px 0px 0px 25px;
    }
    ^ .note {
      height: 150px;
      width: 398px;
      margin-left: 25px;
    }
    ^buttons {
      padding: 12px;
    }

    ^ .foam-u2-ActionView-primary {
      margin: 12px;
    }
  `,

  methods: [
    function initE() {
      this.SUPER();

      this
      .tag(this.ModalHeader.create({
        title: 'Export'
      }))
      .addClass(this.myClass())
      .startContext({ data: this })
        .start()
          .start().addClass('label').add('Data Type').end()
          .start(this.DATA_TYPE).end()
          .start().addClass('label').add('Response').end()
          .start(this.NOTE).addClass('input-box').addClass('note').end()
          .start(this.Cols).style({ 'justify-content': 'flex-start' }).addClass(this.myClass('buttons'))
            .start(this.DOWNLOAD).end()
            .start(this.CONVERT).end()
          .end()
        .end()
      .endContext();
    }
  ],

  actions: [
    async function convert() {
      if ( ! this.exportData && ! this.exportObj ) {
        console.log('Neither exportData nor exportObj exist');
        return;
      }

      var exportDriver = await this.exportDriverRegistryDAO.find(this.dataType);
      exportDriver = foam.lookup(exportDriver.driverName).create();

      this.note = this.exportData ?
        await exportDriver.exportDAO(this.__context__, this.exportData) :
        await exportDriver.exportFObject(this.__context__, this.exportObj);
    },

    async function download() {
      if ( ! this.exportData && ! this.exportObj ) {
        console.log('Neither exportData nor exportObj exist');
        return;
      }

      var exportDriverReg = await this.exportDriverRegistryDAO.find(this.dataType);
      var exportDriver    = foam.lookup(exportDriverReg.driverName).create();

      var p = this.exportData ?
        exportDriver.exportDAO(this.__context__, this.exportData) :
        Promise.resolve(exportDriver.exportFObject(this.__context__, this.exportObj));

      p.then(result => {
        result = 'data:' + exportDriverReg.mimeType + ',' + result;
        var link = document.createElement('a');
        link.setAttribute('href', encodeURI(result));
        link.setAttribute('download', 'data.' + exportDriverReg.extension);
        document.body.appendChild(link);
        link.click();
      })
    }
  ]

});
<|endoftext|>


<commit_before>// REACT COMPONENTS
import React from 'react';
import { Form } from 'semantic-ui-react';

// PROJECT COMPONENTS
import { FormPartsContainer, IntervalColumnHeadings, CashFlowRow } from './formHelpers';

// COMPONENT HELPER FUNCTIONS
import { getTimeSetter } from '../utils/getTimeSetter';

/**
* @todo Figure out which programs need to know which types of incomes
* and categorize/tag them accordingly.
*
* @todo Calc and store `client.currentUnearnedIncomeMonthly`. I think
* we do still have to keep the other specific income soruces separate
* as they're possibly used in other calculations.
*/

// ========================================
// COMPONENTS
// ========================================
/**
* @todo Is it possible for id's to be the same as the text in the label?
* @todo Stuff like interest of bank accounts? (unearned income?)
* @todo Other assets (not counted in gross income? income categories?)
* @todo Add note: "Household income (a before tax income, and does not include
* funds such as income from children under 18 years old, amounts received
* through training programs funded by HUD, and the income of a live-in aide)"
* (@see {@link http://www.masslegalhelp.org/housing/financial-eligibility})
* @todo Relevant? "State housing programs base eligibility on net yearly income.
* Net yearly income does not include funds such as wages earned by full-time
* students, worker's compensation, and a certain amount of wages earned by a
* tenant 62 or older. It also allows you to deduct certain amounts, such as
* necessary medical expenses and personal care services." (@see {@link
* http://www.masslegalhelp.org/housing/financial-eligibility})
*/

/** @todo description
*
* @function
* @param {object} props
* @property {object} props.__ - explanation
*
* @returns Component
*/
const IncomeForm = function ({ current, time, setClientProperty }) {

  var type = 'income';

  /** Makes sure values are propagated to 'future' properties if needed */
  var ensureFuture = function ( evnt, inputProps ) {
     setClientProperty( evnt, {...inputProps, fillFuture: true });
  };  // End ensureFuture()

  var sharedProps = {
    timeState: current,
    time: time,
    type: type,
    setClientProperty: ensureFuture
  };

  return (

      <IntervalColumnHeadings type={type}/>

      {/* All kinds of things need to be explained. */}
      {/* @todo Change 'labelInfo' to visible blurb at top */}
  	  <CashFlowRow  {...sharedProps} generic='earned' labelInfo='(Weekly income = hourly wage times average number of work hours per week)'>
          Earned income
		  </CashFlowRow>
      <CashFlowRow {...sharedProps} generic='TAFDC'> TAFDC </CashFlowRow>
      <CashFlowRow {...sharedProps} generic='SSI'> SSI </CashFlowRow>
      <CashFlowRow {...sharedProps} generic='SSDI'> SSDI </CashFlowRow>
      <CashFlowRow {...sharedProps} generic='childSupportIn'> Child support received </CashFlowRow>
      <CashFlowRow {...sharedProps} generic='unemployment'> Unemployment </CashFlowRow>
      <CashFlowRow {...sharedProps} generic='workersComp'> Worker’s comp </CashFlowRow>
      <CashFlowRow {...sharedProps} generic='pension'> Pension </CashFlowRow>
      <CashFlowRow {...sharedProps} generic='socialSecurity'> Social security </CashFlowRow>
      <CashFlowRow {...sharedProps} generic='alimony'> Alimony </CashFlowRow>
      <CashFlowRow {...sharedProps} generic='otherIncome'> Other income </CashFlowRow>

  );  // end return

};  // End IncomeForm()

/** @todo description
*
* @function
* @param {object} props
* @property {object} props.__ - explanation
*
* @returns Component
*/
// `props` is a cloned version of the original props. References broken.
const CurrentIncomeStep = function ( props ) {

  const setTimeProp = getTimeSetter( 'current', props.changeClient );

  /** @todo Are these titles accurate now? */
  return (
    <Form className = 'income-form flex-item flex-column'>
      <FormPartsContainer
        title     = 'Current Household Income'
        clarifier = 'Income that you collected in the past 12 months.'
        left      = {{name: 'Previous', func: props.previousStep}}
        right     = {{name: 'Next', func: props.nextStep}}>
          <IncomeForm setClientProperty={setTimeProp} current={props.client.current} time={'current'} />
      </FormPartsContainer>
    </Form>
  );

};  // End CurrentIncomeStep()

export { CurrentIncomeStep };
<commit_msg>Fixes #441, explanation of SNAP being calculated<commit_after>// REACT COMPONENTS
import React from 'react';
import { Form } from 'semantic-ui-react';

// PROJECT COMPONENTS
import { FormPartsContainer, IntervalColumnHeadings, CashFlowRow } from './formHelpers';

// COMPONENT HELPER FUNCTIONS
import { getTimeSetter } from '../utils/getTimeSetter';

/**
* @todo Figure out which programs need to know which types of incomes
* and categorize/tag them accordingly.
*
* @todo Calc and store `client.currentUnearnedIncomeMonthly`. I think
* we do still have to keep the other specific income soruces separate
* as they're possibly used in other calculations.
*/

// ========================================
// COMPONENTS
// ========================================
/**
* @todo Is it possible for id's to be the same as the text in the label?
* @todo Stuff like interest of bank accounts? (unearned income?)
* @todo Other assets (not counted in gross income? income categories?)
* @todo Add note: "Household income (a before tax income, and does not include
* funds such as income from children under 18 years old, amounts received
* through training programs funded by HUD, and the income of a live-in aide)"
* (@see {@link http://www.masslegalhelp.org/housing/financial-eligibility})
* @todo Relevant? "State housing programs base eligibility on net yearly income.
* Net yearly income does not include funds such as wages earned by full-time
* students, worker's compensation, and a certain amount of wages earned by a
* tenant 62 or older. It also allows you to deduct certain amounts, such as
* necessary medical expenses and personal care services." (@see {@link
* http://www.masslegalhelp.org/housing/financial-eligibility})
*/

/** @todo description
*
* @function
* @param {object} props
* @property {object} props.__ - explanation
*
* @returns Component
*/
const IncomeForm = function ({ current, time, setClientProperty }) {

  var type = 'income';

  /** Makes sure values are propagated to 'future' properties if needed */
  var ensureFuture = function ( evnt, inputProps ) {
     setClientProperty( evnt, {...inputProps, fillFuture: true });
  };  // End ensureFuture()

  var sharedProps = {
    timeState: current,
    time: time,
    type: type,
    setClientProperty: ensureFuture
  };

  return (

      <IntervalColumnHeadings type={type}/>

      {/* All kinds of things need to be explained. */}
      {/* @todo Change 'labelInfo' to visible blurb at top */}
  	  <CashFlowRow  {...sharedProps} generic='earned' labelInfo='(Weekly income = hourly wage times average number of work hours per week)'>
          Earned income
		  </CashFlowRow>
      <CashFlowRow {...sharedProps} generic='TAFDC'> TAFDC </CashFlowRow>
      <CashFlowRow {...sharedProps} generic='SSI'> SSI </CashFlowRow>
      <CashFlowRow {...sharedProps} generic='SSDI'> SSDI </CashFlowRow>
      <CashFlowRow {...sharedProps} generic='childSupportIn'> Child support received </CashFlowRow>
      <CashFlowRow {...sharedProps} generic='unemployment'> Unemployment </CashFlowRow>
      <CashFlowRow {...sharedProps} generic='workersComp'> Worker’s comp </CashFlowRow>
      <CashFlowRow {...sharedProps} generic='pension'> Pension </CashFlowRow>
      <CashFlowRow {...sharedProps} generic='socialSecurity'> Social security </CashFlowRow>
      <CashFlowRow {...sharedProps} generic='alimony'> Alimony </CashFlowRow>
      <CashFlowRow {...sharedProps} generic='otherIncome'> Other income </CashFlowRow>
      <Form.Field>This prototype will attempt to make its own calculations for SNAP amount</Form.Field>

  );  // end return

};  // End IncomeForm()

/** @todo description
*
* @function
* @param {object} props
* @property {object} props.__ - explanation
*
* @returns Component
*/
// `props` is a cloned version of the original props. References broken.
const CurrentIncomeStep = function ( props ) {

  const setTimeProp = getTimeSetter( 'current', props.changeClient );

  /** @todo Are these titles accurate now? */
  return (
    <Form className = 'income-form flex-item flex-column'>
      <FormPartsContainer
        title     = 'Current Household Income'
        clarifier = 'Income that you collected in the past 12 months.'
        left      = {{name: 'Previous', func: props.previousStep}}
        right     = {{name: 'Next', func: props.nextStep}}>
          <IncomeForm setClientProperty={setTimeProp} current={props.client.current} time={'current'} />
      </FormPartsContainer>
    </Form>
  );

};  // End CurrentIncomeStep()

export { CurrentIncomeStep };
<|endoftext|>


<commit_before>define('isr/player', ['jquery', 'isr', 'isr/config', 'isr/entity'], function($, Game, Config, Entity) {
   var $activeScene;
   var $activeCharacter;
   var scenePlayer = {
      'church' : {'x': 5, 'y' : 8},
      'family' : {'x': 5, 'y' : 8},
      'temple-1' : {'x': 5, 'y' : 8},
      'temple-2' : {'x': 5, 'y' : 8},
      'war-1' : {'x': 0, 'y' : 3},
      'war-2' : {'x': 0, 'y' : 3}
   };
   // current player position
   var playerMovState = {
      'x' : 0,
      'y' : 2,
      'facing' : 'up',
      // var to indicate if character is currently moving
      'moving' : false
   };
   var lives = 3;
   var startTime = 0;

   Game.director.on('scene:change', function(sceneName) {
      // save current player pos to scenePlayer
      if ($activeScene) {
         scenePlayer[$activeScene[0].id].x = playerMovState.x;
         scenePlayer[$activeScene[0].id].y = playerMovState.y;
      }
      console.log('switched scene to ' +sceneName);
      $activeScene = $('#' + sceneName);
      $activeCharacter = $activeScene.find('.character');
      // get last player pos of player of this scene
      playerMovState.x = scenePlayer[sceneName].x;
      playerMovState.y = scenePlayer[sceneName].y;
      $activeCharacter.css('left', playerMovState.x * Config.tile.width);
      $activeCharacter.css('top', playerMovState.y * Config.tile.height);

   });

   $('body').keyup(function(e) {
      var charPos = $activeCharacter.offset();
      // W || Arrow up
      var up = ~~((e.keyCode === 87) || (e.keyCode === 38));
      // S || Arrow down
      var down = ~~((e.keyCode === 83) || (e.keyCode === 40));
      // A || Arrow left
      var left = ~~((e.keyCode === 65) || (e.keyCode === 37));
      // D || Arrow right
      var right = ~~((e.keyCode === 68) || (e.keyCode === 39));
      // space for interaction
      var space = ~~(e.keyCode === 32);
      var direction;

      if (up) {
         direction = 'up';
      } else if (down) {
         direction = 'down';
      } else if (left) {
         direction = 'left';
      } else if (right) {
         direction = 'right';
      }

      /*var newPos = {
         left : charPos.left,
         top : charPos.top
      };*/

      // move player
      if (!playerMovState.moving && direction) {
        Entity.move({
           'elemMovState' : playerMovState,
           '$scene' : $activeScene,
           '$element' : $activeCharacter,
           'direction' : direction,
           'callback' : function() {
              // trigger global playerMoved and scene specific playerMoved events
              Game.director.currentScene.trigger('playerMoved', playerMovState, $activeScene[0].id);
              //Game.director.trigger('playerMoved', playerMovState);
           }
        });
      }

      if (space) {
         doAction();
      }
      e.stopPropagation();
      e.preventDefault();
      e.stopImmediatePropagation();
      return false;
   });

   /**
    * Function to do stuff like fight or talk
    */
   var doAction = function() {
      // first check direction
      var targetTile = {
         x : playerMovState.x,
         y : playerMovState.y
      };
      switch(playerMovState.facing) {
         case 'up':
            targetTile.y += 1;
         break;
         case 'down':
            targetTile.y -= 1;
         break;
         case 'left':
            targetTile.x -= 1;
         break;
         case 'right':
            targetTile.y += 1;
         break;
      }
      // check if on there is sth on the tile the player is facing to interact with
      var interactableStuff = $activeScene.find('#x' + targetTile.x + '-y' + targetTile.y + ' .interactable');
      interactableStuff.each(function(index) {
         if ($(this).hasClass('attackable')) {
            // TODO do fancy animation
         } else if ($(this).hasClass('talkable')) {
            alert('Your are talking to someone');
         }
      });
      if (interactableStuff.length === 0) {
         //TODO add fancy sword swing into empty air
         alert('You successfully killed empty air');
      }
   };

   var decreaseLives = function() {
      lives--;
      if (lives === 0) {
         alert('Game over');
         location.reload();
      }
   };

   return {
      'playerMovState' : playerMovState,
      'decreaseLives' : decreaseLives
   };
}); <commit_msg>changed action key to "f"<commit_after>define('isr/player', ['jquery', 'isr', 'isr/config', 'isr/entity'], function($, Game, Config, Entity) {
   var $activeScene;
   var $activeCharacter;
   var scenePlayer = {
      'church' : {'x': 5, 'y' : 8},
      'family' : {'x': 5, 'y' : 8},
      'temple-1' : {'x': 5, 'y' : 8},
      'temple-2' : {'x': 5, 'y' : 8},
      'war-1' : {'x': 0, 'y' : 3},
      'war-2' : {'x': 0, 'y' : 3}
   };
   // current player position
   var playerMovState = {
      'x' : 0,
      'y' : 2,
      'facing' : 'up',
      // var to indicate if character is currently moving
      'moving' : false
   };
   var lives = 3;
   var startTime = 0;

   Game.director.on('scene:change', function(sceneName) {
      // save current player pos to scenePlayer
      if ($activeScene) {
         scenePlayer[$activeScene[0].id].x = playerMovState.x;
         scenePlayer[$activeScene[0].id].y = playerMovState.y;
      }
      console.log('switched scene to ' +sceneName);
      $activeScene = $('#' + sceneName);
      $activeCharacter = $activeScene.find('.character');
      // get last player pos of player of this scene
      playerMovState.x = scenePlayer[sceneName].x;
      playerMovState.y = scenePlayer[sceneName].y;
      $activeCharacter.css('left', playerMovState.x * Config.tile.width);
      $activeCharacter.css('top', playerMovState.y * Config.tile.height);

   });

   $('body').keyup(function(e) {
      var charPos = $activeCharacter.offset();
      // W || Arrow up
      var up = ~~((e.keyCode === 87) || (e.keyCode === 38));
      // S || Arrow down
      var down = ~~((e.keyCode === 83) || (e.keyCode === 40));
      // A || Arrow left
      var left = ~~((e.keyCode === 65) || (e.keyCode === 37));
      // D || Arrow right
      var right = ~~((e.keyCode === 68) || (e.keyCode === 39));
      // F for interaction
      var action = ~~(e.keyCode === 70);
      var direction;

      if (up) {
         direction = 'up';
      } else if (down) {
         direction = 'down';
      } else if (left) {
         direction = 'left';
      } else if (right) {
         direction = 'right';
      }

      /*var newPos = {
         left : charPos.left,
         top : charPos.top
      };*/

      // move player
      if (!playerMovState.moving && direction) {
        Entity.move({
           'elemMovState' : playerMovState,
           '$scene' : $activeScene,
           '$element' : $activeCharacter,
           'direction' : direction,
           'callback' : function() {
              // trigger global playerMoved and scene specific playerMoved events
              Game.director.currentScene.trigger('playerMoved', playerMovState, $activeScene[0].id);
              //Game.director.trigger('playerMoved', playerMovState);
           }
        });
      }

      if (action) {
         doAction();
      }
      e.stopPropagation();
      e.preventDefault();
      e.stopImmediatePropagation();
      return false;
   });

   /**
    * Function to do stuff like fight or talk
    */
   var doAction = function() {
      // first check direction
      var targetTile = {
         x : playerMovState.x,
         y : playerMovState.y
      };
      switch(playerMovState.facing) {
         case 'up':
            targetTile.y += 1;
         break;
         case 'down':
            targetTile.y -= 1;
         break;
         case 'left':
            targetTile.x -= 1;
         break;
         case 'right':
            targetTile.y += 1;
         break;
      }
      // check if on there is sth on the tile the player is facing to interact with
      var interactableStuff = $activeScene.find('#x' + targetTile.x + '-y' + targetTile.y + ' .interactable');
      interactableStuff.each(function(index) {
         if ($(this).hasClass('attackable')) {
            // TODO do fancy animation
         } else if ($(this).hasClass('talkable')) {
            alert('Your are talking to someone');
         }
      });
      if (interactableStuff.length === 0) {
         //TODO add fancy sword swing into empty air
         alert('You successfully killed empty air');
      }
   };

   var decreaseLives = function() {
      lives--;
      if (lives === 0) {
         alert('Game over');
         location.reload();
      }
   };

   return {
      'playerMovState' : playerMovState,
      'decreaseLives' : decreaseLives
   };
}); <|endoftext|>


<commit_before>(function() {

  this.Player = function() {

    var defaults = {
        videoId: null,
        playerClass: 'custom-player',
        extraClass: null,
        width: 480,
        height: 270,
        autoPlay: null,
        controls: null,
        relatedVideos: 0,
        showInfo: null,
        wrap: null,
        wrapClass: 'custom-player-wrapper',
        playButton: null,
        playButtonClass: 'play-button',
        playButtonText: 'Play',
        pauseButton: null,
        pauseButtonClass: 'pause-button',
        pauseButtonText: 'Pause',
        mute: null,
        placement: null
    };

    // Add our defaults above into a global array of options
    if ( arguments[0] && typeof arguments[0] === 'object' ) {
      this.options = extendDefaults( defaults, arguments[0] );
    }

    if( document.readyState !== 'complete' ) {
      document.addEventListener( 'DOMContentLoaded', ( this.init ).bind( this ), false);
    } else {
      // Run Player.init()
      this.init();
    }

  };

  Player.prototype.init = function() {

    var this_ = this,
        options = this_.options;

    // If any extraClass names have been added add them
    var extraClass = options.extraClass,
        classCheck = extraClass === null,
        addExtraClass = classCheck ? '' : ' ' + extraClass;

    if ( !document.getElementById( options.videoId ) ) {
      this.generateEl( 'div', options.videoId, options.playerClass + addExtraClass );
    }

    // Generate the wrapper
    if ( options.wrap || options.wrapClass !== 'custom-player-wrapper' ) {
      this.wrapper = document.createElement( 'div' );
      this.wrapper.className = options.wrapClass + ' ' + options.videoId;
      this.customPlayer = document.getElementById( options.videoId );
      this.wrapper.appendChild( this.customPlayer.cloneNode( true ) );
      this.customPlayer.parentNode.replaceChild( this.wrapper, this.customPlayer );
    }

    var callback = ( function() {
      this_.player = new YT.Player( options.videoId, {
        videoId: options.videoId,
        height: options.height,
        width: options.width,
        playerVars: {
          autoplay: options.autoPlay,
          controls: options.controls,
          rel: options.relatedVideos,
          showinfo: options.showInfo,
        },
        events: {
          onReady: ( this_._onPlayerReady ).bind( this ),
          onStateChange: ( this_._onStateChange ).bind( this )
        }
      });

      // Load custom controls
      if ( options.playButton || options.pauseButton ) {
        this_.customControls();
      }
    }).bind( this );

    // Test if the youtube api is loaded
    if ( 'YT' in window ) {
      if ( window.YT.loaded === 0 ) {
        if ( window.onYouTubePlayerAPIReady ) {
          var oldonYouTubePlayerAPIReady = window.onYouTubePlayerAPIReady;
        }
        window.onYouTubePlayerAPIReady = function() {
          if ( window.onYouTubePlayerAPIReady ) {
            oldonYouTubePlayerAPIReady();
          }
          callback();
        };
      } else {
        callback();
      }
    } else {
      window.YT = {
        loaded: 0
      };

      window.onYouTubePlayerAPIReady = function() {
        callback();
      };

      var tag = document.createElement( 'script' );
      var firstScriptTag = document.getElementsByTagName( 'script' )[0];

      tag.src = 'https://www.youtube.com/iframe_api';
      firstScriptTag.parentNode.insertBefore( tag, firstScriptTag );
    }

  };

  // Controls
  Player.prototype.customControls = function() {
    var this_ = this,
        options = this_.options,
        customControlsClass = 'custom-controls',
        customControlsSelector = '.' + customControlsClass;

    // Cache the created element so we know which is which
    // when multiple videos are present
    this.videoControls = this.generateEl( 'div', null, customControlsClass );
    // If there is a wrapper add the controls inside it
    if ( this_.options.wrap ) {
      var wrapper = document.querySelector( '.' + this_.options.wrapClass ),
          controls = document.querySelector( customControlsSelector );

      wrapper.appendChild( controls );
    }

    // Play button
    // Check if anything to do with Play button is enabled
    if ( options.playButton ) {
      var playButtonEl;

      playButtonEl = document.createElement( 'button' );
      // Store class name of button
      playButtonClass = playButtonEl.className = this_.options.videoId + '-' + this_.options.playButtonClass;
      playButtonEl.innerHTML = options.playButtonText;
      this.videoControls.appendChild( playButtonEl );

      document.querySelector( '.' + playButtonClass ).addEventListener( 'click', function() {
        this_.player.playVideo();
      });
    }

    // Pause video
    // Check if anything to do with Pause button is enabled
    if ( options.pauseButton ) {
      var pauseButtonEl;

      pauseButtonEl = document.createElement( 'button' );
      pauseButtonEl.innerHTML = options.pauseButtonText;

      pauseButtonEl.className = options.videoId + '-' + options.pauseButtonClass;
      this.videoControls.appendChild( pauseButtonEl );

      document.querySelector( '.' + pauseButtonClass ).addEventListener( 'click', function() {
        this_.player.pauseVideo();
      });
    }

  };

  // Events
  // Player ready
  Player.prototype._onPlayerReady = function( event ) {
    if ( this.options.mute === 1 ) {
      event.target.mute();
    }
  };
  // Player state change
  Player.prototype._onStateChange = function( event ) {};

  // Private Methods

  // Build the necessary markup
  // and inject it into the document if needed
  Player.prototype.generateEl = function( el, id, className ) {
    var docFrag, element;

    docFrag = document.createDocumentFragment();
    element = document.createElement( el );

    if ( id ) {
      element.id = id;
    }
    if ( className ) {
      element.className = className;
    }

    docFrag.appendChild( element );

    if ( this.options.placement !== null ) {
      var selector = document.querySelector( this.options.placement );
      if ( !selector ) { throw new selectorError( selector ); }
      selector.appendChild( docFrag );
    } else {
      document.body.appendChild( docFrag );
    }

    function selectorError( value ) {
      this.message = 'Make sure your selector is on the page';
      this.toString = function() {
        return this.value + ' ' + this.message;
      };
    }

    return element;
  };

  // Utility method to extend defaults with user options
  function extendDefaults( source, properties ) {
    var property;
    for ( property in properties ) {
      if ( properties.hasOwnProperty( property ) ) {
        source[ property ] = properties[ property ];
      }
    }
    return source;
  }

  // Look for all elements that has a data-video-id
  // and populate an array
  var playerData = document.querySelectorAll( '[data-video-id]' );
  var standaloneVideos = [];

  [].slice.call(playerData).forEach(function( vid ) {
    var options = vid.dataset;
    options.element = vid;
    vid.id = vid.dataset.videoId;
    standaloneVideos.push(new Player(options));
  });

  return standaloneVideos;

})();
<commit_msg>Removed unnecessary variable<commit_after>(function() {

  this.Player = function() {

    var defaults = {
        videoId: null,
        playerClass: 'custom-player',
        extraClass: null,
        width: 480,
        height: 270,
        autoPlay: null,
        controls: null,
        relatedVideos: 0,
        showInfo: null,
        wrap: null,
        wrapClass: 'custom-player-wrapper',
        playButton: null,
        playButtonClass: 'play-button',
        playButtonText: 'Play',
        pauseButton: null,
        pauseButtonClass: 'pause-button',
        pauseButtonText: 'Pause',
        mute: null,
        placement: null
    };

    // Add our defaults above into a global array of options
    if ( arguments[0] && typeof arguments[0] === 'object' ) {
      this.options = extendDefaults( defaults, arguments[0] );
    }

    if( document.readyState !== 'complete' ) {
      document.addEventListener( 'DOMContentLoaded', ( this.init ).bind( this ), false);
    } else {
      // Run Player.init()
      this.init();
    }

  };

  Player.prototype.init = function() {

    var this_ = this,
        options = this_.options;

    // If any extraClass names have been added add them
    var extraClass = options.extraClass,
        classCheck = extraClass === null,
        addExtraClass = classCheck ? '' : ' ' + extraClass;

    if ( !document.getElementById( options.videoId ) ) {
      this.generateEl( 'div', options.videoId, options.playerClass + addExtraClass );
    }

    // Generate the wrapper
    if ( options.wrap || options.wrapClass !== 'custom-player-wrapper' ) {
      this.wrapper = document.createElement( 'div' );
      this.wrapper.className = options.wrapClass + ' ' + options.videoId;
      this.customPlayer = document.getElementById( options.videoId );
      this.wrapper.appendChild( this.customPlayer.cloneNode( true ) );
      this.customPlayer.parentNode.replaceChild( this.wrapper, this.customPlayer );
    }

    var callback = ( function() {
      this_.player = new YT.Player( options.videoId, {
        videoId: options.videoId,
        height: options.height,
        width: options.width,
        playerVars: {
          autoplay: options.autoPlay,
          controls: options.controls,
          rel: options.relatedVideos,
          showinfo: options.showInfo,
        },
        events: {
          onReady: ( this_._onPlayerReady ).bind( this ),
          onStateChange: ( this_._onStateChange ).bind( this )
        }
      });

      // Load custom controls
      if ( options.playButton || options.pauseButton ) {
        this_.customControls();
      }
    }).bind( this );

    // Test if the youtube api is loaded
    if ( 'YT' in window ) {
      if ( window.YT.loaded === 0 ) {
        if ( window.onYouTubePlayerAPIReady ) {
          var oldonYouTubePlayerAPIReady = window.onYouTubePlayerAPIReady;
        }
        window.onYouTubePlayerAPIReady = function() {
          if ( window.onYouTubePlayerAPIReady ) {
            oldonYouTubePlayerAPIReady();
          }
          callback();
        };
      } else {
        callback();
      }
    } else {
      window.YT = {
        loaded: 0
      };

      window.onYouTubePlayerAPIReady = function() {
        callback();
      };

      var tag = document.createElement( 'script' );
      var firstScriptTag = document.getElementsByTagName( 'script' )[0];

      tag.src = 'https://www.youtube.com/iframe_api';
      firstScriptTag.parentNode.insertBefore( tag, firstScriptTag );
    }

  };

  // Controls
  Player.prototype.customControls = function() {
    var this_ = this,
        options = this_.options,
        customControlsClass = 'custom-controls',
        customControlsSelector = '.' + customControlsClass;

    // Cache the created element so we know which is which
    // when multiple videos are present
    this.videoControls = this.generateEl( 'div', null, customControlsClass );
    // If there is a wrapper add the controls inside it
    if ( this_.options.wrap ) {
      var wrapper = document.querySelector( '.' + this_.options.wrapClass ),
          controls = document.querySelector( customControlsSelector );

      wrapper.appendChild( controls );
    }

    // Play button
    // Check if anything to do with Play button is enabled
    if ( options.playButton ) {
      var playButtonEl;

      playButtonEl = document.createElement( 'button' );
      playButtonEl.innerHTML = options.playButtonText;

      playButtonEl.className = options.videoId + '-' + options.playButtonClass;
      this.videoControls.appendChild( playButtonEl );

      document.querySelector( '.' + playButtonEl.className ).addEventListener( 'click', function() {
        this_.player.playVideo();
      });
    }

    // Pause video
    // Check if anything to do with Pause button is enabled
    if ( options.pauseButton ) {
      var pauseButtonEl;

      pauseButtonEl = document.createElement( 'button' );
      pauseButtonEl.innerHTML = options.pauseButtonText;

      pauseButtonEl.className = options.videoId + '-' + options.pauseButtonClass;
      this.videoControls.appendChild( pauseButtonEl );

      document.querySelector( '.' + pauseButtonEl.className ).addEventListener( 'click', function() {
        this_.player.pauseVideo();
      });
    }

  };

  // Events
  // Player ready
  Player.prototype._onPlayerReady = function( event ) {
    if ( this.options.mute === 1 ) {
      event.target.mute();
    }
  };
  // Player state change
  Player.prototype._onStateChange = function( event ) {};

  // Private Methods

  // Build the necessary markup
  // and inject it into the document if needed
  Player.prototype.generateEl = function( el, id, className ) {
    var docFrag, element;

    docFrag = document.createDocumentFragment();
    element = document.createElement( el );

    if ( id ) {
      element.id = id;
    }
    if ( className ) {
      element.className = className;
    }

    docFrag.appendChild( element );

    if ( this.options.placement !== null ) {
      var selector = document.querySelector( this.options.placement );
      if ( !selector ) { throw new selectorError( selector ); }
      selector.appendChild( docFrag );
    } else {
      document.body.appendChild( docFrag );
    }

    function selectorError( value ) {
      this.message = 'Make sure your selector is on the page';
      this.toString = function() {
        return this.value + ' ' + this.message;
      };
    }

    return element;
  };

  // Utility method to extend defaults with user options
  function extendDefaults( source, properties ) {
    var property;
    for ( property in properties ) {
      if ( properties.hasOwnProperty( property ) ) {
        source[ property ] = properties[ property ];
      }
    }
    return source;
  }

  // Look for all elements that has a data-video-id
  // and populate an array
  var playerData = document.querySelectorAll( '[data-video-id]' );
  var standaloneVideos = [];

  [].slice.call(playerData).forEach(function( vid ) {
    var options = vid.dataset;
    options.element = vid;
    vid.id = vid.dataset.videoId;
    standaloneVideos.push(new Player(options));
  });

  return standaloneVideos;

})();
<|endoftext|>


<commit_before>(function() {

  this.Player = function() {

    var defaults = {
        videoId: null,
        playerClass: 'custom-player',
        extraClass: null,
        width: 480,
        height: 270,
        autoPlay: null,
        controls: null,
        relatedVideos: 0,
        showInfo: null,
        wrap: null,
        wrapClass: 'custom-player-wrapper',
        playButton: null,
        playButtonText: 'Play',
        pauseButton: null,
        pauseButtonText: 'Pause',
        mute: null,
        placement: null
    };

    // Add our defaults above into a global array of options
    if (arguments[0] && typeof arguments[0] === 'object') {
      this.options = extendDefaults( defaults, arguments[0] );
    }

    // Global vars
    this.wrap = this.options.wrap;
    this.extraClass = this.options.extraClass;
    this.mute = this.options.mute;
    this.placement = this.options.placement;

    var usingDataAttr = false;

    if( document.readyState !== 'complete' ) {
      document.addEventListener('DOMContentLoaded', (this.init).bind(this), false);
    } else {
      // Run Player.init()
      this.init();
    }

  };

  Player.prototype.init = function() {
    var this_ = this;

    if( !usingDataAttr ) {
      // If any extraClass names have been added add them
      var extraClass = this_.options.extraClass,
          classCheck = extraClass === null,
          addExtraClass = classCheck ? '' : ' ' + extraClass;

      this.generateEl( 'div', this_.options.videoId, this_.options.playerClass + addExtraClass );
    } else {
      playerData.id = this_.options.videoId;
    }

    // Generate the wrapper
    if ( this.wrap ) {
      this.wrapper = document.createElement( 'div' );
      this.wrapper.className = this_.options.wrapClass;
      this.customPlayer = document.getElementById( this_.options.videoId );
      this.wrapper.appendChild( this.customPlayer.cloneNode( true ) );
      this.customPlayer.parentNode.replaceChild( this.wrapper, this.customPlayer );
    }

    var callback = (function() {
      this_.player = new YT.Player(this_.options.videoId, {
        videoId: this_.options.videoId,
        height: this_.options.height,
        width: this_.options.width,
        playButton: this_.options.playButton,
        pauseButton: this_.options.pauseButton,
        playerVars: {
          autoplay: this_.options.autoPlay,
          controls: this_.options.controls,
          rel: this_.options.relatedVideos,
          showinfo: this_.options.showInfo,
        },
        events: {
          onReady: (this_._onPlayerReady).bind(this),
          onStateChange: (this_._onStateChange).bind(this)
        }
      });

      // Load custom controls
      if ( this_.options.playButton || this_.options.pauseButton ) {
        this_.customControls();
      }
    }).bind(this);

    // Test if the youtube api is loaded
    if ('YT' in window) {
      if (window.YT.loaded === 0) {
        if (window.onYouTubePlayerAPIReady) {
          var oldonYouTubePlayerAPIReady = window.onYouTubePlayerAPIReady;
        }
        window.onYouTubePlayerAPIReady = function() {
          if (window.onYouTubePlayerAPIReady) { oldonYouTubePlayerAPIReady(); }
          callback();
        };
      } else {
        callback();
      }
    } else {
      window.YT = {
        loaded: 0
      };

      window.onYouTubePlayerAPIReady = function() {
        callback();
      };

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

      tag.src = 'https://www.youtube.com/iframe_api';
      var firstScriptTag = document.getElementsByTagName( 'script' )[0];
      firstScriptTag.parentNode.insertBefore( tag, firstScriptTag );
    }

  };

  // Controls
  Player.prototype.customControls = function() {
    var this_ = this;
    var customControls = 'custom-controls';

    this.generateEl( 'div', customControls );

    // Find the first custom-controls element
    // TODO: Look for multiple elements on the page
      var controls = document.querySelector( '.' + customControls ),
          wrapper = document.querySelector( '.custom-player-wrapper' );
    if ( this_.options.wrap ) {

      wrapper.appendChild( controls );
    }

    // Play button
    if ( this_.options.playButton ) {
      var playButtonEl;
      playButtonEl = document.createElement( 'button' );
      document.querySelector( '.' + customControls ).appendChild( playButtonEl );
      playButtonEl.innerHTML = this_.options.playButtonText;
      playButtonEl.className = this_.options.playButton;

      document.querySelector( '.' + this.playButton ).addEventListener( 'click', function() {
        this_.player.playVideo();
      });
    }

    // Pause video
    if ( this_.options.pauseButton ) {
      var pauseButtonEl;
      pauseButtonEl = document.createElement( 'button' );
      document.querySelector( '.' + customControls ).appendChild( pauseButtonEl );
      pauseButtonEl.innerHTML = this_.options.pauseButtonText;
      pauseButtonEl.className = this_.options.pauseButton;

      document.querySelector( '.' + this_.options.pauseButton ).addEventListener( 'click', function() {
        this_.player.pauseVideo();
      });
    }

  };

  // Events
  Player.prototype._onPlayerReady = function( event ) {
    if ( this.mute === 1) {
      event.target.mute();
    }
  };
  Player.prototype._onStateChange = function( event ) {};

  // Private Methods

  // Build the necessary markup
  // and inject it into the document if needed
  Player.prototype.generateEl = function( el, id, className ) {
    var docFrag, element;

    docFrag = document.createDocumentFragment();
    element = document.createElement( el );

    if ( id ) {
      element.id = id;
    }
    if ( className ) {
      element.className = className;
    }

    docFrag.appendChild( element );

    if ( this.placement !== null ) {
      var selector = document.querySelector( this.placement );
      selector.appendChild( docFrag );
    } else {
      document.body.appendChild( docFrag );
    }
  }

  // Utility method to extend defaults with user options
  function extendDefaults( source, properties ) {
    var property;
    for ( property in properties ) {
      if ( properties.hasOwnProperty( property ) ) {
        source[ property ] = properties[ property ];
      }
    }
    return source;
  }

  // Look for the first element that contains data-video-id
  // TODO: Look for multiple data-video-id's
  var playerData = document.querySelector( '[data-video-id]' );
  if ( playerData ) {
    var usingDataAttr = true;
    // If it exists create a new Player
    // and populate it with the rest of the data attributes
    var options = playerData.dataset,
        newPlayer = new Player(options);
  }

})();
<commit_msg>Split out custom controls selector and class<commit_after>(function() {

  this.Player = function() {

    var defaults = {
        videoId: null,
        playerClass: 'custom-player',
        extraClass: null,
        width: 480,
        height: 270,
        autoPlay: null,
        controls: null,
        relatedVideos: 0,
        showInfo: null,
        wrap: null,
        wrapClass: 'custom-player-wrapper',
        playButton: null,
        playButtonText: 'Play',
        pauseButton: null,
        pauseButtonText: 'Pause',
        mute: null,
        placement: null
    };

    // Add our defaults above into a global array of options
    if (arguments[0] && typeof arguments[0] === 'object') {
      this.options = extendDefaults( defaults, arguments[0] );
    }

    // Global vars
    this.wrap = this.options.wrap;
    this.extraClass = this.options.extraClass;
    this.mute = this.options.mute;
    this.placement = this.options.placement;

    var usingDataAttr = false;

    if( document.readyState !== 'complete' ) {
      document.addEventListener('DOMContentLoaded', (this.init).bind(this), false);
    } else {
      // Run Player.init()
      this.init();
    }

  };

  Player.prototype.init = function() {
    var this_ = this;

    if( !usingDataAttr ) {
      // If any extraClass names have been added add them
      var extraClass = this_.options.extraClass,
          classCheck = extraClass === null,
          addExtraClass = classCheck ? '' : ' ' + extraClass;

      this.generateEl( 'div', this_.options.videoId, this_.options.playerClass + addExtraClass );
    } else {
      playerData.id = this_.options.videoId;
    }

    // Generate the wrapper
    if ( this.wrap ) {
      this.wrapper = document.createElement( 'div' );
      this.wrapper.className = this_.options.wrapClass;
      this.customPlayer = document.getElementById( this_.options.videoId );
      this.wrapper.appendChild( this.customPlayer.cloneNode( true ) );
      this.customPlayer.parentNode.replaceChild( this.wrapper, this.customPlayer );
    }

    var callback = (function() {
      this_.player = new YT.Player(this_.options.videoId, {
        videoId: this_.options.videoId,
        height: this_.options.height,
        width: this_.options.width,
        playButton: this_.options.playButton,
        pauseButton: this_.options.pauseButton,
        playerVars: {
          autoplay: this_.options.autoPlay,
          controls: this_.options.controls,
          rel: this_.options.relatedVideos,
          showinfo: this_.options.showInfo,
        },
        events: {
          onReady: (this_._onPlayerReady).bind(this),
          onStateChange: (this_._onStateChange).bind(this)
        }
      });

      // Load custom controls
      if ( this_.options.playButton || this_.options.pauseButton ) {
        this_.customControls();
      }
    }).bind(this);

    // Test if the youtube api is loaded
    if ('YT' in window) {
      if (window.YT.loaded === 0) {
        if (window.onYouTubePlayerAPIReady) {
          var oldonYouTubePlayerAPIReady = window.onYouTubePlayerAPIReady;
        }
        window.onYouTubePlayerAPIReady = function() {
          if (window.onYouTubePlayerAPIReady) { oldonYouTubePlayerAPIReady(); }
          callback();
        };
      } else {
        callback();
      }
    } else {
      window.YT = {
        loaded: 0
      };

      window.onYouTubePlayerAPIReady = function() {
        callback();
      };

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

      tag.src = 'https://www.youtube.com/iframe_api';
      var firstScriptTag = document.getElementsByTagName( 'script' )[0];
      firstScriptTag.parentNode.insertBefore( tag, firstScriptTag );
    }

  };

  // Controls
  Player.prototype.customControls = function() {
    var this_ = this;
    var customControlsClass = 'custom-controls';
    var customControlsSelector = '.' + customControlsClass;

    this.generateEl( 'div', null, customControlsClass );

    // Find the first custom-controls element
    // TODO: Look for multiple elements on the page
    if ( this_.options.wrap ) {
      var controls = document.querySelector( customControlsSelector ),
          wrapper = document.querySelector( '.' + this_.options.wrapClass );

      wrapper.appendChild( controls );
    }

    // Play button
    if ( this_.options.playButton ) {
      var playButtonEl;
      playButtonEl = document.createElement( 'button' );
      playButtonEl.innerHTML = this_.options.playButtonText;
      playButtonEl.className = this_.options.playButton;
      document.querySelector( customControlsSelector ).appendChild( playButtonEl );

      document.querySelector( '.' + this.playButton ).addEventListener( 'click', function() {
        this_.player.playVideo();
      });
    }

    // Pause video
    if ( this_.options.pauseButton ) {
      var pauseButtonEl;
      pauseButtonEl = document.createElement( 'button' );
      pauseButtonEl.innerHTML = this_.options.pauseButtonText;
      pauseButtonEl.className = this_.options.pauseButton;

      document.querySelector( customControlsSelector ).appendChild( pauseButtonEl );
      document.querySelector( '.' + this_.options.pauseButton ).addEventListener( 'click', function() {
        this_.player.pauseVideo();
      });
    }

  };

  // Events
  Player.prototype._onPlayerReady = function( event ) {
    if ( this.mute === 1) {
      event.target.mute();
    }
  };
  Player.prototype._onStateChange = function( event ) {};

  // Private Methods

  // Build the necessary markup
  // and inject it into the document if needed
  Player.prototype.generateEl = function( el, id, className ) {
    var docFrag, element;

    docFrag = document.createDocumentFragment();
    element = document.createElement( el );

    if ( id ) {
      element.id = id;
    }
    if ( className ) {
      element.className = className;
    }

    docFrag.appendChild( element );

    if ( this.placement !== null ) {
      var selector = document.querySelector( this.placement );
      selector.appendChild( docFrag );
    } else {
      document.body.appendChild( docFrag );
    }
  }

  // Utility method to extend defaults with user options
  function extendDefaults( source, properties ) {
    var property;
    for ( property in properties ) {
      if ( properties.hasOwnProperty( property ) ) {
        source[ property ] = properties[ property ];
      }
    }
    return source;
  }

  // Look for the first element that contains data-video-id
  // TODO: Look for multiple data-video-id's
  var playerData = document.querySelector( '[data-video-id]' );
  if ( playerData ) {
    var usingDataAttr = true;
    // If it exists create a new Player
    // and populate it with the rest of the data attributes
    var options = playerData.dataset,
        newPlayer = new Player(options);
  }

})();
<|endoftext|>