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();
+
+})();