Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static/js/framework/clientmon.js b/static/js/framework/clientmon.js
new file mode 100644
index 0000000..aa6dc0a
--- /dev/null
+++ b/static/js/framework/clientmon.js
@@ -0,0 +1,51 @@
+/* Copyright 2016 The Chromium Authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+(function(window) {
+  'use strict';
+
+  // This code sets up a reporting mechanism for uncaught javascript errors
+  // to the server. It reports at most every THRESHOLD_MS milliseconds and
+  // each report contains error signatures with counts.
+
+  let errBuff = {};
+  let THRESHOLD_MS = 2000;
+
+  function throttle(fn) {
+    let last, timer;
+    return function() {
+      let now = Date.now();
+      if (last && now < last + THRESHOLD_MS) {
+        clearTimeout(timer);
+        timer = setTimeout(function() {
+          last = now;
+          fn.apply();
+        }, THRESHOLD_MS + last - now);
+      } else {
+        last = now;
+        fn.apply();
+      }
+    };
+  }
+  let flushErrs = throttle(function() {
+    let data = {errors: JSON.stringify(errBuff)};
+    CS_doPost('/_/clientmon.do', null, data);
+    errBuff = {};
+  });
+
+  window.addEventListener('error', function(evt) {
+    let signature = evt.message;
+    if (evt.error instanceof Error) {
+      signature += '\n' + evt.error.stack;
+    }
+    if (!errBuff[signature]) {
+      errBuff[signature] = 0;
+    }
+    errBuff[signature] += 1;
+    flushErrs();
+  });
+})(window);
diff --git a/static/js/framework/env.js b/static/js/framework/env.js
new file mode 100644
index 0000000..baf19cb
--- /dev/null
+++ b/static/js/framework/env.js
@@ -0,0 +1,73 @@
+/* Copyright 2016 The Chromium Authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+/**
+ * @fileoverview Defines the type of the CS_env Javascript object
+ * provided by the Codesite server.
+ *
+ * This is marked as an externs file so that any variable defined with a
+ * CS.env type will not have its properties renamed.
+ * @externs
+ */
+
+/** Codesite namespace object. */
+var CS = {};
+
+/**
+ * Javascript object holding basic information about the current page.
+ * This is defined as an interface so that we can use CS.env as a Closure
+ * type name, but it will never be implemented; rather, it will be
+ * made available on every page as the global object CS_env (see
+ * codesite/templates/demetrius/header.ezt).
+ *
+ * The type of the CS_env global object will actually be one of
+ * CS.env, CS.project_env, etc. depending on the page
+ * rendered by the server.
+ *
+ * @interface
+ */
+CS.env = function() {};
+
+/**
+ * Like relativeBaseUrl, but a full URL preceded by http://code.google.com
+ * @type {string}
+ */
+CS.env.prototype.absoluteBaseUrl;
+
+/**
+ * Path to versioned static assets (mostly js and css).
+ * @type {string}
+ */
+CS.env.prototype.appVersion;
+
+/**
+ * Request token for the logged-in user, or null for the anonymous user.
+ * @type {?string}
+ */
+CS.env.prototype.token;
+
+/**
+ * Email address of the logged-in user, or null for anon.
+ * @type {?string}
+ */
+CS.env.prototype.loggedInUserEmail;
+
+/**
+ * Url to the logged-in user's profile, or null for anon.
+ * @type {?string}
+ */
+CS.env.prototype.profileUrl;
+
+/**
+ * CS.env specialization for browsing project pages.
+ * @interface
+ * @extends {CS.env}
+ */
+CS.project_env = function() {};
+
+/** @type {string} */
+CS.project_env.prototype.projectName;
diff --git a/static/js/framework/externs.js b/static/js/framework/externs.js
new file mode 100644
index 0000000..a0375a1
--- /dev/null
+++ b/static/js/framework/externs.js
@@ -0,0 +1,25 @@
+/* Copyright 2016 The Chromium Authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+/** @type {CS.env} */
+var CS_env;
+
+// Exported functions must be mentioned in this externs file so that JSCompiler
+// will allow exporting functions by writing '_hideID = CS_hideID'.
+var _hideID;
+var _showID;
+var _hideEl;
+var _showEl;
+var _showInstead;
+var _toggleHidden;
+var _toggleCollapse;
+var _CS_dismissCue;
+var _CS_updateProjects;
+var _CP_checkProjectName;
+var _TKR_toggleStar;
+var _TKR_toggleStarLocal;
+var _TKR_syncStarIcons;
diff --git a/static/js/framework/framework-ajax.js b/static/js/framework/framework-ajax.js
new file mode 100644
index 0000000..038c4c3
--- /dev/null
+++ b/static/js/framework/framework-ajax.js
@@ -0,0 +1,153 @@
+/* Copyright 2016 The Chromium Authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+
+/**
+ * @fileoverview AJAX-related helper functions.
+ */
+
+
+var DEBOUNCE_THRESH_MS = 2000;
+
+
+/**
+ * Simple debouncer to handle text input.  Don't try to hit the server
+ * until the user has stopped typing for a few seconds.  E.g.,
+ * var debouncedKeyHandler = debounce(keyHandler);
+ * el.addEventListener('keyup', debouncedKeyHandler);
+ */
+function debounce(func, opt_threshold_ms) {
+  let timeout;
+  return function() {
+    let context = this, args = arguments;
+    let later = function() {
+      timeout = null;
+      func.apply(context, args);
+    };
+    clearTimeout(timeout);
+    timeout = setTimeout(later, opt_threshold_ms || DEBOUNCE_THRESH_MS);
+  };
+}
+
+
+/**
+ * Builds a POST string from a parameter dictionary.
+ * @param {Array|Object} args: parameters to encode. Either an object
+ *   mapping names to values or an Array of doubles containing [key, value].
+ * @return {string} encoded POST data.
+ */
+function CS_postData(args) {
+  let params = [];
+
+  if (args instanceof Array) {
+    for (var key in args) {
+      let inputValue = args[key];
+      let name = inputValue[0];
+      let value = inputValue[1];
+      if (value !== undefined) {
+        params.push(name + '=' + encodeURIComponent(String(value)));
+      }
+    }
+  } else {
+    for (var key in args) {
+      params.push(key + '=' + encodeURIComponent(String(args[key])));
+    }
+  }
+
+  params.push('token=' + encodeURIComponent(window.prpcClient.token));
+
+  return params.join('&');
+}
+
+/**
+ * Helper for an extremely common kind of XHR: a POST with an XHRF token
+ * where we silently ignore server or connectivity errors.  If the token
+ * has expired, get a new one and retry the original request with the new
+ * token.
+ * @param {string} url request destination.
+ * @param {function(event)} callback function to be called
+ *   upon successful completion of the request.
+ * @param {Object} args parameters to encode as POST data.
+ */
+function CS_doPost(url, callback, args) {
+  window.prpcClient.ensureTokenIsValid().then(() => {
+    let xh = XH_XmlHttpCreate();
+    XH_XmlHttpPOST(xh, url, CS_postData(args), callback);
+  });
+}
+
+
+/**
+ * Helper function to strip leading junk characters from a JSON response
+ * and then parse it into a JS constant.
+ *
+ * The reason that "}])'\n" is prepended to the response text is that
+ * it makes it impossible for a hacker to hit one of our JSON servlets
+ * via a <script src="..."> tag and do anything with the result.  Even
+ * though a JSON response is just a constant, it could be passed into
+ * hacker code by tricks such as overriding the array constructor.
+ */
+function CS_parseJSON(xhr) {
+  return JSON.parse(xhr.responseText.substr(5));
+}
+
+
+/**
+ * Promise-based version of CS_parseJSON using the fetch API.
+ *
+ * Sends a GET request to a JSON endpoint then strips the XSSI prefix off
+ * of the response before resolving the promise.
+ *
+ * Args:
+ *   url (string): The URL to fetch.
+ * Returns:
+ *   A promise, resolved when the request returns. Also be sure to call
+ *   .catch() on the promise (or wrap in a try/catch if using async/await)
+ *   if you don't want errors to halt script execution.
+ */
+function CS_fetch(url) {
+  return fetch(url, {credentials: 'same-origin'})
+    .then((res) => res.text())
+    .then((rawResponse) => JSON.parse(rawResponse.substr(5)));
+}
+
+
+/**
+ * After we refresh the form token, we need to actually submit the form.
+ * formToSubmit keeps track of which form the user was trying to submit.
+ */
+var formToSubmit = null;
+
+/**
+ * If the form token that was generated when the page was served has
+ * now expired, then request a refreshed token from the server, and
+ * don't submit the form until after it arrives.
+ */
+function refreshTokens(event, formToken, formTokenPath, tokenExpiresSec) {
+  if (!window.prpcClient.constructor.isTokenExpired(tokenExpiresSec)) {
+    return;
+  }
+
+  formToSubmit = event.target;
+  event.preventDefault();
+  const message = {
+    token: formToken,
+    tokenPath: formTokenPath,
+  };
+  const refreshTokenPromise = window.prpcClient.call(
+    'monorail.Sitewide', 'RefreshToken', message);
+
+  refreshTokenPromise.then((freshToken) => {
+    let tokenFields = document.querySelectorAll('input[name=token]');
+    for (let i = 0; i < tokenFields.length; ++i) {
+      tokenFields[i].value = freshToken.token;
+    }
+    if (formToSubmit) {
+      formToSubmit.submit();
+    }
+  });
+}
diff --git a/static/js/framework/framework-ajax_test.js b/static/js/framework/framework-ajax_test.js
new file mode 100644
index 0000000..c5218d3
--- /dev/null
+++ b/static/js/framework/framework-ajax_test.js
@@ -0,0 +1,37 @@
+/* Copyright 2016 The Chromium Authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+/**
+ * @fileoverview Tests for framework-ajax.js.
+ */
+
+var CS_env;
+
+function setUp() {
+  CS_env = {'token': 'd34db33f'};
+}
+
+function testPostData() {
+  assertEquals(
+    'token=d34db33f',
+    CS_postData({}));
+  assertEquals(
+    'token=d34db33f',
+    CS_postData({}, true));
+  assertEquals(
+    '',
+    CS_postData({}, false));
+  assertEquals(
+    'a=5&b=foo&token=d34db33f',
+    CS_postData({a: 5, b: 'foo'}));
+
+  let unescaped = {};
+  unescaped['f oo?'] = 'b&ar';
+  assertEquals(
+    'f%20oo%3F=b%26ar',
+    CS_postData(unescaped, false));
+}
diff --git a/static/js/framework/framework-cues.js b/static/js/framework/framework-cues.js
new file mode 100644
index 0000000..2c620a1
--- /dev/null
+++ b/static/js/framework/framework-cues.js
@@ -0,0 +1,38 @@
+/* Copyright 2016 The Chromium Authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+/**
+ * @fileoverview Simple functions for dismissible on-page help ("cues").
+ */
+
+/**
+ * Dimisses the cue.  This both updates the DOM and hits the server to
+ * record the fact that the user has dismissed it, so that it won't
+ * be shown again.
+ *
+ * If no security token is present, only the DOM is updated and
+ * nothing is recorded on the server.
+ *
+ * @param {string} cueId The identifier of the cue to hide.
+ * @return {boolean} false to cancel any event.
+ */
+function CS_dismissCue(cueId) {
+  let cueElements = document.querySelectorAll('.cue');
+  for (let i = 0; i < cueElements.length; ++i) {
+    cueElements[i].style.display = 'none';
+  }
+
+  if (CS_env.token) {
+    window.prpcClient.call(
+      'monorail.Users', 'SetUserPrefs',
+      {prefs: [{name: cueId, value: 'true'}]});
+  }
+  return false;
+}
+
+// Exports
+_CS_dismissCue = CS_dismissCue;
diff --git a/static/js/framework/framework-display.js b/static/js/framework/framework-display.js
new file mode 100644
index 0000000..9213e82
--- /dev/null
+++ b/static/js/framework/framework-display.js
@@ -0,0 +1,191 @@
+/* Copyright 2016 The Chromium Authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+/**
+ * Functions used by the Project Hosting to control the display of
+ * elements on the page, rollovers, and popup menus.
+ *
+ * Most of these functions are extracted from dit-display.js
+ */
+
+
+/**
+ * Hide the HTML element with the given ID.
+ * @param {string} id The HTML element ID.
+ * @return {boolean} Always returns false to cancel the browser event
+ *     if used as an event handler.
+ */
+function CS_hideID(id) {
+  $(id).style.display = 'none';
+  return false;
+}
+
+
+/**
+ * Show the HTML element with the given ID.
+ * @param {string} id The HTML element ID.
+ * @return {boolean} Always returns false to cancel the browser event
+ *     if used as an event handler.
+ */
+function CS_showID(id) {
+  $(id).style.display = '';
+  return false;
+}
+
+
+/**
+ * Hide the given HTML element.
+ * @param {Element} el The HTML element.
+ * @return {boolean} Always returns false to cancel the browser event
+ *     if used as an event handler.
+ */
+function CS_hideEl(el) {
+  el.style.display = 'none';
+  return false;
+}
+
+
+/**
+ * Show the given HTML element.
+ * @param {Element} el The HTML element.
+ * @return {boolean} Always returns false to cancel the browser event
+ *     if used as an event handler.
+ */
+function CS_showEl(el) {
+  el.style.display = '';
+  return false;
+}
+
+
+/**
+ * Show one element instead of another.  That is to say, show a new element and
+ * hide an old one.  Usually the element is the element that the user clicked
+ * on with the intention of "expanding it" to access the new element.
+ * @param {string} newID The ID of the HTML element to show.
+ * @param {Element} oldEl The HTML element to hide.
+ * @return {boolean} Always returns false to cancel the browser event
+ *     if used as an event handler.
+ */
+function CS_showInstead(newID, oldEl) {
+  $(newID).style.display = '';
+  oldEl.style.display = 'none';
+  return false;
+}
+
+/**
+ * Toggle the open/closed state of a section of the page.  As a result, CSS
+ * rules will make certain elements displayed and other elements hidden.  The
+ * section is some HTML element that encloses the element that the user clicked
+ * on.
+ * @param {Element} el The element that the user clicked on.
+ * @return {boolean} Always returns false to cancel the browser event
+ *     if used as an event handler.
+ */
+function CS_toggleHidden(el) {
+  while (el) {
+    if (el.classList.contains('closed')) {
+      el.classList.remove('closed');
+      el.classList.add('opened');
+      return false;
+    }
+    if (el.classList.contains('opened')) {
+      el.classList.remove('opened');
+      el.classList.add('closed');
+      return false;
+    }
+    el = el.parentNode;
+  }
+}
+
+
+/**
+ * Toggle the expand/collapse state of a section of the page.  As a result, CSS
+ * rules will make certain elements displayed and other elements hidden.  The
+ * section is some HTML element that encloses the element that the user clicked
+ * on.
+ * TODO(jrobbins): eliminate redundancy with function above.
+ * @param {Element} el The element that the user clicked on.
+ * @return {boolean} Always returns false to cancel the browser event
+ *     if used as an event handler.
+ */
+function CS_toggleCollapse(el) {
+  while (el) {
+    if (el.classList.contains('collapse')) {
+      el.classList.remove('collapse');
+      el.classList.add('expand');
+      return false;
+    }
+    if (el.classList.contains('expand')) {
+      el.classList.remove('expand');
+      el.classList.add('collapse');
+      return false;
+    }
+    el = el.parentNode;
+  }
+}
+
+
+/**
+ * Register a function for mouse clicks on the results table.  We
+ * listen on the table to avoid adding 1000 individual listeners on
+ * the cells.  This is needed because some browsers (now including
+ * Chrome) do not generate click events for mouse buttons other than
+ * the primary mouse button.  Chrome and Firefox generate auxclick
+ * events, but Edge does not.
+ */
+
+function CS_addClickListener(tableEl, handler) {
+  function maybeClick(event) {
+    const target = getTargetFromEvent(event);
+
+    const inLink = target.tagName == 'A' || target.parentNode.tagName == 'A';
+
+    if (inLink && !target.classList.contains('computehref')) {
+      // The <a> elements already have the correct hrefs.
+      return;
+    }
+    if (event.button == 2) {
+      // User is trying to open a context menu, not trying to navigate.
+      return;
+    }
+
+    let td = target;
+    while (td && td.tagName != 'TD' && td.tagName != 'TH') {
+      td = td.parentNode;
+    }
+    if (td.classList.contains('rowwidgets')) {
+      // User clicked on a checkbox.
+      return;
+    }
+    // User clicked on an issue ID link or text or cell.
+    event.preventDefault();
+    handler(event);
+  }
+  tableEl.addEventListener('click', maybeClick);
+  tableEl.addEventListener('auxclick', maybeClick);
+}
+
+function getTargetFromEvent(event) {
+  let target = event.target || event.srcElement;
+  if (target.shadowRoot) {
+  // Find the element within the shadowDOM.
+    const path = event.path || event.composedPath();
+    target = path[0];
+  }
+  return target;
+}
+
+
+// Exports
+_hideID = CS_hideID;
+_showID = CS_showID;
+_hideEl = CS_hideEl;
+_showEl = CS_showEl;
+_showInstead = CS_showInstead;
+_toggleHidden = CS_toggleHidden;
+_toggleCollapse = CS_toggleCollapse;
+_addClickListener = CS_addClickListener;
diff --git a/static/js/framework/framework-menu.js b/static/js/framework/framework-menu.js
new file mode 100644
index 0000000..35bbebc
--- /dev/null
+++ b/static/js/framework/framework-menu.js
@@ -0,0 +1,566 @@
+/* Copyright 2016 The Chromium Authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+/**
+ * @fileoverview This file represents a standalone, reusable drop down menu
+ * widget that can be attached to any element on a given page. It supports
+ * multiple instances of the widget on a page. It has no dependencies. Usage
+ * is as simple as creating a new Menu object and supplying it with a target
+ * element.
+ */
+
+/**
+ * The entry point and constructor for the Menu object. Creating
+ * a valid instance of this object will insert a drop down menu
+ * near the element supplied as the target, attach all the necessary
+ * events and insert the necessary elements on the page.
+ *
+ * @param {Element} target the target element on the page to which
+ *     the drop down menu will be placed near.
+ * @param {Function=} opt_onShow function to execute every time the
+ *     menu is made visible, most likely through a click on the target.
+ * @constructor
+ */
+var Menu = function(target, opt_onShow) {
+  this.iid = Menu.instance.length;
+  Menu.instance[this.iid] = this;
+  this.target = target;
+  this.onShow = opt_onShow || null;
+
+  // An optional trigger element on the page that can be used to trigger
+  // the drop-down. Currently hard-coded to be the same as the target element.
+  this.trigger = target;
+  this.items = [];
+  this.onOpenEvents = [];
+  this.menu = this.createElement('div', 'menuDiv instance' + this.iid);
+  this.targetId = this.target.getAttribute('id');
+  let menuId = (this.targetId != null) ?
+    'menuDiv-' + this.targetId : 'menuDiv-instance' + this.iid;
+  this.menu.setAttribute('id', menuId);
+  this.menu.role = 'listbox';
+  this.hide();
+  this.addCategory('default');
+  this.addEvent(this.trigger, 'click', this.toggle.bind(this));
+  this.addEvent(window, 'resize', this.adjustSizeAndLocation.bind(this));
+
+  // Hide the menu if a user clicks outside the menu widget
+  this.addEvent(document, 'click', this.hide.bind(this));
+  this.addEvent(this.menu, 'click', this.stopPropagation());
+  this.addEvent(this.trigger, 'click', this.stopPropagation());
+};
+
+// A reference to the element or node that the drop down
+// will appear next to
+Menu.prototype.target = null;
+
+// Element ID of the target. ID will be assigned to the newly created
+// menu div based on the target ID. A default ID will be
+// assigned If there is no ID on the target.
+Menu.prototype.targetId = null;
+
+/**
+ * A reference to the element or node that will trigger
+ * the drop down to appear. If not specified, this value
+ * will be the same as <Menu Instance>.target
+ * @type {Element}
+ */
+Menu.prototype.trigger = null;
+
+// A reference to the event type that will "open" the
+// menu div. By default this is the (on)click method.
+Menu.prototype.triggerType = null;
+
+// A reference to the element that will appear when the
+// trigger is clicked.
+Menu.prototype.menu = null;
+
+/**
+ * Function to execute every time the menu is made shown.
+ * @type {Function}
+ */
+Menu.prototype.onShow = null;
+
+// A list of category divs. By default these categories
+// are set to display none until at least one element
+// is placed within them.
+Menu.prototype.categories = null;
+
+// An id used to track timed intervals
+Menu.prototype.thread = -1;
+
+// The static instance id (iid) denoting which menu in the
+// list of Menu.instance items is this instantiated object.
+Menu.prototype.iid = -1;
+
+// A counter to indicate the number of items added with
+// addItem(). After 5 items, a height is set on the menu
+// and a scroll bar will appear.
+Menu.prototype.items = null;
+
+// A flag to detect whether or not a scroll bar has been added
+Menu.prototype.scrolls = false;
+
+// onOpen event handlers; each function in this list will
+// be executed and passed the executing instance as a
+// parameter before the menu is to be displayed.
+Menu.prototype.onOpenEvents = null;
+
+/**
+ * An extended short-cut for document.createElement(); this
+ * method allows the creation of an element, the assignment
+ * of one or more class names and the ability to set the
+ * content of the created element all with one function call.
+ * @param {string} element name of the element to create. Examples would
+ *     be 'div' or 'a'.
+ * @param {string} opt_className an optional string to assign to the
+ *     newly created element's className property.
+ * @param {string|Element} opt_content either a snippet of HTML or a HTML
+ *     element that is to be appended to the newly created element.
+ * @return {Element} a reference to the newly created element.
+ */
+Menu.prototype.createElement = function(element, opt_className, opt_content) {
+  let div = document.createElement(element);
+  div.className = opt_className;
+  if (opt_content) {
+    this.append(opt_content, div);
+  }
+  return div;
+};
+
+/**
+ * Uses a fairly browser agnostic approach to applying a callback to
+ * an element on the page.
+ *
+ * @param {Element|EventTarget} element a reference to an element on the page to
+ *     which to attach and event.
+ * @param {string} eventType a browser compatible event type as a string
+ *     without the sometimes assumed on- prefix. Examples: 'click',
+ *     'mousedown', 'mouseover', etc...
+ * @param {Function} callback a function reference to invoke when the
+ *     the event occurs.
+ */
+Menu.prototype.addEvent = function(element, eventType, callback) {
+  if (element.addEventListener) {
+    element.addEventListener(eventType, callback, false);
+  } else {
+    try {
+      element.attachEvent('on' + eventType, callback);
+    } catch (e) {
+      element['on' + eventType] = callback;
+    }
+  }
+};
+
+/**
+ * Similar to addEvent, this provides a specialied handler for onOpen
+ * events that apply to this instance of the Menu class. The supplied
+ * callbacks are appended to an internal array and called in order
+ * every time the menu is opened. The array can be accessed via
+ * menuInstance.onOpenEvents.
+ */
+Menu.prototype.addOnOpen = function(eventCallback) {
+  let eventIndex = this.onOpenEvents.length;
+  this.onOpenEvents.push(eventCallback);
+  return eventIndex;
+};
+
+/**
+ * This method will create a div with the classes .menuCategory and the
+ * name of the category as supplied in the first parameter. It then, if
+ * a title is supplied, creates a title div and appends it as well. The
+ * optional title is styled with the .categoryTitle and category name
+ * class.
+ *
+ * Categories are stored within the menu object instance for programmatic
+ * manipulation in the array, menuInstance.categories. Note also that this
+ * array is doubly linked insofar as that the category div can be accessed
+ * via it's index in the array as well as by instance.categories[category]
+ * where category is the string name supplied when creating the category.
+ *
+ * @param {string} category the string name used to create the category;
+ *     used as both a class name and a key into the internal array. It
+ *     must be a valid JavaScript variable name.
+ * @param {string|Element} opt_title this optional field is used to visibly
+ *     denote the category title. It can be either HTML or an element.
+ * @return {Element} the newly created div.
+ */
+Menu.prototype.addCategory = function(category, opt_title) {
+  this.categories = this.categories || [];
+  let categoryDiv = this.createElement('div', 'menuCategory ' + category);
+  categoryDiv._categoryName = category;
+  if (opt_title) {
+    let categoryTitle = this.createElement('b', 'categoryTitle ' +
+          category, opt_title);
+    categoryTitle.style.display = 'block';
+    this.append(categoryTitle);
+    categoryDiv._categoryTitle = categoryTitle;
+  }
+  this.append(categoryDiv);
+  this.categories[this.categories.length] = this.categories[category] =
+      categoryDiv;
+
+  return categoryDiv;
+};
+
+/**
+ * This method removes the contents of a given category but does not
+ * remove the category itself.
+ */
+Menu.prototype.emptyCategory = function(category) {
+  if (!this.categories[category]) {
+    return;
+  }
+  let div = this.categories[category];
+  for (let i = div.childNodes.length - 1; i >= 0; i--) {
+    div.removeChild(div.childNodes[i]);
+  }
+};
+
+/**
+ * This function is the most drastic of the cleansing functions; it removes
+ * all categories and all menu items and all HTML snippets that have been
+ * added to this instance of the Menu class.
+ */
+Menu.prototype.clear = function() {
+  for (var i = 0; i < this.categories.length; i++) {
+    // Prevent memory leaks
+    this.categories[this.categories[i]._categoryName] = null;
+  }
+  this.items.splice(0, this.items.length);
+  this.categories.splice(0, this.categories.length);
+  this.categories = [];
+  this.items = [];
+  for (var i = this.menu.childNodes.length - 1; i >= 0; i--) {
+    this.menu.removeChild(this.menu.childNodes[i]);
+  }
+};
+
+/**
+ * Passed an instance of a menu item, it will be removed from the menu
+ * object, including any residual array links and possible memory leaks.
+ * @param {Element} item a reference to the menu item to remove.
+ * @return {Element} returns the item removed.
+ */
+Menu.prototype.removeItem = function(item) {
+  let result = null;
+  for (let i = 0; i < this.items.length; i++) {
+    if (this.items[i] == item) {
+      result = this.items[i];
+      this.items.splice(i, 1);
+    }
+    // Renumber
+    this.items[i].item._index = i;
+  }
+  return result;
+};
+
+/**
+ * Removes a category from the menu element and all of its children thus
+ * allowing the Element to be collected by the browsers VM.
+ * @param {string} category the name of the category to retrieve and remove.
+ */
+Menu.prototype.removeCategory = function(category) {
+  let div = this.categories[category];
+  if (!div || !div.parentNode) {
+    return;
+  }
+  if (div._categoryTitle) {
+    div._categoryTitle.parentNode.removeChild(div._categoryTitle);
+  }
+  div.parentNode.removeChild(div);
+  for (var i = 0; i < this.categories.length; i++) {
+    if (this.categories[i] === div) {
+      this.categories[this.categories[i]._categoryName] = null;
+      this.categories.splice(i, 1);
+      return;
+    }
+  }
+  for (var i = 0; i < div.childNodes.length; i++) {
+    if (div.childNodes[i]._index) {
+      this.items.splice(div.childNodes[i]._index, 1);
+    } else {
+      this.removeItem(div.childNodes[i]);
+    }
+  }
+};
+
+/**
+ * This heart of the menu population scheme, the addItem function creates
+ * a combination of elements that visually form up a menu item. If no
+ * category is supplied, the default category is used. The menu item is
+ * an <a> tag with the class .menuItem. The menu item is directly styled
+ * as a block element. Other than that, all styling should be done via a
+ * external CSS definition.
+ *
+ * @param {string|Element} html_or_element a string of HTML text or a
+ *     HTML element denoting the contents of the menu item.
+ * @param {string} opt_href the href of the menu item link. This is
+ *     the most direct way of defining the menu items function.
+ *     [Default: '#'].
+ * @param {string} opt_category the category string name of the category
+ *     to append the menu item to. If the category doesn't exist, one will
+ *     be created. [Default: 'default'].
+ * @param {string} opt_title used when creating a new category and is
+ *     otherwise ignored completely. It is also ignored when supplied if
+ *     the named category already exists.
+ * @return {Element} returns the element that was created.
+ */
+Menu.prototype.addItem = function(html_or_element, opt_href, opt_category,
+  opt_title) {
+  let category = opt_category ? (this.categories[opt_category] ||
+                                 this.addCategory(opt_category, opt_title)) :
+    this.categories['default'];
+  let menuHref = (opt_href == undefined ? '#' : opt_href);
+  let menuItem = undefined;
+  if (menuHref) {
+    menuItem = this.createElement('a', 'menuItem', html_or_element);
+  } else {
+    menuItem = this.createElement('span', 'menuText', html_or_element);
+  }
+  let itemText = typeof html_or_element == 'string' ? html_or_element :
+    html_or_element.textContent || 'ERROR';
+
+  menuItem.style.display = 'block';
+  if (menuHref) {
+    menuItem.setAttribute('href', menuHref);
+  }
+  menuItem._index = this.items.length;
+  menuItem.role = 'option';
+  this.append(menuItem, category);
+  this.items[this.items.length] = {item: menuItem, text: itemText};
+
+  return menuItem;
+};
+
+/**
+ * Adds a visual HTML separator to the menu, optionally creating a
+ * category as per addItem(). See above.
+ * @param {string} opt_category the category string name of the category
+ *     to append the menu item to. If the category doesn't exist, one will
+ *     be created. [Default: 'default'].
+ * @param {string} opt_title used when creating a new category and is
+ *     otherwise ignored completely. It is also ignored when supplied if
+ *     the named category already exists.
+ */
+Menu.prototype.addSeparator = function(opt_category, opt_title) {
+  let category = opt_category ? (this.categories[opt_category] ||
+                                 this.addCategory(opt_category, opt_title)) :
+    this.categories['default'];
+  let hr = this.createElement('hr', 'menuSeparator');
+  this.append(hr, category);
+};
+
+/**
+ * This method performs all the dirty work of positioning the menu. It is
+ * responsible for dynamic sizing, insertion and deletion of scroll bars
+ * and calculation of offscreen width considerations.
+ */
+Menu.prototype.adjustSizeAndLocation = function() {
+  let style = this.menu.style;
+  style.position = 'absolute';
+
+  let firstCategory = null;
+  for (let i = 0; i < this.categories.length; i++) {
+    this.categories[i].className = this.categories[i].className.
+      replace(/ first/, '');
+    if (this.categories[i].childNodes.length == 0) {
+      this.categories[i].style.display = 'none';
+    } else {
+      this.categories[i].style.display = '';
+      if (!firstCategory) {
+        firstCategory = this.categories[i];
+        firstCategory.className += ' first';
+      }
+    }
+  }
+
+  let alreadyVisible = style.display != 'none' &&
+      style.visibility != 'hidden';
+  let docElemWidth = document.documentElement.clientWidth;
+  let docElemHeight = document.documentElement.clientHeight;
+  let pageSize = {
+    w: (window.innerWidth || docElemWidth && docElemWidth > 0 ?
+      docElemWidth : document.body.clientWidth) || 1,
+    h: (window.innerHeight || docElemHeight && docElemHeight > 0 ?
+      docElemHeight : document.body.clientHeight) || 1,
+  };
+  let targetPos = this.find(this.target);
+  let targetSize = {w: this.target.offsetWidth,
+    h: this.target.offsetHeight};
+  let menuSize = {w: this.menu.offsetWidth, h: this.menu.offsetHeight};
+
+  if (!alreadyVisible) {
+    let oldVisibility = style.visibility;
+    let oldDisplay = style.display;
+    style.visibility = 'hidden';
+    style.display = '';
+    style.height = '';
+    style.width = '';
+    menuSize = {w: this.menu.offsetWidth, h: this.menu.offsetHeight};
+    style.display = oldDisplay;
+    style.visibility = oldVisibility;
+  }
+
+  let addScroll = (this.menu.offsetHeight / pageSize.h) > 0.8;
+  if (addScroll) {
+    menuSize.h = parseInt((pageSize.h * 0.8), 10);
+    style.height = menuSize.h + 'px';
+    style.overflowX = 'hidden';
+    style.overflowY = 'auto';
+  } else {
+    style.height = style.overflowY = style.overflowX = '';
+  }
+
+  style.top = (targetPos.y + targetSize.h) + 'px';
+  style.left = targetPos.x + 'px';
+
+  if (menuSize.w < 175) {
+    style.width = '175px';
+  }
+
+  if (addScroll) {
+    style.width = parseInt(style.width, 10) + 13 + 'px';
+  }
+
+  if ((targetPos.x + menuSize.w) > pageSize.w) {
+    style.left = targetPos.x - (menuSize.w - targetSize.w) + 'px';
+  }
+};
+
+
+/**
+ * This function is used heavily, internally. It appends text
+ * or the supplied element via appendChild(). If
+ * the opt_target variable is present, the supplied element will be
+ * the container rather than the menu div for this instance.
+ *
+ * @param {string|Element} text_or_element the html or element to insert
+ *     into opt_target.
+ * @param {Element} opt_target the target element it should be appended to.
+ *
+ */
+Menu.prototype.append = function(text_or_element, opt_target) {
+  let element = opt_target || this.menu;
+  if (typeof opt_target == 'string' && this.categories[opt_target]) {
+    element = this.categories[opt_target];
+  }
+  if (typeof text_or_element == 'string') {
+    element.textContent += text_or_element;
+  } else {
+    element.appendChild(text_or_element);
+  }
+};
+
+/**
+ * Displays the menu (such as upon mouseover).
+ */
+Menu.prototype.over = function() {
+  if (this.menu.style.display != 'none') {
+    this.show();
+  }
+  if (this.thread != -1) {
+    clearTimeout(this.thread);
+    this.thread = -1;
+  }
+};
+
+/**
+ * Hides the menu (such as upon mouseout).
+ */
+Menu.prototype.out = function() {
+  if (this.thread != -1) {
+    clearTimeout(this.thread);
+    this.thread = -1;
+  }
+  this.thread = setTimeout(this.hide.bind(this), 400);
+};
+
+/**
+ * Stops event propagation.
+ */
+Menu.prototype.stopPropagation = function() {
+  return (function(e) {
+    if (!e) {
+      e = window.event;
+    }
+    e.cancelBubble = true;
+    if (e.stopPropagation) {
+      e.stopPropagation();
+    }
+  });
+};
+
+/**
+ * Toggles the menu between hide/show.
+ */
+Menu.prototype.toggle = function(event) {
+  event.preventDefault();
+  if (this.menu.style.display == 'none') {
+    this.show();
+  } else {
+    this.hide();
+  }
+};
+
+/**
+ * Makes the menu visible, then calls the user-supplied onShow callback.
+ */
+Menu.prototype.show = function() {
+  if (this.menu.style.display != '') {
+    for (var i = 0; i < this.onOpenEvents.length; i++) {
+      this.onOpenEvents[i].call(null, this);
+    }
+
+    // Invisibly show it first
+    this.menu.style.visibility = 'hidden';
+    this.menu.style.display = '';
+    this.adjustSizeAndLocation();
+    if (this.trigger.nodeName && this.trigger.nodeName == 'A') {
+      this.trigger.blur();
+    }
+    this.menu.style.visibility = 'visible';
+
+    // Hide other menus
+    for (var i = 0; i < Menu.instance.length; i++) {
+      let menuInstance = Menu.instance[i];
+      if (menuInstance != this) {
+        menuInstance.hide();
+      }
+    }
+
+    if (this.onShow) {
+      this.onShow();
+    }
+  }
+};
+
+/**
+ * Makes the menu invisible.
+ */
+Menu.prototype.hide = function() {
+  this.menu.style.display = 'none';
+};
+
+Menu.prototype.find = function(element) {
+  let curleft = 0, curtop = 0;
+  if (element.offsetParent) {
+    do {
+      curleft += element.offsetLeft;
+      curtop += element.offsetTop;
+    }
+    while ((element = element.offsetParent) && (element.style &&
+          element.style.position != 'relative' &&
+          element.style.position != 'absolute'));
+  }
+  return {x: curleft, y: curtop};
+};
+
+/**
+ * A static array of object instances for global reference.
+ * @type {Array.<Menu>}
+ */
+Menu.instance = [];
diff --git a/static/js/framework/framework-myhotlists.js b/static/js/framework/framework-myhotlists.js
new file mode 100644
index 0000000..6459090
--- /dev/null
+++ b/static/js/framework/framework-myhotlists.js
@@ -0,0 +1,109 @@
+/* Copyright 2016 The Chromium Authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+/**
+ * @fileoverview This file initializes the "My Hotlists" drop down menu in the
+ *     user bar. It utilizes the menu widget defined in framework-menu.js.
+ */
+
+/** @type {Menu} */
+var myhotlists;
+
+(function() {
+  var target = document.getElementById('hotlists-dropdown');
+
+  if (!target) {
+    return;
+  }
+
+  myhotlists = new Menu(target, function() {});
+
+  myhotlists.addEvent(window, 'load', CS_updateHotlists);
+  myhotlists.addOnOpen(CS_updateHotlists);
+  myhotlists.addEvent(window, 'load', function() {
+    document.body.appendChild(myhotlists.menu);
+  });
+})();
+
+
+/**
+ * Grabs the list of logged in user's hotlists to populate the "My Hotlists"
+ * drop down menu.
+ */
+async function CS_updateHotlists() {
+  if (!myhotlists) return;
+
+  if (!window.CS_env.loggedInUserEmail) {
+    myhotlists.clear();
+    myhotlists.addItem('sign in to see your hotlists',
+                       window.CS_env.login_url,
+                       'controls');
+    return;
+  }
+
+  const ownedHotlistsMessage = {
+    user: {
+      display_name: window.CS_env.loggedInUserEmail,
+    }};
+
+  const responses = await Promise.all([
+    window.prpcClient.call(
+      'monorail.Features', 'ListHotlistsByUser', ownedHotlistsMessage),
+    window.prpcClient.call(
+      'monorail.Features', 'ListStarredHotlists', {}),
+    window.prpcClient.call(
+      'monorail.Features', 'ListRecentlyVisitedHotlists', {}),
+  ]);
+  const ownedHotlists = responses[0];
+  const starredHotlists = responses[1];
+  const visitedHotlists = responses[2];
+
+  myhotlists.clear();
+
+  const sortByName = (hotlist1, hotlist2) => {
+    hotlist1.name.localeCompare(hotlist2.name);
+  };
+
+  if (ownedHotlists.hotlists) {
+    ownedHotlists.hotlists.sort(sortByName);
+    ownedHotlists.hotlists.forEach(hotlist => {
+      const name = hotlist.name;
+      const userId = hotlist.ownerRef.userId;
+      const url = `/u/${userId}/hotlists/${name}`;
+      myhotlists.addItem(name, url, 'hotlists', 'Hotlists');
+    });
+  }
+
+  if (starredHotlists.hotlists) {
+    myhotlists.addSeparator();
+    starredHotlists.hotlists.sort(sortByName);
+    starredHotlists.hotlists.forEach(hotlist => {
+      const name = hotlist.name;
+      const userId = hotlist.ownerRef.userId;
+      const url = `/u/${userId}/hotlists/${name}`;
+      myhotlists.addItem(name, url, 'starred_hotlists', 'Starred Hotlists');
+    });
+  }
+
+  if (visitedHotlists.hotlists) {
+    myhotlists.addSeparator();
+    visitedHotlists.hotlists.sort(sortByName);
+    visitedHotlists.hotlists.forEach(hotlist => {
+      const name = hotlist.name;
+      const userId = hotlist.ownerRef.userId;
+      const url = `/u/${userId}/hotlists/${name}`;
+      myhotlists.addItem(
+          name, url, 'visited_hotlists', 'Recently Visited Hotlists');
+    });
+  }
+
+  myhotlists.addSeparator();
+  myhotlists.addItem(
+      'All hotlists', `/u/${window.CS_env.loggedInUserEmail}/hotlists`,
+      'controls');
+  myhotlists.addItem('Create hotlist', '/hosting/createHotlist', 'controls');
+}
diff --git a/static/js/framework/framework-stars.js b/static/js/framework/framework-stars.js
new file mode 100644
index 0000000..946264e
--- /dev/null
+++ b/static/js/framework/framework-stars.js
@@ -0,0 +1,114 @@
+/* Copyright 2016 The Chromium Authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+/**
+ * This file contains JS functions that support setting and showing
+ * stars throughout Monorail.
+ */
+
+
+/**
+ * The character to display when the user has starred an issue.
+ */
+var TKR_STAR_ON = '\u2605';
+
+
+/**
+ * The character to display when the user has not starred an issue.
+ */
+var TKR_STAR_OFF = '\u2606';
+
+
+/**
+ * Function to toggle the star on an issue.  Does both an update of the
+ * DOM and hit the server to record the star.
+ *
+ * @param {Element} el The star <a> element.
+ * @param {String} projectName name of the project to be starred, or name of
+ *                 the project containing the issue to be starred.
+ * @param {Integer} localId number of the issue to be starred.
+ * @param {String} projectName number of the user to be starred.
+ */
+function TKR_toggleStar(el, projectName, localId, userId, hotlistId) {
+  const starred = (el.textContent.trim() == TKR_STAR_OFF);
+  TKR_toggleStarLocal(el);
+
+  const starRequestMessage = {starred: Boolean(starred)};
+  if (userId) {
+    starRequestMessage.user_ref = {user_id: userId};
+    window.prpcClient.call('monorail.Users', 'StarUser', starRequestMessage);
+  } else if (projectName && localId) {
+    starRequestMessage.issue_ref = {
+      project_name: projectName,
+      local_id: localId,
+    };
+    window.prpcClient.call('monorail.Issues', 'StarIssue', starRequestMessage);
+  } else if (projectName) {
+    starRequestMessage.project_name = projectName;
+    window.prpcClient.call(
+      'monorail.Projects', 'StarProject', starRequestMessage);
+  } else if (hotlistId) {
+    starRequestMessage.hotlist_ref = {hotlist_id: hotlistId};
+    window.prpcClient.call(
+      'monorail.Features', 'StarHotlist', starRequestMessage);
+  }
+}
+
+
+/**
+ * Just update the display state of a star, without contacting the server.
+ * Optionally update the value of a form element as well. Useful for when
+ * a user is entering a new issue and wants to set its initial starred state.
+ * @param {Element} el Star <img> element.
+ * @param {string} opt_formElementId HTML ID of the hidden form element for
+ *      stars.
+ */
+function TKR_toggleStarLocal(el, opt_formElementId) {
+  let starred = (el.textContent.trim() == TKR_STAR_OFF) ? 1 : 0;
+
+  el.textContent = starred ? TKR_STAR_ON : TKR_STAR_OFF;
+  el.style.color = starred ? 'cornflowerblue' : 'grey';
+  el.title = starred ? 'You have starred this item' : 'Click to star this item';
+
+  if (opt_formElementId) {
+    $(opt_formElementId).value = '' + starred; // convert to string
+  }
+}
+
+
+/**
+ * When we show two star icons on the same details page, keep them
+ * in sync with each other. And, update a message about starring
+ * that is displayed near the issue update form.
+ * @param {Element} clickedStar The star that the user clicked on.
+ * @param {string} otherStarId ID of the other star icon.
+ */
+function TKR_syncStarIcons(clickedStar, otherStarId) {
+  let otherStar = document.getElementById(otherStarId);
+  if (!otherStar) {
+    return;
+  }
+  TKR_toggleStarLocal(otherStar);
+
+  let vote_feedback = document.getElementById('vote_feedback');
+  if (!vote_feedback) {
+    return;
+  }
+
+  if (clickedStar.textContent == TKR_STAR_OFF) {
+    vote_feedback.textContent =
+        'Vote for this issue and get email change notifications.';
+  } else {
+    vote_feedback.textContent = 'Your vote has been recorded.';
+  }
+}
+
+
+// Exports
+_TKR_toggleStar = TKR_toggleStar;
+_TKR_toggleStarLocal = TKR_toggleStarLocal;
+_TKR_syncStarIcons = TKR_syncStarIcons;
diff --git a/static/js/framework/project-name-check.js b/static/js/framework/project-name-check.js
new file mode 100644
index 0000000..65c2bdf
--- /dev/null
+++ b/static/js/framework/project-name-check.js
@@ -0,0 +1,30 @@
+/* Copyright 2016 The Chromium Authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+/**
+ * @fileoverview Functions that support project name checks when
+ * creating a new project.
+ */
+
+/**
+ * Function that communicates with the server.
+ * @param {string} projectName The proposed project name.
+ */
+async function checkProjectName(projectName) {
+  const message = {
+    project_name: projectName
+  };
+  const response = await window.prpcClient.call(
+      'monorail.Projects', 'CheckProjectName', message);
+  if (response.error) {
+    $('projectnamefeedback').textContent = response.error;
+    $('submit_btn').disabled = 'disabled';
+  }
+}
+
+// Make this function globally available
+_CP_checkProjectName = checkProjectName;