Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static/third_party/js/skipper.js b/static/third_party/js/skipper.js
new file mode 100644
index 0000000..4c131b1
--- /dev/null
+++ b/static/third_party/js/skipper.js
@@ -0,0 +1,335 @@
+/**
+ * Copyright 2008 Steve McKay.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Kibbles.Skipper is a Javascript library providing support for keyboard
+ * navigation among DOM object on a page.
+ */
+(function(){
+
+var _stops = new Array();  // list of stop objects
+var _lastStop;  // id of the last stop we visited to.
+
+// Named options. The value can be a literal value, or a function to call.
+var _options = {
+	padding_top: 0, // window offset when scrolling
+  padding_bottom: 0,
+  scroll_window: true
+};
+
+/*
+ * Constants identifying listener types. Used with the method that
+ * enables registration of listeners.
+ */
+var _LISTENER_TYPE = {
+  PRE: 'pre',
+  POST: 'post'
+};
+
+// map of stop listeners by type. pre listeners are called before navigation
+// post listeners are called after navigation.
+var _stopListener = {
+	pre: [],
+	post: []
+};
+
+/**
+ * Remove all stop previously identified stop elements.
+ */
+function _reset() {
+	_stops = new Array();
+}
+
+function _get(i) {
+	return _stops[i];
+}
+
+function _set(i, element) {
+	_stops[i] = element;
+}
+
+function _insert(i, element) {
+  if (i < 0 || i > _stops.length - 1) {
+    throw "Index out of bounds.";
+  }
+	_stops.splice(i, 0, element);
+  if (i <= _lastStop) {
+    _lastStop++;
+  }
+}
+
+function _append(element) {
+	_stops.push(element);
+}
+
+function _del(i) {
+  if (i < 0 || i > _stops.length - 1) {
+    throw "Index out of bounds.";
+  }
+	_stops.splice(i, 1);
+  if (_lastStop >= i) {
+    _lastStop--;
+  }
+}
+
+function _length() {
+	return _stops.length;
+}
+
+/**
+ * Sets the named option to the specified value.
+ */
+function _setOption(name, value) {
+	_options[name] = value;
+}
+
+/**
+ * Register a key to move forward one stop.
+ */
+function _addFwdKey(character) {
+	kibbles.keys.addKeyPressListener(character, _gotoNextStop);
+}
+
+/**
+ * Register a key to move back one stop.
+ */
+function _addRevKey(character) {
+	kibbles.keys.addKeyPressListener(character, _gotoPreviousStop);
+}
+
+/**
+ * Adds a stop listener.
+ */
+function _addStopListener(type, handler) {
+	if (type == _LISTENER_TYPE.PRE) {
+		_stopListener.pre.push(handler);
+	} else if (type == _LISTENER_TYPE.POST) {
+		_stopListener.post.push(handler);
+	}
+}
+
+/**
+ * Scroll to next stop if any.
+ */
+function _gotoNextStop() {
+	_setCurrentStop(_getNextStop());
+}
+
+/**
+ * Scroll to previous stop if any.
+ */
+function _gotoPreviousStop() {
+	_setCurrentStop(_getPreviousStop());
+}
+
+/**
+ * Update the current and previous stops, scrolling window to the location
+ * of the specified stop, and notifying listeners in the process.
+ */
+function _setCurrentStop(i) {
+	if (i >= 0) {
+		var prevStop = _lastStop;
+		_lastStop = i;
+
+    var next = new Stop(i);
+    var prev = (prevStop >= 0) ? new Stop(prevStop) : undefined;
+
+		_notifyListeners(next, prev, _stopListener.pre);
+
+		// If the y coord of the stop was not previously determined
+		// it may have been hidden. Since "PRE" listeners may reveal
+		// hidden stops, we try again if "y" is not know.
+		if (!next.y) next.y = _findObjectPosition(next.element);
+
+		// if we can't id the y coords at this point, we throw an exception.
+		if (!next.y && !(next.y >= 0)) {
+			throw "Next stop does not y coords. Aborting.";
+		}
+		_notifyListeners(next, prev, _stopListener.post);
+	}
+}
+
+/**
+ * Called by a listener, not directly.
+ */
+function _scrollOpportunityListener(next, prev) {
+  if (!_getOptionValue('scroll_window')) return;
+
+  if (next && next.element) {
+
+    var viewTop = _windowScrollTop();
+    var viewBottom = viewTop + document.documentElement.clientHeight;
+
+    var padTop = _getOptionValue('padding_top');
+
+    var bottom = viewBottom - padTop;
+
+    // if we skipped below the bottom padding
+    if (next.y > bottom) {
+      window.scrollTo(0, next.y - padTop);
+      return;
+    }
+
+    var padBottom = _getOptionValue('padding_bottom');
+    // if we skipped above the top offset
+    var top = viewTop + padBottom;
+    if (next.y < top) {
+      window.scrollTo(0, (next.y - document.documentElement.clientHeight) + padBottom);
+      return;
+    }
+  }
+}
+
+function _windowScrollTop() {
+  if (window.document.body.scrollTop) {
+    return window.document.body.scrollTop;
+  } else if (window.document.documentElement.scrollTop) {
+    return window.document.documentElement.scrollTop;
+  } else if (window.pageYOffset) {
+    return window.pageYOffset;
+  }
+  return 0;
+}
+
+
+/**
+ * Returns an option value or if the value is a function,
+ * the value returned by the function.
+ */
+function _getOptionValue(name) {
+	var opt = _options[name];
+	if (typeof opt == "function") {
+		return opt();
+	}
+	return opt;
+}
+
+/**
+ * Notify all supplied stop listeners.
+ */
+function _notifyListeners(stop, previousStop, listeners) {
+	if (stop && listeners) {
+		try {
+			for (var i = 0; i < listeners.length; i++) {
+				listeners[i](stop, previousStop);
+			}
+		} catch(err) {
+			// don't let a grumpy listener bring us down.
+		}
+	}
+}
+
+/**
+ * Returns the next stop or null if none stop available.
+ */
+function _getNextStop() {
+	var i = 0;
+
+	// if we've already visited a stop, use that as the base for the next stop.
+	if (_lastStop >= 0) {
+		i = _lastStop + 1;
+	}
+
+	// if the presumed next stop is out of bounds, return null.
+	if (i > _stops.length - 1) {
+		return;
+	}
+  return i;
+}
+
+/**
+ * Returns the previous stop or null if none available.
+ */
+function _getPreviousStop() {
+	var i = _stops.length - 1;
+
+	// if we've already visited a stop, use that as the base for the next stop.
+	if (_lastStop >= 0) {
+		i = _lastStop - 1;
+	}
+
+	// if the presumed next stop is out of bounds, return null.
+	if (i < 0) {
+		return;
+	}
+  return i;
+}
+
+/**
+ * Convenience wrapper for "stop" related information.
+ */
+function Stop(i, y) {
+	this.index = i;
+	this.element = _stops[i];
+	this.y = _findObjectPosition(this.element);
+}
+
+/**
+ * Returns the vertical coordinate of the top of specified object
+ * relative to the top of the entire page.
+ */
+function _findObjectPosition(obj) {
+	if (obj) {
+		var curtop = 0;
+		if (obj.offsetParent) {
+			while (obj.offsetParent) {
+				curtop += obj.offsetTop;
+				obj = obj.offsetParent;
+			}
+		} else if (obj.y) {
+			curtop += obj.y;
+		}
+		return curtop;
+	}
+	return null;
+}
+
+if (!window.kibbles.keys) {
+  throw "Kibbles.Skipper requires Kibbles.Keys which is not loaded."
+      + " Can't continue.";
+}
+
+/**
+ * A nice little namespace to call our own.
+ *
+ * Formalizing Kibbles.Skipper as a traditional javascript class caused
+ * headaches with respect to capturing the context (what is "this"
+ * at any point in time). So we use a simple script exported via the
+ * "kibbles.skipper" namespace.
+ */
+window.kibbles.skipper = {
+	setOption: _setOption,
+	addFwdKey: _addFwdKey,
+	addRevKey: _addRevKey,
+	LISTENER_TYPE: _LISTENER_TYPE,
+	addStopListener: _addStopListener,
+  setCurrentStop: _setCurrentStop,
+	// array like methods for stop manipulation
+	get: _get,
+	set: _set,
+	append: _append,
+	insert: _insert,
+	del: _del,
+	length: _length,
+	reset: _reset
+}
+
+_addStopListener(kibbles.skipper.LISTENER_TYPE.POST, _scrollOpportunityListener)
+
+// we depend on kibbles.keys.
+kibbles.keys.listen();
+
+})();